HTTP緩存機制在iOS中的應用和體現

1、 什麼是緩存

Web 緩存是能夠保存文檔副本的HTTP設備。算法

HTTP緩存通常爲兩種,本地緩存和代理緩存。本地緩存就是客戶端設備中的緩存,代理緩存就是緩存代理服務器,常見的就 是 CDN。緩存

2、緩存機制

1. 緩存機制

緩存的機制是針對客戶端-緩存設備-源站的交互而言的,緩存的處理機制以下: bash

緩存機制

如上圖所示,通常而言,緩存是否新鮮採用 Cache-Control/Expires 進行判斷,也叫作強制緩存。服務器的再驗證通常採用 If-None-Match + ETag 或者 If-Modified-Since + Last-Modified 的「條件get」請求來判斷,也叫作對比緩存。服務器

2. 本地緩存的特殊之處

當存在多個緩存設備時,好比客戶端設備中有緩存,CDN 中也有緩存,此時就有兩個緩存了。只要客戶端本地有緩存,那麼客戶端就是一個一般意義上的 Web緩存設備,只不過客戶端-服務器的距離幾乎爲 0 而已。網絡

緩存的概念至關重要,緩存是一個設備。全部緩存相關的邏輯都是按照三個關鍵點來進行的,這三個關鍵點就是:客戶端、緩存、源服務器,當同時存在客戶端緩存和代理緩存時,其狀況多是:測試

多個緩存設備

  • 本地緩存的特殊之處就在於在判斷緩存未失效時能夠直接使用緩存而不用發送 Request。

所以,請求一個 HTTP 時,先查詢客戶端本地的緩存,檢查是否有緩存,若是有緩存再根據 Cache-Control: max-age=xxx 來判斷緩存是否新鮮,若是足夠新鮮,也就是第二次請求在 Cache-Control 時間內,此時能夠直接使用本地保存的 Reponse + data,徹底不須要進行請求。優化

若是不夠新鮮了,就不能使用本地緩存了,而是應該發起正式的請求。請求到了緩存服務器,緩存服務器會發送帶條件的再驗證請求到源站,也就是使用 If-Modified-Since 等方法來進行再驗證。若是驗證經過,緩存未過時,更新 Cache-Control/Expires 的值,從新計算時間,整合 Reponse 以後返回給客戶端,返回的實體中不包含 data,此時狀態碼爲 304。若是緩存失效,那麼返回的狀態碼爲200,實體中包含所有 data。ui

其時序圖以下: url

客戶端-本地緩存-代理緩存-源站

3、緩存過時

意義:當存在緩存時,使用過時驗證的機制來驗證緩存是否可使用,這一機制也有不少人稱之爲強制緩存spa

這一步通常是在本地緩存或者代理緩存中進行,經過 Cache-Control 或者是 Expires 進行驗證。

1. Expires

老式的 HTTP1.0協議使用 Expires字段來表示文檔的過時日期,好比:

Expires:Thu,15 Apr  2010  20:00:00  GMT
複製代碼

**意義:**這個字段可使用一個組件的當前副本,直到指定的時間爲止。

缺陷:

  1. 客戶端和服務端的時鐘必須嚴格一致;
  2. 時間到期以後服務器須要從新設置;

因此就有了第二種方式:

2. Cache-Control:max-age

Cache-Control:max-age 是對 Expires的優化處理,好比:

Cathe-Control:max-age=315360000
複製代碼

**意義:**從請求開始在max-age時間均可以使用緩存,以外的使用請求。

如此,就能夠消除 Expires 時間統一的限制。

**總結:**如今強制緩存通常都採用 Cache-Control: max-age=xxx 來設置。

備註:Cache-Control 還有不少其餘的可選值,後文會介紹。

4、服務器再驗證

意義: 即便緩存過時了,也不意味着緩存文件和原始服務器上的文件不一致,這只是意味着要進行時間覈對來確認緩存是否仍然可使用。這個狀況叫作服務器再驗證。

**驗證機制:**HTTP 容許客戶端向服務器發送一個「條件GET」,根據條件判斷,只有當服務器中的文檔和緩存不同時,服務器纔會在 Response 主體中包含所有的內容,不然返回 304,Response 中不包含資源。

條件語句有不少種,經常使用的有兩種:

1. If-Modified-Since 和 Last-Modified

If-Modified-Since 客戶端使用,在請求頭中添加。Last-Modified 服務端使用,在響應頭中返回。兩個配合使用來驗證資源是否真的發生了改變。若是改變了,狀態碼爲200,響應主體中包含全部內容,若是爲改變,狀態碼爲304,響應實體中不包含主體,只包含頭部。

舉個栗子🌰:

第一次請求的 Response 以下:

Response

從圖中能夠看出,Response 中包含 Cache-Control: max-age=10,表示在10秒內能夠直接使用緩存,超過10秒就須要進行再驗證。同時 Response 中帶有一個 Last-Modified:

第二次進行請求的請求頭和響應頭:

再驗證經過

上圖表示10秒後進行了緩存再驗證且驗證經過,因此返回的是304。

2. If-None-Match 和 ETag

**意義:**有些資源會週期性重寫,可是內容卻未發生變化,此時 If-Modified-Since 就不能知足要求,而是須要一個實體標籤。

客戶端記錄服務端在響應頭中的 ETag 並在請求通中使用 If-None-Match 字段提交給服務器。同理,若是 ETag 一致,表示緩存的資源在源站中未發生改變,因此此時會返回 304,表示緩存可用。不然就會返回 200,在返回的主體中包含完整的內容。同時,Response 中會返回最新的 ETag。

舉個栗子,緩存再驗證經過:

ETag緩存再驗證經過

4. 強弱驗證

**意義:**有些文檔被修改了,可是修改的內容不重要,好比註釋,此時須要一個強弱標籤來告訴使用者,什麼狀況下緩存還能夠繼續使用。

仍然相似於 Git 上的代碼管理。每次提交代碼,都會在對應的分支上生成一個索引值,可是並非每次提交都會修改版本號的,更不是每次更新都會生成一個大的版本號。

緩存也存在這種狀況,由於資源的某些無傷大雅的修改並不影響原先副本的繼續使用,好比註釋。因此存在強弱驗證的狀況:

**弱驗證:**內容的主要含義發生變化時,弱驗證器纔會發生變化。 **強驗證:**只要內容發生變化,強驗證器就會發生變化。

例如:

Etag:w/"2.6"
If-None-Match:w/"2.6"
複製代碼

不帶 w/ 就是強驗證,例如:

Etag:"2.6"
If-None-Match:"2.6"
複製代碼

5. If-None-Match的多值狀況

If-None-Match 能夠有多個值,表示這些版本的副本在緩存中都存在,如圖:

If-None-Match

6. 前後問題

由於緩存控制的緣由,對緩存的使用會分不少狀況。好比 Cache-Control 爲 no-cache 時,表示必須進行再驗證經過後,才能使用緩存。而 Cache-Control 爲 max-age=xxx 時表示在收到 Response 以後的這個時間內均可以使用緩存。

另外,除了 Cache-Control 對緩存的控制,還會有試探性過時的機制,所以緩存的使用與否的邏輯並非簡單的 Yes or No,而是須要根據多重條件進行綜合判斷,後文會有存在 Cache-Control 和不存在 Cache-Control 狀況下的常見邏輯。

所以,If-Modified-Since/If-None-Match 和 Last-Modified/ETag 二者的前後關係不肯定。其一種常見的做用機制是服務端返回 Last-Modified 字段,客戶端進行緩存,若是須要進行緩存的再驗證(好比max-age過時了),那麼就將存儲的值做爲 If-Modified-Since 的值添加在請求頭中發送給服務器。

7. 特別注意

  1. 若是服務器返回一個實體標籤(ETag),HTTP/1.1,客戶端就必須使用實體標籤;
  2. 若是服務器只回送了一個 Last-Modified 值,客戶端就可使用 If-Modified-Since 驗證,非必須;
  3. 若是二者都提供了,那麼就須要使用兩種驗證方案,這樣就能夠兼容 HTTP/1.0 和 HTTP/1.1,但不是必須;
  4. 若是客戶端的頭部中既包含實體標籤又包含最後修改日期,那麼服務端只有在兩個條件都驗證經過時才能返回 304;

如圖:

再驗證

再驗證

正由於如此,纔有了試探性過時的緩存策略。

5、緩存控制

1. Cache-Control之於代理緩存

  • no-store

緩存中不得存儲任何關於客戶端請求和服務端響應的內容。每次由客戶端發起的請求都會下載完整的響應內容。

  • no-cache

代理緩存能夠存儲緩存,可是必須在和源站進行驗證以後才能提供給客戶端。

  • private

只能用於私有緩存(客戶端緩存),中間人不能緩存,默認爲private;

  • public

公共緩存,能夠用於中間人(代理緩存、CDN);

  • must-revalidate

若是過時,那麼必須驗證後才能使用或者提供給客戶端,比 no-cache 稍微寬鬆,no-cache 不論是否過時都要驗證;

  • max-age

過時時間,從服務器將資源傳來之時,資源處於新鮮狀態的秒數;

  • pragma

是HTTP/1.0標準中定義的一個 header 屬性,請求中包含Pragma 的效果跟在頭信息中定義Cache-Control: no-cache相同,可是HTTP的響應頭沒有明肯定義這個屬性,因此它不能拿來徹底替代HTTP/1.1中定義的Cache-control頭。一般定義Pragma以向後兼容基於HTTP/1.0的客戶端。

2. Cache-Control之於客戶端

客戶端也能夠在請求中添加 Cache-Control 請求首部,完整的意義以下:

Cache-Control請求指令

其中最經常使用的有兩種:

  • Cache-Control: no-cache + Pragma: no-cache

表示代理緩存必須對緩存進行驗證,驗證經過後才能提供緩存。Pragma 是爲了支持 HTTP1.0;

  • Cache-Control: no-store

表示代理緩存不能提供緩存,且代理緩存中的的緩存資源應該刪除;

6、試探性過時

1. 定義

若是響應中沒有 Cache-Control 也沒有 Expires ,那麼緩存就能夠計算出一個試探性最大使用期。

最大使用期的計算可使用任意算法,可是若是獲得的最大試用期大於24小時,就應該想響應首部添加一個試探性過時,可是這種方式的使用不多,經常使用的是 LM-factor 算法。

2. LM-factor 算法

使用條件:

    1. 響應中沒有 Cache-Control 也沒有 Expires;
    1. 響應中存在 Last-Modified;

計算方法:

試探性最大使用期 = rate * (請求時間 - 最後修改時間)

舉個栗子🌰:

LM-factor 算法

3. 特別提示

《HTTP權威指南》中特別指出:

試探性過時特別注意

而實際上,safari 就對試探性過時進行了實現。

7、iOS中的系統實現的緩存策略

iOS 中的 NSURLSession 中使用到了 NSURLRequestCachePolicy ,這個枚舉就是 Apple 遵循 HTTP 協議,將 iPhone 做爲一個本地緩存設備,實現了和協議對應的緩存邏輯,可是其邏輯使用到了試探性過時,其判斷邏輯大體以下:

iOS中的NSURLRequestCachePolicy

驗證:

iOS 中,會存在這樣一種狀況: 若是 Response 存在 ETag,Request頭部中會自動添加 If-None-Matched,可是若是同時存在 Last-Modified,卻不會主動添加If-Modified-Since,而是直接存儲後使用,在第二次使用時不請求網絡直接使用緩存,此時試探性過時機制生效,緩存未過時。

原始的響應頭(省略了一些內容):

HTTP/1.1 200 OK
Date	Wed, 19 Feb 2020 08:09:14 GMT
Content-Type	image/jpeg
Server	openresty/1.11.2.5
Content-MD5	c6090671ef82012e7e71b6dc938dc706
ETag	51cf999237cf860b7fd92e6986fc4767
Last-Modified	Wed, 12 Feb 2020 12:01:36 Asia/Shanghai
複製代碼

再次請求就抓不到包了,卻獲得了 200 的響應,且包含 data,此時就是試探性過時機制生效,直接使用的本地的 Reponse 和 data,並無進行請求。

其實,可使用 charles 斷點,對這個 Response 進行幾種操做:

  1. 去掉 Last-Modified
  2. 修改 Last-Modified 爲和請求時間相近
  3. 添加 Cache-Control

1. 去掉Last-Modified

意義: Response 中只有 ETag,因此客戶端下一次請求時不觸發試探性過時,也不會觸發強制緩存,而是直接請求進行緩存的再驗證;

操做以下:

去掉Last-Modified ,而後,經過 charles 斷點 修改響應頭,去掉了 Last-Modified 字段,最終的響應頭以下:

HTTP/1.1 200 OK
Date	Wed, 19 Feb 2020 08:09:14 GMT
Content-Type	image/jpeg
Server	openresty/1.11.2.5
Content-MD5	c6090671ef82012e7e71b6dc938dc706
ETag	51cf999237cf860b7fd92e6986fc4767
複製代碼

再次進行請求, charles 可以抓到包,證實沒有觸發強制緩存,也沒有觸發試探性過時,請求頭和響應頭以下:

緩存再驗證成功

由圖可知:客戶端發送了一個條件GET進行緩存驗證,且驗證成功。

2. 修改 Last-Modified 爲和請求時間相近

由於不存在 Cache-Control 可是卻存在 Last-Modified,因此 iOS 會觸發試探性過時。

未修改以前這個值相對較老,試探性過時觸發以後會判斷短期內緩存不會過時,因此不會發送請求,直接使用緩存。

可是若是修改 Last-Modified 爲和請求時間相近,那麼試探性過時的計算結果爲緩存很快就會過時,資源變更比較頻繁,因此此時 iOS 會發送一個一個條件請求進行緩存再驗證。

修改後的 Reponse 以下:

HTTP/1.1 200 OK
Date: Wed, 19 Feb 2020 09:06:27 GMT
Content-Type: image/jpeg
Content-Length: 3444
Connection: keep-alive
Server: openresty/1.11.2.5
Content-MD5: c6090671ef82012e7e71b6dc938dc706
ETag: 51cf999237cf860b7fd92e6986fc4767
Last-Modified: Wed, 19 Feb 2020 09:06:27 GMT
複製代碼

再次進行請求的請求頭和響應頭以下:

試探性過時判斷結果爲緩存過時

3. 添加 Cache-Control

**意義:**添加 Cache-Control 後,就按照正常邏輯走了,也就是說不會觸發試探性緩存了。

由於 Cache-Control 相對複雜,這裏直接使用 max-age=5 和 max-age=36500 和 來做爲示例;

修改後的響應頭爲:

HTTP/1.1 200 OK
Date: Wed, 19 Feb 2020 09:13:05 GMT
Content-Type: image/jpeg
Content-Length: 3444
Connection: keep-alive
Server: openresty/1.11.2.5
Content-MD5: c6090671ef82012e7e71b6dc938dc706
Cache-Control: max-age=5
ETag: 51cf999237cf860b7fd92e6986fc4767
Last-Modified: Wed, 12 Feb 2020 12:01:36 Asia/Shanghai
複製代碼

過5秒以後再次請求確定不會觸發強制緩存,而是在強制緩存失效以後,進行正常的進行緩存再驗證:

強制緩存判斷緩存失效後再驗證成功

同理,若是 max-age=36500,短時間內強制緩存生效,確定是直接使用本地緩存而不進行請求。

附上測試代碼:

// 圖片
NSURL *url = [NSURL URLWithString:@"http://cms-bucket.ws.126.net/2020/0212/51cf9992j00q5kluo00bmc000tj00tjc.jpg?imageView&thumbnail=140y88"];

NSMutableURLRequest *request = [NSMutableURLRequest new];
request.HTTPMethod = @"GET";
request.URL = url;

// 查詢是否有緩存
NSCachedURLResponse *cacheReponse = [[NSURLCache sharedURLCache] cachedResponseForRequest:request];
if (cacheReponse) {
    NSLog(@"本地存在緩存");
} else {
    NSLog(@"本地無緩存");
}

NSURLSessionDataTask *task = [[NSURLSession sharedSession] dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
    NSLog(@"%@",request.allHTTPHeaderFields);
    NSHTTPURLResponse *httpReponse = (NSHTTPURLResponse *)response;
    NSLog(@"statusCode:%li",httpReponse.statusCode);

     if (data.length > 0) {
        NSLog(@"響應有數據");
    } else {
        NSLog(@"響應無數據");
    }
}];
[task resume];
複製代碼

總結

幾個知識點再總結下:

總結

結尾

正常來說,iOS 中使用默認的緩存機制,而後服務端按照 HTTP/1.1 協議正確配置好緩存過時字段(Expires/Cache-Control)和條件驗證字段(If-Modified-Since/If-None-Match),基本上就能知足大部分的需求,並且將緩存更新與否的決定權交給了服務端,也就是 H5 頁面能夠控制 App 中的頁面是否更新,並不用發包。

更多文章
相關文章
相關標籤/搜索