iOS~URLCache探索
一個隨時需要進行HTTP請求的完善的iOS應用,為了流暢的體驗,用戶流量的節省,緩存是不得不考慮的需求。值得慶幸的是,Apple已經為開發者們做好了這一切,接下來,就一起研究一下一個被很多開發者忽略的類:NSURLCache。
了解NSURLCahe
NSURLCache類用NSURLRequest對象和NSCachedURLResponse對象的一對一映射關系實現了請求數據的緩存。它同時提供內存緩存和硬盤緩存,你可以分別自定義內存緩存和硬盤緩存的大小,同時也可以自定義硬盤緩存的目錄。
這是官方文檔對NSURLCache的描述。其中NSURLRequest對象是請求對象,不必多說。NSCachedURLResponse對象是對緩存數據的封裝,其中的data屬性是請求回來的JSON(或者其他格式)的二進制數據。
以下是NSURLCache類提供的方法,基本能夠滿足大多數的緩存需求。
@interface NSURLCache : NSObject
/** 緩存類的單例 */
@property (class, strong) NSURLCache *sharedURLCache;
/** 初始化方法 */
- (instancetype)initWithMemoryCapacity:(NSUInteger)memoryCapacity diskCapacity:(NSUInteger)diskCapacity diskPath:(nullable NSString *)path;
/** 取得緩存數據的方法 */
- (nullable NSCachedURLResponse *)cachedResponseForRequest:(NSURLRequest *)request;
/** 存儲緩存數據的方法 */
- (void)storeCachedResponse:(NSCachedURLResponse *)cachedResponse forRequest:(NSURLRequest *)request;
/** 刪除指定request的緩存 */
- (void)removeCachedResponseForRequest:(NSURLRequest *)request;
/** 刪除全部緩存 */
- (void)removeAllCachedResponses;
/** 刪除緩存數據的一部分 */
- (void)removeCachedResponsesSinceDate:(NSDate *)date;
/** 內存緩存的大小 單位:字節 */
@property NSUInteger memoryCapacity;
/** 硬盤緩存的大小 單位:字節 */
@property NSUInteger diskCapacity;
/** 當前可用的內存緩存大小 單位:字節 */
@property (readonly) NSUInteger currentMemoryUsage;
/** 當前可用的硬盤緩存大小 單位:字節 */
@property (readonly) NSUInteger currentDiskUsage;
@end
緩存工作過程的理解
事實上,就算什么也不寫,系統也會根據默認的規則幫你緩存HTTP請求。但是項目中諸多的邏輯往往并不能讓我們如此悠閑。
此處舉一個小例子:項目中的請求一般都需要把參數加密,一般的加密算法,同樣一個請求,每次加密出來的串都是不一樣的。上面說過,NSURLCache是用NSURLRequest作為Key來實現緩存的,每次的URL不同導致每次取到的緩存都為空。這時候就需要做一些事情來保證緩存系統按照我們期望的樣子正常運行。
我自己的理解和總結,NSURLCache的工作過程是這樣的:
1.請求前的配置,包括請求頭,響應頭,超時時間以及緩存策略(后面會說到有關緩存策略)。
2.真正去服務器請求前,判斷緩存策略,調用cachedResponseForRequest:(NSURLRequest *)request方法試著取緩存或者直接請求網絡。
3.如果緩存策略允許取緩存,并且取到了緩存,請求成功并且返回緩存數據。
4.如果緩存策略允許取緩存,并且沒有取到緩存,再次判斷緩存策略,如果緩存策略允許聯網,則聯網請求,否則,請求失敗。
5.上述2,3任何一種請求成功的話,判斷緩存策略和服務器返回的響應頭。如果允許存儲,則調用storeCachedResponse:(NSCachedURLResponse *)cachedResponse forRequest:(NSURLRequest *)request將返回的數據存儲到內存以及硬盤,否則,直接返回請求成功。
下面是Apple提供的7種緩存策略以及含義:
如果使用了默認緩存策略,也就是上面表格中第一個,需要從返回的response的header中獲取相應的字段來指導緩存該如何進行。
1.Cache-Control字段:常用的有 no-cache,no-store,和max-age。其中no-cache代表不能使用這個緩存,no-store代表不存儲這個數據,max-age代表緩存的有效期(單位為秒)。
2.Expires字段:緩存過期時間,后面跟一個日期,此日期之前都可以直接使用本緩存。如果Expires與Cache-Control同時存在,則Cache-Control優先。
3.Last-Modified和If-Modified-Since字段:如果response中有Last-Modified,則在下次請求時,給request的header設置If-Modified-Since為Last-Modified的值,服務器校驗數據是否有變化,如果有變化,返回新數據,否則,返回304狀態碼,可以使用此緩存。
4.ETag和If-None-Match字段:如果response中有ETag,則在下次請求時,給request的header設置If-None-Match為ETag的值,服務器校驗數據是否有變化,如果有變化,返回新數據,否則,返回304狀態碼,可以使用此緩存。
以上的緩存協議字段只是我所了解的比較常見的幾種,當然HTTP緩存協議還包括很多很多的內容,有興趣的同學可以自行了解。
Demo應用
為了加深理解,我寫了一個小Demo來探索NSURLCache的運行過程。
Demo很簡單,只有WXYRequest請求類,繼承自NSURLCache的自定義WXYURLCache類和發起請求的ViewController控制器。
第一步、配置自定義緩存類。
//配置緩存
NSUInteger memoryCapacity = 20*1024*1024;
NSUInteger diskCapacity = 50*1024*1024;
WXYURLCache *customURLCache = [[WXYURLCache alloc] initWithMemoryCapacity:memoryCapacity diskCapacity:diskCapacity diskPath:[WXYURLCache customCachePath]];
[NSURLCache setSharedURLCache:customURLCache];
設置了20M的內存緩存和50M的硬盤緩存。以及自定義的緩存目錄。這是一個單例,設置了之后,整個工程里走系統緩存的請求都會遵循這個設置。此處需要注意一點,自定義的目錄只需要設置一個目錄名即可,它會自動存到應用程序沙盒的Caches目錄下,不需要手動獲取Caches目錄。
+ (NSString *)customCachePath{
return @"CustomCache";
}
第二步、設置請求,這里使用了AFNetworking。
+ (void)requestWithSuccess:(SuccessBlock)success failure:(failureBlock)failure{
AFHTTPSessionManager *sessionManager = [AFHTTPSessionManager manager];
//配置請求頭
sessionManager.requestSerializer = [AFHTTPRequestSerializer serializer];
sessionManager.requestSerializer.cachePolicy = [self getCachePolicy];//緩存策略
//配置響應頭
sessionManager.responseSerializer = [AFJSONResponseSerializer serializer];
[sessionManager GET:customURLString parameters:nil progress:nil success:^(NSURLSessionDataTask * _Nonnull task, id _Nullable responseObject) {
NSLog(@"\n請求成功:\n \nURL:%@\n \nresponse:%@\n\n", task.currentRequest.URL.absoluteString, responseObject);
success(responseObject);
} failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
NSLog(@"\n請求失敗: \n \nURL:%@\n \nerror:%@\n\n", task.currentRequest.URL.absoluteString, [error.userInfo objectForKey:@"NSLocalizedDescription"]);
failure(error);
}];
}
其中的緩存策略,每種都試了一遍。
+ (NSURLRequestCachePolicy)getCachePolicy{
NSURLRequestCachePolicy cachePolicy;
/** 根據后臺返回的響應頭來做判斷如何緩存 */
//cachePolicy = NSURLRequestUseProtocolCachePolicy;
/** 每次刷新,不取緩存 */
//cachePolicy = NSURLRequestReloadIgnoringLocalCacheData;
/** 有緩存取緩存,無緩存請求 */
cachePolicy = NSURLRequestReturnCacheDataElseLoad;
/** 有緩存取緩存,無緩存返回失敗 */
//cachePolicy = NSURLRequestReturnCacheDataDontLoad;
return cachePolicy;
}
第三步、重寫NSURLCache的方法。
重寫取緩存方法。
- (NSCachedURLResponse *)cachedResponseForRequest:(NSURLRequest *)request{
NSCachedURLResponse *cachedURLResponse = [super cachedResponseForRequest:request];
id cacheData = nil;
if (!cachedURLResponse.data) {
cacheData = @"取到的緩存為空";
}
else{
cacheData = [NSJSONSerialization JSONObjectWithData:cachedURLResponse.data options:NSJSONReadingMutableContainers error:nil];
}
NSLog(@"\n取緩存:\n \nURL:%@\n \nresponse:%@\n\n", request.URL.absoluteString, cacheData);
return cachedURLResponse;
}
重寫存緩存方法。
- (void)storeCachedResponse:(NSCachedURLResponse *)cachedResponse forRequest:(NSURLRequest *)request{
id cacheData = [NSJSONSerialization JSONObjectWithData:cachedResponse.data options:NSJSONReadingMutableContainers error:nil];
NSLog(@"\n存緩存:\n \nURL:%@\n \nresponse:%@\n\n", request.URL.absoluteString, cacheData);
[super storeCachedResponse:cachedResponse forRequest:request];
}
最后一步、請求數據看控制臺輸出。
這里使用了一個獲取域名資質信息的免費接口。
#define customURLString @"http://www.sojson.com/api/beian/baidu.com"
發起請求,以NSURLRequestReturnCacheDataElseLoad(有緩存取緩存,無緩存請求)緩存策略為例。
/** 發起請求 */
- (IBAction)WXY_Requet:(id)sender {
[WXYRequest requestWithSuccess:^(id response) {
} failure:^(NSError *error) {
}];
}
可以看到控制臺的輸出是這樣的:
2017-03-20 23:12:23.425763 WXYURLChache[530:99758]
取緩存:
URL:http://www.sojson.com/api/beian/baidu.com
response:取到的緩存為空
2017-03-20 23:12:24.094012 WXYURLChache[530:99758]
存緩存:
URL:http://www.sojson.com/api/beian/baidu.com
response:{
checkDate = "";
domain = " baidu.com ";
icp = "\U4eacICP\U8bc1030173\U53f7";
indexUrl = "www.baidu.com";
name = "\U5317\U4eac\U767e\U5ea6\U7f51\U8baf\U79d1\U6280\U6709\U9650\U516c\U53f8";
nature = "\U4f01\U4e1a";
nowIcp = "\U4eacICP\U8bc1030173\U53f7-1";
search = "baidu.com";
sitename = "\U767e\U5ea6";
type = 200;
}
2017-03-20 23:12:24.095622 WXYURLChache[530:99560]
請求成功:
URL:http://www.sojson.com/api/beian/baidu.com
response:{
checkDate = "";
domain = " baidu.com ";
icp = "\U4eacICP\U8bc1030173\U53f7";
indexUrl = "www.baidu.com";
name = "\U5317\U4eac\U767e\U5ea6\U7f51\U8baf\U79d1\U6280\U6709\U9650\U516c\U53f8";
nature = "\U4f01\U4e1a";
nowIcp = "\U4eacICP\U8bc1030173\U53f7-1";
search = "baidu.com";
sitename = "\U767e\U5ea6";
type = 200;
}
首先根據緩存策略取緩存,因為是第一次請求,沒有緩存。然后聯網請求,將請求回來的數據存入緩存。最后返回請求成功。通過Charles抓包抓到了一個HTTP請求。
這時什么也不做,發起第二次同樣的的請求,可以看到這時的控制臺輸出變成了這樣:
2017-03-20 23:19:29.117268 WXYURLChache[530:99619]
取緩存:
URL:http://www.sojson.com/api/beian/baidu.com
response:{
checkDate = "";
domain = " baidu.com ";
icp = "\U4eacICP\U8bc1030173\U53f7";
indexUrl = "www.baidu.com";
name = "\U5317\U4eac\U767e\U5ea6\U7f51\U8baf\U79d1\U6280\U6709\U9650\U516c\U53f8";
nature = "\U4f01\U4e1a";
nowIcp = "\U4eacICP\U8bc1030173\U53f7-1";
search = "baidu.com";
sitename = "\U767e\U5ea6";
type = 200;
}
2017-03-20 23:19:29.120207 WXYURLChache[530:100693]
存緩存:
URL:http://www.sojson.com/api/beian/baidu.com
response:{
checkDate = "";
domain = " baidu.com ";
icp = "\U4eacICP\U8bc1030173\U53f7";
indexUrl = "www.baidu.com";
name = "\U5317\U4eac\U767e\U5ea6\U7f51\U8baf\U79d1\U6280\U6709\U9650\U516c\U53f8";
nature = "\U4f01\U4e1a";
nowIcp = "\U4eacICP\U8bc1030173\U53f7-1";
search = "baidu.com";
sitename = "\U767e\U5ea6";
type = 200;
}
2017-03-20 23:19:29.123088 WXYURLChache[530:99560]
請求成功:
URL:http://www.sojson.com/api/beian/baidu.com
response:{
checkDate = "";
domain = " baidu.com ";
icp = "\U4eacICP\U8bc1030173\U53f7";
indexUrl = "www.baidu.com";
name = "\U5317\U4eac\U767e\U5ea6\U7f51\U8baf\U79d1\U6280\U6709\U9650\U516c\U53f8";
nature = "\U4f01\U4e1a";
nowIcp = "\U4eacICP\U8bc1030173\U53f7-1";
search = "baidu.com";
sitename = "\U767e\U5ea6";
type = 200;
}
這次取緩存的方法取到了緩存,同樣將數據存儲了一次。然后返回請求成功。
通過Charles抓包沒有抓到任何請求。說明這次并沒有聯網請求,而是根據緩存策略直接使用的緩存。
把手機打開飛行模式,再次發起請求。可以看到控制臺的輸出和上面是一樣的。說明取緩存的策略下,即使是沒有網絡,也會返回請求成功。
最后一次實驗,飛行模式打開,清除掉緩存。
/** 清空緩存 */
- (IBAction)WXY_RemoveCache:(id)sender {
[[NSURLCache sharedURLCache] removeAllCachedResponses];
}
這時發起請求,可以看到控制臺的輸出是這樣:
2017-03-20 23:28:37.618673 WXYURLChache[530:101997]
取緩存:
URL:http://www.sojson.com/api/beian/baidu.com
response:取到的緩存為空
2017-03-20 23:28:37.646091 WXYURLChache[530:99560]
請求失敗:
URL:http://www.sojson.com/api/beian/baidu.com
error:The Internet connection appears to be offline.
首先根據緩存策略取緩存,取不到緩存,聯網請求,無網絡請求失敗之后,并不會調用存儲緩存的方法,直接返回請求失敗。
以上只是探討了NSURLRequestReturnCacheDataElseLoad這一種緩存策略的表現情況。其他策略,限于篇幅,不做贅述。有興趣的同學可以自行試驗以加深理解。
寫在最后
因為最近要替換掉項目中基于ASI的網絡框架,改用AFN。在封裝的過程中,順便研究了一下緩存相關,覺得有必要記錄一下,分享給大家,所以寫了這篇文章。
另外,值得注意的是,ASI并不是基于NSURLSession或者NSURLCOnnection的封裝,所以并不會走NSURLCache的緩存,它有自己的一套緩存系統。只有NSURLSession或者NSURLCOnnection的請求才會走Apple提供的這個緩存類。
還有,看了一下緩存的沙盒目錄,NSURLCache通過數據庫來實現存儲緩存。
最后,像前面說的,并不是有了這個類,就可以不去管緩存的事情了,根據項目架構和需求的不同,在NSURLCache之上需要做的還有很多很多。
來自:http://www.cocoachina.com/ios/20170322/18934.html