WKWebView默認緩存策略與HTTP緩存協議

今天同事反應H5更新了資源,但iOS App裏面仍然使用的是舊的緩存資源。爲何會這樣呢?要弄清楚這個問題,首先得弄清楚WKWebView的緩存原理。css

1、WKWebView默認緩存策略

下圖是蘋果官方文檔提供的默認緩存策略(NSURLRequestUseProtocolCachePolicy)的流程圖。html

默認緩存策略圖(來源蘋果官方)

官方文檔上是這樣描述的:web

For the HTTP and HTTPS protocols, NSURLRequestUseProtocolCachePolicy performs the following behavior:
1. If a cached response does not exist for the request, the URL loading system fetches the data from the originating source.
2. Otherwise, if the cached response does not indicate that it must be revalidated every time, and if the cached response is not stale (past its expiration date), the URL loading system returns the cached response.
3. If the cached response is stale or requires revalidation, the URL loading system makes a HEAD request to the originating source to see if the resource has changed. If so, the URL loading system fetches the data from the originating source. Otherwise, it returns the cached response.ajax

官方文檔說,算法

  1. 緩存不存在,則直接請求。
  2. 緩存存在,且緩存response頭沒有指明每次必須校驗資源更新(revalidated這個詞可能會產生誤導,後文說),且緩存沒有過時,則系統會直接返回緩存,不會發起請求
  3. 若是緩存過時了或者要求每次必須校驗資源更新,則會發起一個校驗資源更新的請求,若是(服務器告訴客戶端)資源有更新則使用服務器返回來的新數據,若是資源沒有更新則使用本地緩存。

上面官方文檔只是說了個大概的原理,具體指標和細節並無說清楚。瀏覽器

  1. 什麼狀況下會緩存數據?
  2. 什麼狀況下每次都須要校驗資源更新?
  3. 緩存過時時間是多久?
  4. 校驗資源更新的過程是怎麼樣的?revalidated的指標是什麼?

實際上,WKWebView默認緩存策略徹底遵循HTTP緩存協議,蘋果並無作額外的事情,上面的流程圖和文檔描述只是簡略描述了HTTP緩存協議的一個流程。也就是說,你想弄清楚WKWebView默認緩存策略,你得弄清楚HTTP緩存協議緩存

2、HTTP緩存協議

http緩存協議這個詞是我本身造的哈,本節要講的實際上就是HTTP協議中和緩存有關的請求頭、響應頭的做用和用法。bash

客戶端默認緩存行爲其實是由服務器控制的,客戶端和服務器經過HTTP請求頭和響應頭中的緩存字段來交流,進而影響客戶端的行爲。
下面就來介紹一下相關字段。服務器

1. Pragma、Expires

在 http1.0 時代,給客戶端設定緩存方式可經過這兩個字段。網絡

Pragma是一個通用頭,它只有no-cache這一個值。

通用頭:該字段能夠用於請求頭,也可用於響應頭。(注意:同一個屬性在請求頭和響應頭中意義可能不同,例以下文中的Cache-Control)

做爲請求頭,表示不使用緩存,直接從源服務器獲取資源,這是HTTP1.0的用法,HTTP1.1的用法是Cache-Control:no-cache。不過爲了兼容HTTP1.0,通常Pragma:no-cache和Cache-Control:no-cache聯用,以下。

Cache-Control:no-cache
Pragme:no-cache
複製代碼

做爲響應頭,RFC2616文檔說,Pragma : no-cache的行爲並無被定義,不能保證它的意義和Cache-Control:no-cache一致。

Expires,響應頭,表示緩存過時的時刻,這個是服務器時間。例如
Expires: Fri, 11 Jun 2021 11:33:01 GMT

Pragma、Expires的侷限:響應報文中Expires所定義的緩存時間是相對服務器上的時間而言的,若是客戶端上的時間跟服務器上的時間不一致(特別是用戶修改了本身電腦的系統時間),那緩存時間可能就沒啥意義了。

Expires和Pragme的使用

2. Cache-Control

http1.1新增了 Cache-Control 來配置緩存信息,主要包括:可否緩存、緩存過時時間、是否每次校驗等。

Cache-Control是通用頭。

Cache-Control是通用頭

下圖是Cache-Control可選值表。你也能夠查閱HTTP官方文檔14.9Cache-Control部分。

Cache-Control可選值

Cache-Control 容許自由組合可選值,用逗號分隔。
Cache-Control: max-age=3600, no-cache
上面這句意思是,緩存過時時間是1小時,每次都必須向服務器進行資源更新校驗。

下面介紹幾個經常使用的可選值。

must-revalidate

文章開頭咱們提到了蘋果的流程圖可能會讓人產生歧義,這裏來解釋一下坑在哪裏。
蘋果文檔和流程圖中有個判斷,緩存存在,則須要判斷是否須要每次都校驗,用的是「revalidated」這個詞。而後你看到Cache-Control可選值裏面有個must-revalidate值,你是否是絕不猶豫地就向下面這樣寫了。
Cache-Control: max-age=3600, must-revalidate
我就嘗試設置一個過時時間,可是又但願每次都去校驗更新,因而我像上面這樣寫,結果客戶端仍然是用的緩存,根本沒有網絡請求發出去。 我很幸運地看到了這篇文章,多是最被誤用的 HTTP 響應頭之一 Cache-Control: must-revalidate,強烈推薦閱讀!

HTTP 規範是不容許客戶端使用過時緩存的,除了一些特殊狀況,好比校驗請求發送失敗的時候。而must-revalidate指令是用來排除這些特殊狀況的。帶有 must-revalidate 的緩存過時後,在任何狀況下,都必須成功 revalidate 後才能使用,沒有例外,即便校驗請求發送失敗也不可使用過時的緩存。也就是說,有個大前提是緩存過時了,若是緩存沒過時客戶端會直接使用緩存,並不會發起校驗,顯然不是字面上每次都校驗更新的意思。must-revalidate 命名爲 never-return-stale更合理。而真正每次都校驗更新,應該用no-cache這個字段。
把上面錯誤的寫法改爲下面這樣就OK了:緩存有效期1小時,每次請求都校驗更新。
Cache-Control: max-age=3600, no-cache

no-cache

做爲請求頭,告知中間服務器不使用緩存,向源服務器發起請求。
做爲響應頭,no-cache並非字面上的不緩存,而是每次使用前都得先校驗一下資源更新。

no-store

做爲響應頭,帶有no-store的響應不會被緩存到任意的磁盤或者內存裏,no-store它纔是真正的「no-cache」。

max-age

做爲請求頭,max-age=0表示無論response怎麼設置,在從新獲取資源以前,先進行資源更新校驗。
做爲響應頭,max-age=x表示,緩存有效期是x秒。

Cache-Control的侷限

不少時候,緩存過時了可是資源並無修改,會發送多餘的請求和數據;或者資源修改了緩存還沒過時,客戶端仍然在用緩存。Cache-Control沒法及時和客戶端同步。

3. Last-Modified、If-Modified-Since

爲了彌補Cache-Control沒法及時判斷資源是否有更新的不足,有了Last-Modified、if-Modified-Since字段。

Last-Modified

響應頭,此次命名沒有問題了,這個字段的值就是資源在服務器上最後修改時刻。例如
If-Modified-Since: Thu, 31 Mar 2016 07:07:52 GMT

If-Modified-Since

請求頭,客戶端經過該字段把Last-Modified的值回傳給服務端;客戶端帶上這個字段表示此次請求是向服務端作校驗資源更新校驗。若是資源沒有修改,則服務端返回304不返回數據,客戶端用緩存;資源有修改則返回200和數據。 例如
If-Modified-Since: Thu, 31 Mar 2016 07:07:52 GMT

Last-Modified的啓發式(heuristic)緩存

HTTP/2 200
Date: Wed, 27 Mar 2019 22:00:00 GMT
Last-Modified: Wed, 27 Mar 2019 12:00:00 GMT
複製代碼

上面這個響應,沒有顯示地指明須要緩存,沒有Cache-Control,也沒有 Expires,只有Last-Modified修改時間,這種狀況會產生啓發式緩存。緩存時長=(date_value - last_modified_value) * 0.10 ,這是由 HTTP 規範推薦的算法,但規範中僅僅是推薦而已,並無作強制要求。好比 Firefox 中就在這個算法的基礎上還和 7 天時長取了一次最小值。
何如禁用由 Last-Modified響應頭形成的啓發式緩存:正確的作法是在響應頭中加上 Cache-Control: no-cache。

Last-Modified、If-Modified-Since的缺陷

沒法識別內容是否發生實質性的變化,可能只是修改了文件可是內容沒有變化;沒法識別一秒內進行屢次修改的狀況。

4. ETag、If-None-Match

爲了彌補Last-Modified的沒法判斷內容實質性變化的缺陷,因而有了ETag和If-None-Match字段,這對字段的用法和Last-Modified、If-Modified-Since類似,服務器在響應頭中返回ETag字段,客戶端在下次請求時在If-None-Match中回傳ETag對應的值。

ETag

響應頭,給資源計算得出一個惟一標誌符(好比md5標誌),加在響應頭裏一塊兒返給客戶端,例如
Etag: "5d8c72a5edda8d6a"

If-None-Match

請求頭,客戶端在下次請求時回傳ETag值給服務器。
If-None-Match: "5d8c72a5edda8d6a""

5. 優先級

上面這些緩存控制字段若是同時出現,他們的優先級如何呢?
優先級:Pragma > Cache-Control > Expires > Last-Modified > ETag
這是我在iOS下測試的出來的結論,僅供參考。下面是測試的過程。

響應頭沒有任何緩存字段,每次啓動都會發起請求,返回200。
第一次啓動,響應頭添加Pragma:no-cache和Cache-Control:max-age;第二次啓動,會發起請求,返回304,說明Pragma生效了,Pragma > Cache-Control。
第一次啓動,響應頭沒有過時時間,只有Last-Modified;第二次啓動,使用緩存,沒有發起請求,說明啓發式緩存(上文中有提到)生效。
第一次啓動,響應頭沒有過時時間,只有ETag;第二次啓動,會發起請求,返回304,說明作了資源更新校驗。
第一次啓動,響應頭沒有過時時間,同時有ETag和Last-Modified;第二次啓動,使用緩存,沒有發起請求,啓發式緩存生效,說明Last-Modified>ETag。

更多關於HTTP頭部字段,能夠查看HTTP協議官方文檔
全英文的,看着頭大?我還無心中發現了中文版的。火狐瀏覽器F12調出控制檯,請求頭和響應頭左邊的問號(下圖)是能夠點的!點擊直接跳轉到對用頭字段的網頁,真可謂「哪裏不會點哪裏,媽媽不再用擔憂個人學習了!」哈哈哈哈——

火狐瀏覽器問號能夠點擊

3、實戰:瀏覽器的行爲

介紹完上面的HTTP緩存協議,下面咱們來實戰一下,梳理下瀏覽器的整個交互過程,加深對上面各個字段的理解。
這裏再次拋出蘋果給的流程圖看一眼,實際上瀏覽器(不管是PC仍是移動端)的執行過程就是這個流程圖。

默認緩存策略圖(來源蘋果官方)

下面咱們結合上面的流程圖,以火狐瀏覽器、百度首頁的css文件例,一步步進行說明。不一樣瀏覽器的行爲可能不一致(刷新、強刷等操做瀏覽器會強行添加一些請求頭,不一樣瀏覽器可能添加的不同),可是他們遵循的HTTP協議規則是一致的。

1.第一次請求(至關於iOS第一次啓動)

第一次請求沒有緩存,瀏覽器發出請求。
咱們能夠看到,返回的響應頭中包含了Cache-Control、ETag、Expires、Last-Modified等多個緩存控制字段。瀏覽器進行緩存。

第一次請求

2.在瀏覽器地址欄直接回車(至關於iOS第二次啓動)

以下圖能夠看到,瀏覽器沒有發送請求,而是直接使用了緩存數據。
瀏覽器的判斷過程:首先判斷是否有緩存,有緩存,是否須要校驗資源更新,不須要(響應頭沒有Cache-Control:no-cache字段),而後判斷緩存過時了嗎,沒過時(響應頭Cache-Control:max-age=315360000),因而瀏覽器直接使用緩存,不進行請求。

瀏覽器地址欄回車,使用緩存沒有請求

3.刷新頁面(F5/點擊工具欄中的刷新按鈕/右鍵菜單從新加載)

從結果來看,瀏覽器仍然使用的是緩存。可是此次有發送資源更新校驗的請求,服務端返回304,表示資源沒有變更,瀏覽器使用緩存。
咱們能夠注意到,刷新頁面,火狐瀏覽器(其它瀏覽器行爲可能不同)向請求頭裏強行添加了幾個字段。

Cache-Control:max-age=0
If-Modified-Since:Mon, 07 Nov 2016 07:51:11 GMT
If-None-Match: "352b-540b1498e39c0"
複製代碼

Cache-Control:max-age=0,表示無論上次的響應頭設置的是什麼,此次請求都會進行資源更新校驗。
If-Modified-Since,回傳資源最後修改時間給服務器校驗
If-None-Match,回傳ETag給服務器校驗
瀏覽器的判斷過程:緩存是否存在,存在,是否須要校驗資源更新,須要(Cache-Control:max-age=0),發起資源校驗請求,因爲資源沒有修改,服務器返回304,瀏覽器使用緩存數據。


刷新頁面,瀏覽器向請求頭中添加了一些字段

4.谷歌瀏覽器強制刷新cmd+shift+R(由於火狐沒這功能,因此這裏換成谷歌瀏覽器測試)

結果:瀏覽器進行了請求,服務器返回200和數據。
咱們注意到,谷歌瀏覽器在請求頭中強行添加了兩個字段。

pragma: no-cache
cache-control: no-cache
複製代碼

cache-control: no-cache,在請求頭中表示,(包括中間服務器)不要使用緩存,去源服務器請求資源。
注意:cache-control: no-cache做爲請求頭和響應頭意義是不同的。做爲請求頭表示不緩存,做爲響應頭表示每次都得去校驗資源更新。
pragma: no-cache,和cache-control: no-cache是一個意思,只是爲了兼容HTTP1.0。
瀏覽器的判斷過程:有不使用緩存的標記(cache-control: no-cache),直接發起請求。

谷歌瀏覽器強制刷新,請求頭添加字段

強制刷新,發起請求服務器返回200和數據

4、WKWebView默認緩存策略總結

(一)回答文章開頭的幾個問題

1. 什麼狀況下會緩存數據?

第一次啓動的時候,若是響應頭中不包含任何緩存控制字段(Expires、Cache-Control:max-age、Last-Modified等),那麼不會緩存(仍然可能會有物理緩存,只是不使用),下次直接發起請求。若是響應頭包含了緩存控制字段,大多數狀況下此次數據會被緩存,下次啓動的時候執行緩存邏輯判斷。

2. 什麼狀況下每次都須要校驗資源更新?

a. 若是響應頭中包含Cache-Control:no-cache 或 Pragma:no-cache。
b. 若是請求頭中包含了Cache-Control:max-age=0,這個結論是對的,可是WKWebView的默認策略不會出現這種狀況。
c. 響應頭中緩存控制字段只有ETag字段,沒有過時時間和修改時間。

3. 緩存過時時間是多久?

a. 響應頭中Cache-Control:max-age=3600,表示緩存1小時(3600/60/60),單位秒。
b. 響應頭中Expires的值表示過時時刻(服務器時間)。
c. 響應頭中,若是沒有上述兩個字段,但有Last-Modified字段,則觸發啓發式緩存,緩存時間=(date_value - last_modified_value) * 0.1。
優先級 Cache-Control:max-age > Expires > Last-Modified。

4. 校驗資源更新的過程是怎麼樣的?revalidated的指標是什麼?

revalidated的指標有兩個:Last-Modified最後修改時刻、ETag資源惟一標識。
服務器返回數據時會在響應頭中返回上面兩個指標(有可能只有1個,也能夠2個都有),客戶端再次發起請求時會把這兩個指標回傳給服務器。
If-Modified-Since: Last-Modified的值
If-None-Match: ETag的值
服務器進行比對,若是客戶端的資源是最新的,則返回304,客戶端使用緩存數據;若是服務器資源更新了,則返回200和新數據。

(二)WKWebView默認緩存策略流程總結

對照文章開頭的流程圖,WKWebView默認緩存策略流程總結以下:

  1. 是否有緩存,沒有則直接發起請求。有則進行下一步。
  2. 是否每次都得進行資源更新校驗(響應頭是否有Cache-Control:no-cache或Pragma:no-cache字段),不須要則進入3,須要則進入4。
  3. 緩存是否過時(響應頭,Cache-Control:max-age、Expires、Last-Modified啓發式緩存),沒過時則使用緩存,不發起請求。過時了則進入4。
  4. 客戶端發起資源更新校驗請求(請求頭,If-Modified-Since: Last-Modified值、If-None-Match: ETag值),若是資源沒有更新,服務器返回304,客戶端使用緩存;若是資源有更新,服務器返回200和資源。

5、解決方案:數據更新後仍然有緩存的問題

弄清楚了原理,回到文章開頭的問題,H5資源更新了,可是iOS有緩存沒有同步仍是顯示的原來的數據。那麼怎麼解決呢?
App端是作不了什麼的,這個問題須要後臺處理。

方案一:響應頭,添加Cache-Control:no-cache

通過個人調試發現,服務器返回資源的響應頭是
Cache-Contol: max-age=36000000
問題的緣由在於服務器響應頭的緩存字段配置不合理,沒有配置資源更新校驗字段,而緩存過時時間又過長,所以,即便服務器資源更新了客戶端也不會請求新的資源,而是直接使用「沒有過時」的資源。

咱們作出以下修改,在資源的響應頭中添加no-cache字段,這樣每次瀏覽器都會先去校驗資源更新,就解決了這個問題。
Cache-Control:no-cache

方案二:資源連接加後綴(md五、版本號等)

<script src="test.js?ver=113"></script>
https://hm.baidu.com/hm.js?e23800c454aa573c0ccb16b52665ac26
http://tb1.bdstatic.com/tb/_/tbean_safe_ajax_94e7ca2.js
http://img1.gtimg.com/ninja/2/2016/04/ninja145972803357449.jpg
複製代碼

能夠在資源文件後面加上版本號,每次更新資源的時候變動版本號;還能夠在URL後面加上了md5參數,甚至還能夠將md5值做爲文件名的一部分。
採用上述方法,你能夠把緩存時間設置的特別長,那麼在文件沒有變更的時候,瀏覽器直接使用緩存文件;而在文件有變化的時候,因爲文件版本號的變動,或md5變化致使文件名變化,請求的url變了,瀏覽器會當作新的資源去處理,必定會發起請求,因此不存在更新後仍然有緩存的狀況。經過這樣的處理,增加了靜態資源,特別是圖片資源的緩存時間,避免該資源很快過時,客戶端頻繁向服務端發起資源請求,服務器再返回304響應的狀況(有Last-Modified/Etag)。

6、補充:iOS原生請求默認策略的一些問題

1. iOS原生請求默認策略也遵循上面的規則嗎?

——是的。
NSURLRequest的默認緩存策略是NSURLRequestUseProtocolCachePolicy,徹底遵循上文講得HTTP緩存協議。看下面的例子。

- (void)requestData
{
    NSLog(@"開始請求");
    NSString *url = @"http://www.4399.com/jss/lx6.js";
    NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:url]];
    NSURLSessionDataTask *task = [[NSURLSession sharedSession] dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
        if (!error) {
            NSLog(@"%@",response);
        }
    }];
    [task resume];
}
複製代碼


第一次請求時經過抓包工具看到,響應頭設置了比較長的緩存時間。按照上文的講述的,在緩存沒有過時的狀況下,下次請求會直接返回緩存數據,不在請求。
通過測試,再次請求時抓包工具顯示確實沒有請求發出。同時completionHandler回調,code返回200,data返回數據。甚至,你能夠把網斷了,仍然會有上述回調,code200,data返回數據。印着了上述結論。

2. iOS客戶端須要本身處理 "304 Not Modified" 響應嗎?

不須要。
仍是上面的例子,咱們先把模擬器上的App刪了(清除緩存),從新run。此次我經過抓包工具對這個請求打斷點,在第一次請求返回時在響應頭添加no-cache字段,來測試下收到304響應時客戶端completionHandler回調的狀況。加入no-cache字段後,第二次請求效果以下:

第二次請求-請求頭

第二次請求-響應頭

你們能夠看到,
請求頭,自動(注意,這是系統本身實現的,並不須要客戶端手動添加,這也進一步證實iOS原生請求也是遵循Http緩存協議的)帶上了if-None-Match和if-Modified-Since這兩個字段。那是由於第一次響應頭中咱們添加了no-cache字段,表示下次請求須要校驗資源更新。
響應頭,服務器返回了304 Not Modified。
下面來看看completionHandler回調狀況:

第二次請求-Xcode日誌

從日誌中咱們能夠看出completionHandler回調返回的code仍然是200。

蘋果系統內部對304 Not Modified響應作了特殊處理

  • code字段,固定返回200
  • data字段,由於服務端返回的304報文是不帶data數據字段的,可是蘋果又得把data經過completionHandler回調給客戶端,蘋果會去緩存中取data數據,返回的data字段和第一次響應的data是同一個。
  • response字段,返回的是第二次請求304的響應頭,而不是第一次請求緩存的響應頭。能夠經過下圖佐證,第一次和第二次回調的響應頭不一致。

200和304回調的響應頭不一致

綜上,蘋果內部幫咱們處理了"304 Not Modified"響應。對客戶端來講,你只須要知道返回200就是沒有異常,拿着data用就好了。至於,數據來自緩存仍是來自服務器,緩存有沒有過時,需不須要校驗資源更新等,都交給蘋果吧。

3. code都返回200,那我怎麼知道返回的是緩存數據仍是服務器數據呢**

蘋果並無提供相關的API,不過咱們能夠間接的去判斷。
請求前先去取緩存NSCachedURLResponse,NSCachedURLResponse對象有個response屬性,在completionHandler回調時去比對緩存的response和返回的response是否相同。系統也沒有提供比對NSURLResponse的方法,這裏咱們比對NSHTTPURLResponse的allHeaderFields屬性。

- (void)requestData
{
    NSLog(@"開始請求");
    NSString *url = @"http://www.4399.com/jss/lx6.js";
    NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:url]];
    NSCachedURLResponse *cachedURLResponse = [[NSURLCache sharedURLCache] cachedResponseForRequest:request];
    NSURLResponse *cacheResponse = cachedURLResponse.response;
    NSURLSessionDataTask *task = [[NSURLSession sharedSession] dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
        if ([[cacheResponse valueForKey:@"allHeaderFields"] isEqual:[response valueForKey:@"allHeaderFields"]]) {
            //響應頭相同,是緩存數據
            NSLog(@"allHeaderFields 相同");
        }
    }];
    [task resume];
}
複製代碼

實際上,後臺把緩存字段配置好後,客戶端不須要關心返回的數據是否來自緩存,好像沒有這樣的應用場景。

若是以爲這篇文章對你有幫助,請點個贊吧。若是有疑問能夠關注個人公衆號給我留言。
轉載請註明出處,謝謝!

參考連接:
WKWebView的緩存問題
iOS webview加載時序和緩存問題總結
WKWebView緩存問題 - 圖片資源
對NSURLRequestUseProtocolCachePolicy的理解
 蘋果官網文檔:NSURLRequestUseProtocolCachePolicy
HTTP緩存控制小結
HTTP/1.1官方協議RFC2616
多是最被誤用的 HTTP 響應頭之一 Cache-Control: must-revalidate

相關文章
相關標籤/搜索