NSURLCache詳解和使用

使用緩存的目的是爲了使應用程序能更快速的響應用戶輸入,是程序高效的運行。有時候咱們須要將遠程web服務器獲取的數據緩存起來,以空間換取時間,減小對同一個url屢次請求,減輕服務器的壓力,優化客戶端網絡,讓用戶體驗更良好。git

背景:NSURLCache : 在iOS5之前,apple不支持磁盤緩存,在iOS5的時候,容許磁盤緩存,(NSURLCache 是根據NSURLRequest 來實現的)只支持http,在iOS6之後,支持http和https。github

緩存的實現說明:因爲GET請求通常用來查詢數據,POST請求通常是發大量數據給服務器處理(變更性比較大),所以通常只對GET請求進行緩存,而不對POST請求進行緩存。web

緩存原理:一個NSURLRequest對應一個NSCachedURLResponse數據庫

緩存技術:把緩存的數據都保存到數據庫中。json

NSURLCache的常見用法:api

(1)得到全局緩存對象(不必手動建立)NSURLCache *cache = [NSURLCache sharedURLCache]; 瀏覽器

(2)設置內存緩存的最大容量(字節爲單位,默認爲512KB)- (void)setMemoryCapacity:(NSUInteger)memoryCapacity;緩存

(3)設置硬盤緩存的最大容量(字節爲單位,默認爲10M)- (void)setDiskCapacity:(NSUInteger)diskCapacity;服務器

(4)硬盤緩存的位置:沙盒/Library/Caches網絡

(5)取得某個請求的緩存- (NSCachedURLResponse *)cachedResponseForRequest:(NSURLRequest *)request; 

(6)清除某個請求的緩存- (void)removeCachedResponseForRequest:(NSURLRequest *)request;

(7)清除全部的緩存- (void)removeAllCachedResponses;

 

緩存GET請求:

  要想對某個GET請求進行數據緩存,很是簡單

  NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];

  // 設置緩存策略

  request.cachePolicy = NSURLRequestReturnCacheDataElseLoad;

  只要設置了緩存策略,系統會自動利用NSURLCache進行數據緩存

 

iOS對NSURLRequest提供了7種緩存策略:(實際上能用的只有4種)

NSURLRequestUseProtocolCachePolicy // 默認的緩存策略(取決於協議)

NSURLRequestReloadIgnoringLocalCacheData // 忽略緩存,從新請求

NSURLRequestReloadIgnoringLocalAndRemoteCacheData // 未實現

NSURLRequestReloadIgnoringCacheData = NSURLRequestReloadIgnoringLocalCacheData // 忽略緩存,從新請求

NSURLRequestReturnCacheDataElseLoad// 有緩存就用緩存,沒有緩存就從新請求

NSURLRequestReturnCacheDataDontLoad// 有緩存就用緩存,沒有緩存就不發請求,當作請求出錯處理(用於離線模式)

NSURLRequestReloadRevalidatingCacheData // 未實現

 

緩存的注意事項:

緩存的設置須要根據具體的狀況考慮,若是請求某個URL的返回數據:

  (1)常常更新:不能用緩存!好比股票、彩票數據

  (2)一成不變:果斷用緩存

  (3)偶爾更新:能夠按期更改緩存策略 或者 清除緩存

提示:若是大量使用緩存,會越積越大,建議按期清除緩存

NSURLCache的屬性介紹:

//獲取當前應用的緩存管理對象
+ (NSURLCache *)sharedURLCache;
//設置自定義的NSURLCache做爲應用緩存管理對象
+ (void)setSharedURLCache:(NSURLCache *)cache;
//初始化一個應用緩存對象
/*
memoryCapacity 設置內存緩存容量
diskCapacity 設置磁盤緩存容量
path 磁盤緩存路徑
內容緩存會在應用程序退出後 清空 磁盤緩存不會
*/
- (instancetype)initWithMemoryCapacity:(NSUInteger)memoryCapacity diskCapacity:(NSUInteger)diskCapacity diskPath:(nullable NSString *)path;
//獲取某一請求的緩存
- (nullable NSCachedURLResponse *)cachedResponseForRequest:(NSURLRequest *)request;
//給請求設置指定的緩存
- (void)storeCachedResponse:(NSCachedURLResponse *)cachedResponse forRequest:(NSURLRequest *)request;
//移除某個請求的緩存
- (void)removeCachedResponseForRequest:(NSURLRequest *)request;
//移除全部緩存數據
- (void)removeAllCachedResponses;
//移除某個時間起的緩存設置
- (void)removeCachedResponsesSinceDate:(NSDate *)date NS_AVAILABLE(10_10, 8_0);
//內存緩存容量大小
@property NSUInteger memoryCapacity;
//磁盤緩存容量大小
@property NSUInteger diskCapacity;
//當前已用內存容量
@property (readonly) NSUInteger currentMemoryUsage;
//當前已用磁盤容量
@property (readonly) NSUInteger currentDiskUsage;

與HTTP服務器進行交互的簡單說明:

  Cache-Control頭

   在第一次請求到服務器資源的時候,服務器須要使用Cache-Control這個響應頭來指定緩存策略,它的格式以下:Cache-Control:max-age=xxxx,這個頭指指明緩    存過時的時間

 Cache-Control頭具備以下選項:

  • public: 指示可被任何區緩存

  • private

  • no-cache: 指定該響應消息不能被緩存

  • no-store: 指定不該該緩存

  • max-age: 指定過時時間

  • min-fresh:

  • max-stable:

Last-Modified/If-Modified-Since

Last-Modified 是由服務器返回響應頭,標識資源的最後修改時間.

If-Modified-Since 則由客戶端發送,標識客戶端所記錄的,資源的最後修改時間。服務器接收到帶有該請求頭的請求時,會使用該時間與資源的最後修改時間進行對比,若是發現資源未被修改過,則直接返回HTTP 304而不返回包體,告訴客戶端直接使用本地的緩存。不然響應完整的消息內容。

Etag/If-None-Match

Etag 由服務器發送,告之當資源在服務器上的一個惟一標識符。

客戶端請求時,若是發現資源過時(使用Cache-Control的max-age),發現資源具備Etag聲明,這時請求服務器時則帶上If-None-Match頭,服務器收到後則與資源的標識進行對比,決定返回200或者304。

文件緩存:藉助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

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

實例:




/*! @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]; }

通常數據類型藉助 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。

相關文章
相關標籤/搜索