1. 程式人生 > Android開發 >WKWebView預設快取策略與HTTP快取協議

WKWebView預設快取策略與HTTP快取協議

今天同事反應H5更新了資源,但iOS App裡面仍然使用的是舊的快取資源。為什麼會這樣呢?要弄清楚這個問題,首先得弄清楚WKWebView的快取原理。

一、WKWebView預設快取策略

下圖是蘋果官方檔案提供的預設快取策略(NSURLRequestUseProtocolCachePolicy)的流程圖。

預設快取策略圖(來源蘋果官方)

官方檔案上是這樣描述的:

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.

官方檔案說,

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

上面官方檔案只是說了個大概的原理,具體指標和細節並沒有說清楚。

  1. 什麼情況下會快取資料?
  2. 什麼情況下每次都需要校驗資源更新?
  3. 快取過期時間是多久?
  4. 校驗資源更新的過程是怎麼樣的?revalidated的指標是什麼?

實際上,WKWebView預設快取策略完全遵循HTTP快取協議,蘋果並沒有做額外的事情,上面的流程圖和檔案描述只是簡略描述了HTTP快取協議的一個流程。也就是說,你想弄清楚WKWebView預設快取策略,你得弄清楚HTTP快取協議

二、HTTP快取協議

http快取協議這個詞是我自己造的哈,本節要講的實際上就是HTTP協議中和快取有關的請求頭、響應頭的作用和用法。

客戶端預設快取行為實際上是由伺服器控制的,客戶端和伺服器通過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調出控制檯,請求頭和響應頭左邊的問號(下圖)是可以點的!點選直接跳轉到對用頭欄位的網頁,真可謂“哪裡不會點哪裡,媽媽再也不用擔心我的學習了!”哈哈哈哈——

火狐瀏覽器問號可以點選

三、實戰:瀏覽器的行為

介紹完上面的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和資料

四、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和資源。

五、解決方案:資料更新後仍然有快取的問題

弄清楚了原理,回到文章開頭的問題,H5資源更新了,但是iOS有快取沒有同步還是顯示的原來的資料。那麼怎麼解決呢?
App端是做不了什麼的,這個問題需要後臺處理。

方案一:響應頭,新增Cache-Control:no-cache

經過我的除錯發現,伺服器返回資源的響應頭是
Cache-Contol: max-age=36000000
問題的原因在於伺服器響應頭的快取欄位配置不合理,沒有配置資源更新校驗欄位,而快取過期時間又過長,因此,即使伺服器資源更新了客戶端也不會請求新的資源,而是直接使用“沒有過期”的資源。

我們做出如下修改,在資源的響應頭中新增no-cache欄位,這樣每次瀏覽器都會先去校驗資源更新,就解決了這個問題。
Cache-Control:no-cache

方案二:資源連結加字尾(md5、版本號等)

<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)。

六、補充: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,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