使用緩存的目的是爲了使應用程序能更快速的響應用戶輸入,是程序高效的運行。有時候咱們須要將遠程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。
服務器的文件存貯,大多采用資源變更後就從新生成一個連接的作法。並且若是你的文件存儲採用的是第三方的服務,好比七牛、青雲等服務,則必定是如此。
這種作法雖然是推薦作法,但同時也不排除不一樣文件使用同一個連接。那麼若是服務端的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
是什麼?
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 層面作這種校驗,只在有業務需求時纔會考慮作,好比:
一些建議:
Last-Modified
就夠了。即便 ETag
是首選,但此時二者效果一致。九成以上的需求,效果都一致。若是是通常的數據類型--基於查詢的 get 請求,好比返回值是 data 或 string 類型的 json 返回值。那麼 Last-Modified
服務端支持起來就會困難一點。由於好比
你作了一個博客瀏覽 app ,查詢最近的10條博客, 基於此時的業務考慮 Last-Modified
指的是10條中任意一個博客的更改。那麼服務端須要在你發出請求後,遍歷下10條數據,獲得「10條中是否至少一個被修改了」。並且要保證每一條博客表數據都有一個相似於記錄 Last-Modified
的字段,這顯然不太現實。
若是更新頻率較高,好比最近微博列表、最近新聞列表,這些請求就不適合,更多的處理方式是添加一個接口,客戶端將本地緩存的最後一條數據的的時間戳或 id 傳給服務端,而後服務端會將新增的數據條數返回,沒有新增則返回 nil 或 304。