NSURLCache 学习总结

Overview

NSURLCache 通过将 NSURLRequest 对象映射到 NSCachedURLResponse对象来实现对URL请求响应的缓存。

它提供了内存和磁盘缓存方式,同时允许设置内存和磁盘部分的大小,还可以控制缓存数据持久存储的路径。

NSURLCache 是线程安全的。

NSURLConnection、NSURLSession和UIWebView默认都会使用NSURLCache,所有经过他们请求的数据都将被NSURLCache处理。

缓存策略 NSURLRequestCachePolicy

NSURLRequestUseProtocolCachePolicy

默认缓存策略,对于特定URL使用网络协议中实现的缓存策略。

该策略下,当客户端发起一个请求时首先会检查本地是否包含缓存,如果有缓存则检查缓存是否过期(根据缓存的响应头中的 “Cache-Control:max-age” 或者 Expires判断),如果没有过期则直接使用缓存,如果过期则会发出请求(请求头中包含 “If-Modified-Since” 或者 “If-None-Match”),此时服务器端对比资源 “Last-Modified” 或者 “Etags”(二者都存在的情况下下如果有一个不同则认为缓存已过期),如果不同则返回新数据,否则返回304 Not Modified继续使用缓存数据。

该策略下,通常对于 NSURLSession 不做任何设置,只要服务器端响应头部加上 Cache-Control:max-age:xxx 就可以使用缓存了。

NSURLRequestReloadIgnoringLocalCacheData

忽略缓存,直接请求原始数据。

NSURLRequestReturnCacheDataElseLoad

无论缓存是否过期,有缓存则使用缓存数据,否则请求原始数据。

NSURLRequestReturnCacheDataDontLoad

无论缓存是否过期,有缓存则使用缓存数据,否则视为失败,不会请求原始数据。

使用 NSURLSessionDataDelegate 来更改缓存策略

场景一:服务端没有在响应头中配置 “Cache-Control”,且当前客户端使用的策略是 “NSURLRequestUseProtocolCachePolicy” 时,可以通过NSURLSessionDataDelegate 修改响应头信息,来启用缓存,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
- (void)URLSession:(NSURLSession *)session
dataTask:(NSURLSessionDataTask *)dataTask
willCacheResponse:(NSCachedURLResponse *)proposedResponse
completionHandler:(void (^)(NSCachedURLResponse * _Nullable))completionHandler
{
NSHTTPURLResponse *response = (NSHTTPURLResponse *)proposedResponse.response;
NSDictionary *allHeaders = [response allHeaderFields];
if (!allHeaders[@"Cache-Control"]) {
NSMutableDictionary *newHeaders = [NSMutableDictionary dictionaryWithDictionary:allHeaders];
newHeaders[@"Cache-Control"] = @"max-age=60";
NSHTTPURLResponse *newResponse = [[NSHTTPURLResponse alloc] initWithURL:response.URL statusCode:response.statusCode HTTPVersion:@"HTTP/1.1" headerFields:newHeaders];
NSCachedURLResponse *newCachedURLResponse = [[NSCachedURLResponse alloc] initWithResponse:newResponse data:proposedResponse.data];
completionHandler(newCachedURLResponse);
}else {
completionHandler(proposedResponse);
}
}

场景二:针对某些特殊的请求,可以设置只进行内存缓存,具体代码如下:

1
2
3
4
5
6
7
8
9
- (void)URLSession:(NSURLSession *)session
dataTask:(NSURLSessionDataTask *)dataTask
willCacheResponse:(NSCachedURLResponse *)proposedResponse
completionHandler:(void (^)(NSCachedURLResponse * _Nullable))completionHandler
{
// 只进行内存缓存
NSCachedURLResponse *newCacheResponse = [[NSCachedURLResponse alloc] initWithResponse:proposedResponse.response data:proposedResponse.data userInfo:proposedResponse.userInfo storagePolicy:NSURLCacheStorageAllowedInMemoryOnly];
completionHandler(newCacheResponse);
}

NSURLCache能缓存POST请求吗?

网上很多文章在讲到NSURLCache时都说“NSURLCache 无法缓存POST请求的响应”,但我实际测试发现是可以的,具体可以查看demo。大致代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
 // 创建配置
NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration];

// 创建session
NSURLSession *session = [NSURLSession sessionWithConfiguration:config delegate:self delegateQueue:[NSOperationQueue mainQueue]];

// 创建POST请求
NSURL *url = [NSURL URLWithString:@"http://api.juheapi.com/japi/toh"];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
request.HTTPMethod = @"POST";
NSString *param = [NSString stringWithFormat:@"key=%@&v=1.0&month=%@&day=%@",
kAPPKey, self.monthTextField.text, self.dayTextField.text];
request.HTTPBody = [param dataUsingEncoding:NSUTF8StringEncoding];

// 创建任务
NSURLSessionDataTask *task = [session dataTaskWithRequest:request];
// 发送
[task resume];

如果请求成功,会在沙盒的Library/Caches目录下发现一个以BundleID命名的文件夹,这个文件夹里存放的就是NSURLCache缓存数据的数据库,也就是磁盘缓存的地址:

打开Cache.db,里面有如下几张表格:

其中cfurl_cahce_response存储着response相关信息,如下图可以看到刚才发出的POST请求的URL:

cfurl_cache_blob_data存储着response的响应体信息的二进制:

通过上面的缓存可以看出,NSURLCache其实是可以缓存POST请求的。当然也可以在请求一次后,断网再请求如果能够获取响应,也可以证明。

获取NSURLCache的POST请求缓存

既然已经知道NSURLCache可以缓存POST请求了,下一步就试试能否获取,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13

NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:@"http://api.juheapi.com/japi/toh"]];
request.HTTPMethod = @"POST";

NSString *param = [NSString stringWithFormat:@"key=%@&v=1.0&month=%@&day=%@", kAPPKey, self.monthTextField.text, self.dayTextField.text];
request.HTTPBody = [param dataUsingEncoding:NSUTF8StringEncoding];

NSCachedURLResponse *cachedResponse = [cache cachedResponseForRequest:request];

if (cachedResponse) {
NSString *responseString = [[NSString alloc] initWithData:cachedResponse.data encoding:NSUTF8StringEncoding];
[self showTitile:@"POST缓存" string:responseString];
}

结果发现居然获取不到缓存,仔细检查代码确定跟发起请求时的参数完全一致,难道是cachedResponseForRequest方法有问题吗?于是又用GET请求了一次,并使用cachedResponseForRequest获取缓存,这次成功获取到了,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
NSURLCache *cache = [NSURLCache sharedURLCache];

NSURL *url = [NSURL URLWithString:[NSString stringWithFormat:@"http://api.juheapi.com/japi/toh?key=%@&v=1.0&month=%@&day=%@",kAPPKey, self.monthTextField.text, self.dayTextField.text]];

NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];

NSCachedURLResponse *cachedResponse = [cache cachedResponseForRequest:request];

if (cachedResponse) {
NSString *responseString = [[NSString alloc] initWithData:cachedResponse.data encoding:NSUTF8StringEncoding];
[self showTitile:@"GET缓存" string:responseString];
}

这又是为什么呢?同样的方法,GET能取到而POST却不可以。于是换了个参数,再次请求,发现结果依旧是GET请求的缓存能获取,而POST不可以。

抱着疑惑打开了上文提到的cache.db,也就是NSURLCache的磁盘缓存,打开cfurl_cache_response

通过request_key,我们知道第一条是POST请求,而后面两条是GET请求,但是我明明是分别请求了两次GET请求和两次POST请求,还有一条POST请求的缓存呢?

回想一下POST请求和GET请求的不同,主要体现在POST请求参数是放在请求体中,而GET请求参数是放在URL中的,而上面的表格并没有任何请求体相关信息,只有一个request_key(就是请求的URL),对此我提出一个猜测:

cachedResponseForRequest: 方法是根据request中的URL来获取缓存的(request.url.absoluteString),而不包含请求体部分。

为了验证这个猜测,我们把上面获取POST请求缓存的代码进行修改,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13

NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:@"http://api.juheapi.com/japi/toh"]];
request.HTTPMethod = @"POST";

// NSString *param = [NSString stringWithFormat:@"key=%@&v=1.0&month=%@&day=%@", kAPPKey, self.monthTextField.text, self.dayTextField.text];
// request.HTTPBody = [param dataUsingEncoding:NSUTF8StringEncoding];

NSCachedURLResponse *cachedResponse = [cache cachedResponseForRequest:request];

if (cachedResponse) {
NSString *responseString = [[NSString alloc] initWithData:cachedResponse.data encoding:NSUTF8StringEncoding];
[self showTitile:@"POST缓存" string:responseString];
}

成功获取到了POST请求的缓存,由此得出结论:

1.NSURLCache 可以缓存POST请求的响应
2.NSURLCache 是根据请求的URL进行缓存的,与请求体无关

这样的结论也意味着,进行多次POST请求且仅仅是参数不同,记录只是最后一条缓存,之前的缓存都会被覆盖。如果想要缓存每一次的POST请求,就要自行实现缓存策略,而不是使用NSURLCache。

如何关闭 NSURLCache 缓存

既然 NSURLCache 并没有想象中那么美好,那就试试让它不要自动去缓存,以下提供两种方法关闭 NSURLCache 缓存。

第一种是将 NSURLCache 缓存大小设置为0,代码如下:

1
2
NSURLCache *cache = [[NSURLCache alloc] initWithMemoryCapacity:0 diskCapacity:0 diskPath:nil];
[NSURLCache setSharedURLCache:cache];

第二种是在请求头中设置 “Cache-Control” 为 “no-cache”

1
[request setValue:@"no-cache" forHTTPHeaderField:@"Cache-Control"];

这两种方法无论在 NSURLRequestCachePolicy 哪种策略下,都不会去缓存。

参考文献

1.基于NSURLCache的缓存实现 - 崔江涛
2.Accessing Cached Data - Apple

0%