使用兩行代碼就能完成80%的緩存需求

 

當咱們在談論緩存的時候,咱們在談論什麼?

咱們今天將站在小白用戶的角度,給緩存這個概念進行從新的定義。git

緩存有不一樣的分類方法:github

enter image description here

這裏所指的緩存,是一個寬泛的概念。數據庫

咱們這裏主要按照功能進行劃分:json

enter image description here

- 第一種 第二種
目的 優化型緩存 功能型緩存
具體描述 出於優化考慮:服務器壓力、用戶體驗、爲用戶剩流量等等。同時優化型緩存也有內存緩存和磁盤緩存之分。 App離線也能查看,出於功能考慮,屬於存儲範疇
常見概念 GET網絡請求緩存、WEB緩存 離線存儲
典型應用 微信首頁的會話列表、微信頭像、朋友圈、網易新聞新聞列表、 微信聊天記錄、
Parse對應的類 PFCachedQueryController PFOfflineStore

重度使用緩存的App: 微信、微博、網易新聞、攜程、去哪兒等等。api

GET網絡請求緩存

概述

首先要知道,POST請求不能被緩存,只有 GET 請求能被緩存。由於從數學的角度來說,GET 的結果是 冪等 的,就好像字典裏的 key 與 value 就是冪等的,而 POST 不 冪等 。緩存的思路就是將查詢的參數組成的值做爲 key ,對應結果做爲value。從這個意義上說,一個文件的資源連接,也叫 GET 請求,下文也會這樣看待。數組

80%的緩存需求:兩行代碼就可知足

設置緩存只須要三個步驟:瀏覽器

第一個步驟:請使用 GET 請求。緩存

第二個步驟:安全

若是你已經使用 了 GET 請求,iOS 系統 SDK 已經幫你作好了緩存。你須要的僅僅是設置下內存緩存大小、磁盤緩存大小、以及緩存路徑。甚至這兩行代碼不設置也是能夠的,會有一個默認值。代碼以下:服務器

NSURLCache *urlCache = [[NSURLCache alloc] initWithMemoryCapacity:4 * 1024 * 1024 diskCapacity:20 * 1024 * 1024 diskPath:nil]; [NSURLCache setSharedURLCache:urlCache];

第三個步驟:沒有第三步!

你只要設置了這兩行代碼,基本就可知足80%的緩存需求。AFNetworking 的做者 Mattt曾經說過:

無數開發者嘗試本身作一個簡陋而脆弱的系統來實現網絡緩存的功能,卻不知 NSURLCache 只要兩行代碼就能搞定且好上 100 倍。

(AFN 是否是在暗諷 SDWebImage 複雜又蹩腳的緩存機制??)

要注意

  • iOS 5.0開始,支持磁盤緩存,但僅支持 HTTP
  • iOS 6.0開始,支持 HTTPS 緩存

控制緩存的有效性

咱們知道:

  • 只要是緩存,總會過時。

那麼緩存的過時時間如何控制?

上文中的兩行代碼,已經給出了一個方法,指定超時時間。但這並也許不能知足咱們的需求,若是咱們對數據的一致性,時效性要求很高,即便1秒鐘後數據更改了,客戶端也必須展現更改後的數據。這種狀況如何處理?

下面咱們將對這種需求,進行解決方案的介紹。順序是這樣的:先從文件類型的緩存入手,引入兩個概念。而後再談下,通常數據類型好比 JSON 返回值的緩存處理。

文件緩存:藉助ETag或Last-Modified判斷文件緩存是否有效

Last-Modified

服務器的文件存貯,大多采用資源變更後就從新生成一個連接的作法。並且若是你的文件存儲採用的是第三方的服務,好比七牛、青雲等服務,則必定是如此。

這種作法雖然是推薦作法,但同時也不排除不一樣文件使用同一個連接。那麼若是服務端的file更改了,本地已經有了緩存。如何更新緩存?

這種狀況下須要藉助 ETag 或 Last-Modified 判斷圖片緩存是否有效。

Last-Modified 顧名思義,是資源最後修改的時間戳,每每與緩存時間進行對比來判斷緩存是否過時。

在瀏覽器第一次請求某一個URL時,服務器端的返回狀態會是200,內容是你請求的資源,同時有一個Last-Modified的屬性標記此文件在服務期端最後被修改的時間,格式相似這樣:

        Last-Modified: Fri, 12 May 2006 18:53:33 GMT

客戶端第二次請求此URL時,根據 HTTP 協議的規定,瀏覽器會向服務器傳送 If-Modified-Since 報頭,詢問該時間以後文件是否有被修改過:

        If-Modified-Since: Fri, 12 May 2006 18:53:33 GMT

總結下來它的結構以下:

請求 HeaderValue 響應 HeaderValue
Last-Modified If-Modified-Since

若是服務器端的資源沒有變化,則自動返回 HTTP 304 (Not Changed.)狀態碼,內容爲空,這樣就節省了傳輸數據量。當服務器端代碼發生改變或者重啓服務器時,則從新發出資源,返回和第一次請求時相似。從而保證不向客戶端重複發出資源,也保證當服務器有變化時,客戶端可以獲得最新的資源。

判斷方法用僞代碼表示:

if ETagFromServer != ETagOnClient || LastModifiedFromServer != LastModifiedOnClient
   GetFromServer
else GetFromCache

之因此使用

LastModifiedFromServer != LastModifiedOnClient

而非使用:

LastModifiedFromServer > LastModifiedOnClient

緣由是考慮到可能出現相似下面的狀況:服務端可能對資源文件,廢除其新版,回滾啓用舊版本,此時的狀況是:

LastModifiedFromServer <= LastModifiedOnClient

但咱們依然要更新本地緩存。

參考連接: What takes precedence: the ETag or Last-Modified HTTP header?

Demo10和 Demo11 給出了一個完整的校驗步驟:

並給出了 NSURLConnection 和 NSURLSession 兩個版本:

/*!  @brief 若是本地緩存資源爲最新,則使用使用本地緩存。若是服務器已經更新或本地無緩存則從服務器請求資源。   @details   步驟:  1. 請求是可變的,緩存策略要每次都從服務器加載  2. 每次獲得響應後,須要記錄住 LastModified  3. 下次發送請求的同時,將LastModified一塊兒發送給服務器(由服務器比較內容是否發生變化)   @return 圖片資源  */ - (void)getData:(GetDataCompletion)completion { NSURL *url = [NSURL URLWithString:kLastModifiedImageURL]; NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url cachePolicy:NSURLRequestReloadIgnoringCacheData timeoutInterval:15.0]; // // 發送 etag // if (self.etag.length > 0) { // [request setValue:self.etag forHTTPHeaderField:@"If-None-Match"]; // } // 發送 LastModified if (self.localLastModified.length > 0) { [request setValue:self.localLastModified forHTTPHeaderField:@"If-Modified-Since"]; } [[[NSURLSession sharedSession] dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { // NSLog(@"%@ %tu", response, data.length); // 類型轉換(若是將父類設置給子類,須要強制轉換) NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response; NSLog(@"statusCode == %@", @(httpResponse.statusCode)); // 判斷響應的狀態碼是不是 304 Not Modified (更多狀態碼含義解釋: https://github.com/ChenYilong/iOSDevelopmentTips) if (httpResponse.statusCode == 304) { NSLog(@"加載本地緩存圖片"); // 若是是,使用本地緩存 // 根據請求獲取到`被緩存的響應`! NSCachedURLResponse *cacheResponse = [[NSURLCache sharedURLCache] cachedResponseForRequest:request]; // 拿到緩存的數據 data = cacheResponse.data; } // 獲取而且紀錄 etag,區分大小寫 // self.etag = httpResponse.allHeaderFields[@"Etag"]; // 獲取而且紀錄 LastModified self.localLastModified = httpResponse.allHeaderFields[@"Last-Modified"]; // NSLog(@"%@", self.etag); NSLog(@"%@", self.localLastModified); dispatch_async(dispatch_get_main_queue(), ^{ !completion ?: completion(data); }); }] resume]; }

ETag

ETag 是什麼?

HTTP 協議規格說明定義ETag爲「被請求變量的實體值」 (參見 —— 章節 14.19)。 另外一種說法是,ETag是一個能夠與Web資源關聯的記號(token)。它是一個 hash 值,用做 Request 緩存請求頭,每個資源文件都對應一個惟一的 ETag 值, 服務器單獨負責判斷記號是什麼及其含義,並在HTTP響應頭中將其傳送到客戶端,如下是服務器端返回的格式:

ETag: "50b1c1d4f775c61:df3"


    客戶端的查詢更新格式是這樣的:

    If-None-Match: W/"50b1c1d4f775c61:df3"

其中:

  • If-None-Match - 與響應頭的 Etag 相對應,能夠判斷本地緩存數據是否發生變化

    若是ETag沒改變,則返回狀態304而後不返回,這也和Last-Modified同樣。

總結下來它的結構以下:

請求 HeaderValue 響應 HeaderValue
ETag If-None-Match

ETag 是的功能與 Last-Modified 相似:服務端不會每次都會返回文件資源。客戶端每次向服務端發送上次服務器返回的 ETag 值,服務器會根據客戶端與服務端的 ETag 值是否相等,來決定是否返回 data,同時老是返回對應的 HTTP 狀態碼。客戶端經過 HTTP 狀態碼來決定是否使用緩存。好比:服務端與客戶端的 ETag 值相等,則 HTTP 狀態碼爲 304,不返回 data。服務端文件一旦修改,服務端與客戶端的 ETag 值不等,而且狀態值會變爲200,同時返回 data。

由於修改資源文件後該值會當即變動。這也決定了 ETag 在斷點下載時很是有用。 好比 AFNetworking 在進行斷點下載時,就是藉助它來檢驗數據的。詳見在 AFHTTPRequestOperation 類中的用法:

    //下載暫停時提供斷點續傳功能,修改請求的HTTP頭,記錄當前下載的文件位置,下次能夠從這個位置開始下載。 - (void)pause { unsigned long long offset = 0; if ([self.outputStream propertyForKey:NSStreamFileCurrentOffsetKey]) { offset = [[self.outputStream propertyForKey:NSStreamFileCurrentOffsetKey] unsignedLongLongValue]; } else { offset = [[self.outputStream propertyForKey:NSStreamDataWrittenToMemoryStreamKey] length]; } NSMutableURLRequest *mutableURLRequest = [self.request mutableCopy]; if ([self.response respondsToSelector:@selector(allHeaderFields)] && [[self.response allHeaderFields] valueForKey:@"ETag"]) { //若請求返回的頭部有ETag,則續傳時要帶上這個ETag, //ETag用於放置文件的惟一標識,好比文件MD5值 //續傳時帶上ETag服務端能夠校驗相對上次請求,文件有沒有變化, //如有變化則返回200,迴應新文件的全數據,若無變化則返回206續傳。 [mutableURLRequest setValue:[[self.response allHeaderFields] valueForKey:@"ETag"] forHTTPHeaderField:@"If-Range"]; } //給當前request加Range頭部,下次請求帶上頭部,能夠從offset位置繼續下載 [mutableURLRequest setValue:[NSString stringWithFormat:@"bytes=%llu-", offset] forHTTPHeaderField:@"Range"]; self.request = mutableURLRequest; [super pause]; }

七牛等第三方文件存儲商如今都已經支持ETag,Demo8和9 中給出的演示圖片就是使用的七牛的服務,見:

static NSString *const kETagImageURL = @"http://ac-g3rossf7.clouddn.com/xc8hxXBbXexA8LpZEHbPQVB.jpg";

下面使用一個 Demo 來進行演示用法,

以 NSURLConnection 搭配 ETag 爲例,步驟以下:

  • 請求的緩存策略使用 NSURLRequestReloadIgnoringCacheData,忽略本地緩存
  • 服務器響應結束後,要記錄 Etag,服務器內容和本地緩存對比是否變化的重要依據
  • 在發送請求時,設置 If-None-Match,而且傳入 Etag
  • 鏈接結束後,要判斷響應頭的狀態碼,若是是 304,說明本地緩存內容沒有發生變化

如下代碼詳見 Demo08 :

/*!  @brief 若是本地緩存資源爲最新,則使用使用本地緩存。若是服務器已經更新或本地無緩存則從服務器請求資源。   @details   步驟:  1. 請求是可變的,緩存策略要每次都從服務器加載  2. 每次獲得響應後,須要記錄住 etag  3. 下次發送請求的同時,將etag一塊兒發送給服務器(由服務器比較內容是否發生變化)   @return 圖片資源  */ - (void)getData:(GetDataCompletion)completion { NSURL *url = [NSURL URLWithString:kETagImageURL]; NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url cachePolicy:NSURLRequestReloadIgnoringCacheData timeoutInterval:15.0]; // 發送 etag if (self.etag.length > 0) { [request setValue:self.etag forHTTPHeaderField:@"If-None-Match"]; } [NSURLConnection sendAsynchronousRequest:request queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError) { // NSLog(@"%@ %tu", response, data.length);dd // 類型轉換(若是將父類設置給子類,須要強制轉換) NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response; NSLog(@"statusCode == %@", @(httpResponse.statusCode)); // 判斷響應的狀態碼是不是 304 Not Modified (更多狀態碼含義解釋: https://github.com/ChenYilong/iOSDevelopmentTips) if (httpResponse.statusCode == 304) { NSLog(@"加載本地緩存圖片"); // 若是是,使用本地緩存 // 根據請求獲取到`被緩存的響應`! NSCachedURLResponse *cacheResponse = [[NSURLCache sharedURLCache] cachedResponseForRequest:request]; // 拿到緩存的數據 data = cacheResponse.data; } // 獲取而且紀錄 etag,區分大小寫 self.etag = httpResponse.allHeaderFields[@"Etag"]; NSLog(@"etag值%@", self.etag); !completion ?: completion(data); }]; }

相應的 NSURLSession 搭配 ETag 的版本見 Demo09:

/*!  @brief 若是本地緩存資源爲最新,則使用使用本地緩存。若是服務器已經更新或本地無緩存則從服務器請求資源。   @details   步驟:  1. 請求是可變的,緩存策略要每次都從服務器加載  2. 每次獲得響應後,須要記錄住 etag  3. 下次發送請求的同時,將etag一塊兒發送給服務器(由服務器比較內容是否發生變化)   @return 圖片資源  */ - (void)getData:(GetDataCompletion)completion { NSURL *url = [NSURL URLWithString:kETagImageURL]; NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url cachePolicy:NSURLRequestReloadIgnoringCacheData timeoutInterval:15.0]; // 發送 etag if (self.etag.length > 0) { [request setValue:self.etag forHTTPHeaderField:@"If-None-Match"]; } [[[NSURLSession sharedSession] dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { // NSLog(@"%@ %tu", response, data.length); // 類型轉換(若是將父類設置給子類,須要強制轉換) NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response; NSLog(@"statusCode == %@", @(httpResponse.statusCode)); // 判斷響應的狀態碼是不是 304 Not Modified (更多狀態碼含義解釋: https://github.com/ChenYilong/iOSDevelopmentTips) if (httpResponse.statusCode == 304) { NSLog(@"加載本地緩存圖片"); // 若是是,使用本地緩存 // 根據請求獲取到`被緩存的響應`! NSCachedURLResponse *cacheResponse = [[NSURLCache sharedURLCache] cachedResponseForRequest:request]; // 拿到緩存的數據 data = cacheResponse.data; } // 獲取而且紀錄 etag,區分大小寫 self.etag = httpResponse.allHeaderFields[@"Etag"]; NSLog(@"%@", self.etag); dispatch_async(dispatch_get_main_queue(), ^{ !completion ?: completion(data); }); }] resume]; }

運行效果:

enter image description here

總結

在官方給出的文檔中提出 ETag 是首選的方式,優於 Last-Modified 方式。由於 ETag 是基於 hash ,hash 的規則能夠本身設置,並且是基於一致性,是「強校驗」。 Last-Modified 是基於時間,是弱校驗,弱在哪裏?好比說:若是服務端的資源回滾客戶端的 Last-Modified 反而會比服務端還要新。

雖然 ETag 優於 Last-Modified ,但並不是全部服務端都會支持,而 Last-Modified 則通常都會有該字段。 大多數狀況下須要與服務端進行協調支持 ETag ,若是協商無果就只能退而求其次。

Demo 也給出了一個不支持 ETag 的連接,基本隨便找一張圖片都行:

static NSString *const kLastModifiedImageURL = @"http://image17-c.poco.cn/mypoco/myphoto/20151211/16/17338872420151211164742047.png";

做爲通用型的網絡請求工具 AFNetworking 對該現狀的處理方式是,判斷服務端是否包含 ETag ,而後再進行相應處理。可見AFHTTPRequestOperation 類中的用法,也就是上文中已經給出的斷點下載的代碼。

在回顧下思路:

  • 爲資源分派 hash 值,而後對比服務端與本地緩存是否一致來決定是否須要更新緩存。

這種思路,在開發中常用,好比:處於安全考慮,登錄操做通常不會傳輸帳號密碼,而是傳輸對應的 hash 值-- token ,這裏的 token 就能夠看作一個 file 資源,若是想讓一個用戶登錄超時時間是三天,只須要在服務端每隔三天更改下 token 值,客戶端與服務端值不一致,而後服務端返回 token 過時的提示。

值得注意的一點是:

  • 若是藉助了 Last-Modified 和 ETag,那麼緩存策略則必須使用 NSURLRequestReloadIgnoringCacheData 策略,忽略緩存,每次都要向服務端進行校驗。

若是 GET 中包含有版本號信息

衆多的應用都會在 GET 請求後加上版本號:

 http://abc.com?my_current_version=v1.0.0

這種狀況下, ?v1.0 和 ?v2.0 兩個不一樣版本,請求到的 Last-Modified 和 ETag 會如預期嗎?

這徹底取決於公司服務端同事的實現, Last-Modified 和 ETag 僅僅是一個協議,並無統一的實現方法,而服務端的處理邏輯徹底取決於需求。

你徹底能夠要求服務端同事,僅僅判斷資源的異同,而忽略掉 ?v1.0 和 ?v2.0 兩個版本的區別。

參考連接:if-modified-since vs if-none-match

通常數據類型藉助 Last-Modified 與 ETag 進行緩存

以上的討論是基於文件資源,那麼對通常的網絡請求是否也能應用?

控制緩存過時時間,無非兩種:設置一個過時時間;校驗緩存與服務端一致性,只在不一致時才更新。

通常狀況下是不會對 api 層面作這種校驗,只在有業務需求時纔會考慮作,好比:

  1. 數據更新頻率較低,「萬不得已不會更新」---只在服務器有更新時才更新,以此來保證2G 等惡略網絡環境下,有較好的體驗。好比網易新聞欄目,但相反微博列表、新聞列表就不適合。
  2. 業務數據一致性要求高,數據更新後須要服務端馬上展現給用戶。客戶端顯示的數據必須是服務端最新的數據
  3. 有離線展現需求,必須實現緩存策略,保證弱網狀況下的數據展現的速度。但不考慮使用緩存過時時間來控制緩存的有效性。
  4. 儘可能減小數據傳輸,節省用戶流量

一些建議:

  1. 若是是 file 文件類型,用 Last-Modified 就夠了。即便 ETag 是首選,但此時二者效果一致。九成以上的需求,效果都一致。
  2. 若是是通常的數據類型--基於查詢的 get 請求,好比返回值是 data 或 string 類型的 json 返回值。那麼 Last-Modified 服務端支持起來就會困難一點。由於好比 你作了一個博客瀏覽 app ,查詢最近的10條博客, 基於此時的業務考慮 Last-Modified 指的是10條中任意一個博客的更改。那麼服務端須要在你發出請求後,遍歷下10條數據,獲得「10條中是否至少一個被修改了」。並且要保證每一條博客表數據都有一個相似於記錄 Last-Modified 的字段,這顯然不太現實。

    若是更新頻率較高,好比最近微博列表、最近新聞列表,這些請求就不適合,更多的處理方式是添加一個接口,客戶端將本地緩存的最後一條數據的的時間戳或 id 傳給服務端,而後服務端會將新增的數據條數返回,沒有新增則返回 nil 或 304。

參考連接: 《(慕課網)imooc iPhone3.3 接口數據緩存》

剩下20%的網絡緩存需求

真的有NSURLCache 不能知足的需求?

有人可能要問:

NSURLCache 不是幫咱們作了硬盤緩存麼?那咱們爲何要本身用數據庫作本地緩存啊。爲啥不直接用NSURLCache 不是更方便?

系統幫咱們作的緩存,好處是自動,無需咱們進行復雜的設置。壞處也偏偏是這個:不夠靈活,不能自定義。只能指定一個緩存的總文件夾,不能分別指定每個文件緩存的位置,更不能爲每一個文件建立一個文件夾,也不能指定文件夾的名稱。緩存的對象也是固定的:只能是 GET請求的返回值。

相關文章
相關標籤/搜索