一个随时需要进行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之上需要做的还有很多很多。
以上。