今天同事反應H5更新了資源,但iOS App裏面仍然使用的是舊的緩存資源。爲何會這樣呢?要弄清楚這個問題,首先得弄清楚WKWebView的緩存原理。css
下圖是蘋果官方文檔提供的默認緩存策略(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
官方文檔說,算法
上面官方文檔只是說了個大概的原理,具體指標和細節並無說清楚。瀏覽器
實際上,WKWebView默認緩存策略徹底遵循HTTP緩存協議,蘋果並無作額外的事情,上面的流程圖和文檔描述只是簡略描述了HTTP緩存協議的一個流程。也就是說,你想弄清楚WKWebView默認緩存策略,你得弄清楚HTTP緩存協議。緩存
http緩存協議這個詞是我本身造的哈,本節要講的實際上就是HTTP協議中和緩存有關的請求頭、響應頭的做用和用法。bash
客戶端默認緩存行爲其實是由服務器控制的,客戶端和服務器經過HTTP請求頭和響應頭中的緩存字段來交流,進而影響客戶端的行爲。
下面就來介紹一下相關字段。服務器
在 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所定義的緩存時間是相對服務器上的時間而言的,若是客戶端上的時間跟服務器上的時間不一致(特別是用戶修改了本身電腦的系統時間),那緩存時間可能就沒啥意義了。
http1.1新增了 Cache-Control 來配置緩存信息,主要包括:可否緩存、緩存過時時間、是否每次校驗等。
Cache-Control是通用頭。
下圖是Cache-Control可選值表。你也能夠查閱HTTP官方文檔14.9Cache-Control部分。
Cache-Control 容許自由組合可選值,用逗號分隔。
Cache-Control: max-age=3600, no-cache
上面這句意思是,緩存過時時間是1小時,每次都必須向服務器進行資源更新校驗。
下面介紹幾個經常使用的可選值。
文章開頭咱們提到了蘋果的流程圖可能會讓人產生歧義,這裏來解釋一下坑在哪裏。
蘋果文檔和流程圖中有個判斷,緩存存在,則須要判斷是否須要每次都校驗,用的是「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-store的響應不會被緩存到任意的磁盤或者內存裏,no-store它纔是真正的「no-cache」。
做爲請求頭,max-age=0表示無論response怎麼設置,在從新獲取資源以前,先進行資源更新校驗。
做爲響應頭,max-age=x表示,緩存有效期是x秒。
不少時候,緩存過時了可是資源並無修改,會發送多餘的請求和數據;或者資源修改了緩存還沒過時,客戶端仍然在用緩存。Cache-Control沒法及時和客戶端同步。
爲了彌補Cache-Control沒法及時判斷資源是否有更新的不足,有了Last-Modified、if-Modified-Since字段。
響應頭,此次命名沒有問題了,這個字段的值就是資源在服務器上最後修改時刻。例如
If-Modified-Since: Thu, 31 Mar 2016 07:07:52 GMT
請求頭,客戶端經過該字段把Last-Modified的值回傳給服務端;客戶端帶上這個字段表示此次請求是向服務端作校驗資源更新校驗。若是資源沒有修改,則服務端返回304不返回數據,客戶端用緩存;資源有修改則返回200和數據。 例如
If-Modified-Since: Thu, 31 Mar 2016 07:07:52 GMT
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的沒法判斷內容實質性變化的缺陷,因而有了ETag和If-None-Match字段,這對字段的用法和Last-Modified、If-Modified-Since類似,服務器在響應頭中返回ETag字段,客戶端在下次請求時在If-None-Match中回傳ETag對應的值。
響應頭,給資源計算得出一個惟一標誌符(好比md5標誌),加在響應頭裏一塊兒返給客戶端,例如
Etag: "5d8c72a5edda8d6a"
請求頭,客戶端在下次請求時回傳ETag值給服務器。
If-None-Match: "5d8c72a5edda8d6a""
上面這些緩存控制字段若是同時出現,他們的優先級如何呢?
優先級: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仍是移動端)的執行過程就是這個流程圖。
第一次請求沒有緩存,瀏覽器發出請求。
咱們能夠看到,返回的響應頭中包含了Cache-Control、ETag、Expires、Last-Modified等多個緩存控制字段。瀏覽器進行緩存。
以下圖能夠看到,瀏覽器沒有發送請求,而是直接使用了緩存數據。
瀏覽器的判斷過程:首先判斷是否有緩存,有緩存,是否須要校驗資源更新,不須要(響應頭沒有Cache-Control:no-cache字段),而後判斷緩存過時了嗎,沒過時(響應頭Cache-Control:max-age=315360000),因而瀏覽器直接使用緩存,不進行請求。
從結果來看,瀏覽器仍然使用的是緩存。可是此次有發送資源更新校驗的請求,服務端返回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,瀏覽器使用緩存數據。
結果:瀏覽器進行了請求,服務器返回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),直接發起請求。
第一次啓動的時候,若是響應頭中不包含任何緩存控制字段(Expires、Cache-Control:max-age、Last-Modified等),那麼不會緩存(仍然可能會有物理緩存,只是不使用),下次直接發起請求。若是響應頭包含了緩存控制字段,大多數狀況下此次數據會被緩存,下次啓動的時候執行緩存邏輯判斷。
a. 若是響應頭中包含Cache-Control:no-cache 或 Pragma:no-cache。
b. 若是請求頭中包含了Cache-Control:max-age=0,這個結論是對的,可是WKWebView的默認策略不會出現這種狀況。
c. 響應頭中緩存控制字段只有ETag字段,沒有過時時間和修改時間。
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。
revalidated的指標有兩個:Last-Modified最後修改時刻、ETag資源惟一標識。
服務器返回數據時會在響應頭中返回上面兩個指標(有可能只有1個,也能夠2個都有),客戶端再次發起請求時會把這兩個指標回傳給服務器。
If-Modified-Since: Last-Modified的值
If-None-Match: ETag的值
服務器進行比對,若是客戶端的資源是最新的,則返回304,客戶端使用緩存數據;若是服務器資源更新了,則返回200和新數據。
對照文章開頭的流程圖,WKWebView默認緩存策略流程總結以下:
弄清楚了原理,回到文章開頭的問題,H5資源更新了,可是iOS有緩存沒有同步仍是顯示的原來的數據。那麼怎麼解決呢?
App端是作不了什麼的,這個問題須要後臺處理。
通過個人調試發現,服務器返回資源的響應頭是
Cache-Contol: max-age=36000000
問題的緣由在於服務器響應頭的緩存字段配置不合理,沒有配置資源更新校驗字段,而緩存過時時間又過長,所以,即便服務器資源更新了客戶端也不會請求新的資源,而是直接使用「沒有過時」的資源。
咱們作出以下修改,在資源的響應頭中添加no-cache字段,這樣每次瀏覽器都會先去校驗資源更新,就解決了這個問題。
Cache-Control:no-cache
<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)。
——是的。
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];
}
複製代碼
不須要。
仍是上面的例子,咱們先把模擬器上的App刪了(清除緩存),從新run。此次我經過抓包工具對這個請求打斷點,在第一次請求返回時在響應頭添加no-cache字段,來測試下收到304響應時客戶端completionHandler回調的狀況。加入no-cache字段後,第二次請求效果以下:
你們能夠看到,
請求頭,自動(注意,這是系統本身實現的,並不須要客戶端手動添加,這也進一步證實iOS原生請求也是遵循Http緩存協議的)帶上了if-None-Match和if-Modified-Since這兩個字段。那是由於第一次響應頭中咱們添加了no-cache字段,表示下次請求須要校驗資源更新。
響應頭,服務器返回了304 Not Modified。
下面來看看completionHandler回調狀況:
蘋果系統內部對304 Not Modified響應作了特殊處理
綜上,蘋果內部幫咱們處理了"304 Not Modified"響應。對客戶端來講,你只須要知道返回200就是沒有異常,拿着data用就好了。至於,數據來自緩存仍是來自服務器,緩存有沒有過時,需不須要校驗資源更新等,都交給蘋果吧。
蘋果並無提供相關的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