IOS网络缓存

前言

前面一篇文章HTTP协议之缓存介绍了HTTP协议中缓存是如何定义,客户端,服务端一般要如何根据HTTP中定义的这些字段来实现缓存逻辑

接下来要着重分析一下IOS里面的网络框架,是如何进行网络缓存的
本文使NSURLSession进行网络请求

用什么缓存

ios中的网络缓存主要用到 NSURLCache,结合NSURLSession,NSURLRequest,系统已经为我们做了大部分工作。
NSURLCache将缓存保存到应用程序沙盒Library/Caches/xxx.xxx/路径下

用数据库来保存每个接口的缓存数据。
为什么要用数据库保存而不是以请求做文件名,以请求内容做文件内容保存,因为NSURLCache不光要保存请求的数据,还要保存很多额外的信息

  • 响应头信息(各种Cache-Control字段等等)
  • 缓存的版本
  • 时间戳
  • 一些其他的控制信息

这些字段用来对缓存进行更好的控制,比如当缓存超过设置空间上限的时候,清理缓存的策略等等

Cached.db

如何启用,禁用缓存

启用缓存

系统已经为我们做了足够多的工作,我们只需要很少的步骤就能启用NSURLCache这套缓存系统

如果我们使用系统默认的NSURLSession *session=[NSURLSession sharedSession];那么我们实际上不需要配置任何代码,因为系统默认的全局NSURLSessionNSURLSessionConfiguration里面已经默认配置了一个NSURLCache对象,这个NSURLCache对象也是一个系统默认的全局对象,可以通过NSURLCache *cache=[NSURLCache sharedURLCache];获得

这样我们通过NSURLRequest成功返回的GET请求都会被缓存,注意请求被缓存与我们设置的NSURLRequest的cachePolicy无关

当然我们也可以自己创建一个NSURLCache,自定义内存以及硬盘缓存的容量,并绑定到全局使用

如下代码创建了一个缓存配置,并设置为全局共享缓存配置

1
2
3
4
5
6
7
//新建一个缓存配置
NSURLCache *cache=[[NSURLCache alloc]initWithMemoryCapacity:10*1024*1024
diskCapacity:50*1024*1024
diskPath:nil];

//绑定到全局
[NSURLCache setSharedURLCache:cache];

修改NSURLSessionConfiguration使用的缓存对象
默认的NSURLSessionConfiguration使用的是系统创建的NSURLCache或者我们通过[NSURLCache setSharedURLCache:cache]配置的cache,我们也可以修改config的URLCache属性来直接指定一个NSURLCache对象

1
2
3
4
5
6
//新建一个缓存配置
NSURLCache *cache=[[NSURLCache alloc]initWithMemoryCapacity:10*1024*1024
diskCapacity:50*1024*1024
diskPath:nil];
NSURLSession *session=[NSURLSession sharedSession];
session.configuration.URLCache=cache;

禁用缓存

缓存的启用是通过为NSURLSessionConfiguration配置一个NSURLCache对象(默认的session,默认的config使用的是默认的NSURLCache对象),那么缓存的禁用可以置空这个session的config.URLCache属性,这样这个session的所有请求都不会被缓存

1
2
NSURLSession *session=[NSURLSession sharedSession];
session.configuration.URLCache=nil;

这种方式对这个session中的所有请求都生效,如果我们想只是针对单独的某个请求,可以实现NSURLSession 的delegate

1
2
3
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask
willCacheResponse:(NSCachedURLResponse *)proposedResponse
completionHandler:(void (^)(NSCachedURLResponse * __nullable cachedResponse))completionHandler;

当某个请求将要被缓存的时候,会调用此delegate,在这里我们可以修改将要被缓存的response,同时决定是否缓存这个请求。如下代码修改response中的Cache-Control字段并缓存请求。

1
2
3
4
5
6
7
8
9
10
11
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask
willCacheResponse:(NSCachedURLResponse *)proposedResponse
completionHandler:(void (^)(NSCachedURLResponse * __nullable cachedResponse))completionHandler
{
NSHTTPURLResponse *res= proposedResponse.response;
NSMutableDictionary *headers=res.allHeaderFields.mutableCopy;
[headers setObject:@"max-age=1000000" forKey:@"Cache-Control"];
NSHTTPURLResponse *final=[[NSHTTPURLResponse alloc]initWithURL:proposedResponse.response.URL statusCode:res.statusCode HTTPVersion:@"HTTP/1.1" headerFields:headers];
NSCachedURLResponse *cache=[[NSCachedURLResponse alloc]initWithResponse:final data:proposedResponse.data];
completionHandler(cache);
}

如果不想缓存请求 调用completionHandler(nil)即可

IOS CachePolicy

首先说明,无论CachePolicy被设置成什么,只要session的config中配置了NSURLCache对象,那么,这个session中的GET请求就会执行缓存操作,当然是否缓存成功还要看是否实现了willCacheResponse这个delegate 进行控制。

所以在IOS 这套网络框架里,CachePolicy不是决定是否缓存请求,而是决定,如何使用缓存

NSURLSessionConfiguration里面可以配置一个缓存策略

1
2
3
NSURLSessionConfiguration *config=[NSURLSessionConfiguration defaultSessionConfiguration];

config.requestCachePolicy=NSURLRequestUseProtocolCachePolicy;

在请求的时候,每个NSURLRequest也可以单独配置一个缓存策略

1
2
NSMutableURLRequest * request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:@"http://www.xxx.com"]];
request.cachePolicy=NSURLRequestUseProtocolCachePolicy;

经过测试,当NSURLRequest没有配置缓存策略的时候,会使用session的config配置的缓存策略,
如果NSURLRequest配置了缓存策略,那么使用NSURLRequest自己的缓存策略

NSURLRequestUseProtocolCachePolicy

使用HTTP协议定义的缓存字段来控制缓存行为,只支持Cache-control: max-age=xxx这种形式,前面提到,如果配置了NSURLCache,每个GET请求都会被缓存,当相同的请求第二次请求的时候,查看缓存的响应头中是否包含Cache-control: max-age=xxx

如果不包含,那么根据HTTP协议的定义,这个请求是不需要被缓存的,所以,即使本地已经有缓存,也不会使用,而是重新发起网络请求

如果包含Cache-control: max-age=xxx查看缓存是否已经失效,如果没失效,返回缓存数据,不发起请求,如果失效,那么重新发起网络请求

NSURLRequestReloadIgnoringLocalCacheData|NSURLRequestReloadIgnoringCacheData

这两个策略的定义是一样的,都是忽略本地缓存,每次都发起网络请求

NSURLRequestReturnCacheDataElseLoad

如果本地有缓存,使用缓存,不需要去判断Cache-control: max-age=xxx这些东西,如果没有缓存,发起网络请求

NSURLRequestReturnCacheDataDontLoad

如果本地有缓存,使用缓存,不需要去判断Cache-control: max-age=xxx这些东西,如果没有缓存,返回失败,不进行网络请求,可以理解为离线模式

Last-Modified/ETag

上面提到这么多缓存策略,其实与HTTP协议缓存相关的只有NSURLRequestUseProtocolCachePolicy这种缓存策略,只有在这种策略下,才会用到Cache-control这些东西,并且只支持Cache-control这种缓存逻辑,那么我们之前文章提到过的Last-Modified,Etag这种缓存逻辑要怎么实现呢。

Last-Modified,Etag这两种缓存逻辑,没有被IOS封装进网络框架,所以如果我们想要支持这两种缓存逻辑,需要我们自己来实现。

下面用Last-Modify的方式举一个例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
NSMutableURLRequest * request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:@"http://wthrcdn.etouch.cn/weather_mini?city=北京"]];
request.cachePolicy=NSURLRequestUseProtocolCachePolicy;
request.HTTPMethod=@"GET";
//查看当前请求是否有本地缓存
NSCachedURLResponse *cacheResponse=[[NSURLCache sharedURLCache] cachedResponseForRequest:request];
if (cacheResponse) {
NSHTTPURLResponse *httpResponse=(NSHTTPURLResponse *)cacheResponse.response;
//拿到请求缓存的response header
NSDictionary *header=httpResponse.allHeaderFields;
//查看header是否存在Last-Modified字段,如果有的话,为本次请求头增加If-Modified-Since字段
NSString *lastModify=[header objectForKey:@"Last-Modified"];
if (lastModify) {
[request setValue:lastModify forHTTPHeaderField:@"If-Modified-Since"];
}
}

NSURLSessionDataTask *task=[self.session dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {

NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response;
NSLog(@"statusCode == %@", @(httpResponse.statusCode));
// 判断响应的状态码是否是 304 Not Modified
if (httpResponse.statusCode == 304) {
// 如果是,使用本地缓存
// 根据请求获取到`被缓存的响应`!
NSCachedURLResponse *cacheResponse = [[NSURLCache sharedURLCache] cachedResponseForRequest:request];
// 拿到缓存的数据
completion(cacheResponse.data);
return;
}

//返回200 使用服务端返回的新数据
completion(data);
}];

[self.task resume];