上篇文章 我们已经针对
NSCache
的底层了解了其具体的实现机制和淘汰策略 . 由于文章太长 , 不利于阅读 . 那么这篇文章 , 我们就NSURLCache
以及SDWebImage
中的缓存处理机制进行探究讲解.
目录我就继续上篇文章的来了 , 以便比较阅读.
2. NSURLCache
2.1 介绍
先把官方文档奉上 NSURLCache
首先我们都知道 , 使用 NSURLCache
进行请求数据的缓存时 , 同时本身默认也会有缓存的处理. 那么我们需要做什么 ? 原生默认做了什么 ?
The
NSURLCache
class implements the caching of responses to URL load requests by mappingNSURLRequest
objects toNSCachedURLResponse
objects. It provides a composite in-memory and on-disk cache, and lets you manipulate the sizes of both the in-memory and on-disk portions. You can also control the path where cache data is stored persistently.
啥意思呢 ? 重点就是 它提供了磁盘缓存和内存缓存 , 并可以让用户来指定缓存的磁盘和内存的大小 . 至于其他的什么时候来清楚这些磁盘或者内存中的缓存内容我们无需关心.
并且它提供给我们集中不同的策略以满足灵活多变的需求.
2.1.1 缓存策略
当使用 NSURLSession
时 , 我们可以直接通过 NSMutableURLRequest
来指定 NSURLRequestCachePolicy
.
typedef NS_ENUM(NSUInteger, NSURLRequestCachePolicy)
{
NSURLRequestUseProtocolCachePolicy = 0, //默认策略
NSURLRequestReloadIgnoringLocalCacheData = 1, //忽略缓存,必须从远程地址下载;
NSURLRequestReloadIgnoringLocalAndRemoteCacheData = 4, // Unimplemented 未实现
NSURLRequestReloadIgnoringCacheData = NSURLRequestReloadIgnoringLocalCacheData,
NSURLRequestReturnCacheDataElseLoad = 2, // 无论缓存是否过期 , 有就使用缓存 , 没有就请求数据.
NSURLRequestReturnCacheDataDontLoad = 3,// 无论缓存是否过期 , 有就使用缓存 , 没有就请求失败.
NSURLRequestReloadRevalidatingCacheData = 5, // Unimplemented 未实现
};
在了解 NSURLCache
之前 , 先来看看默认的 HTTP 缓存机制. 也就是 NSURLRequestCachePolicy
的默认策略.
2.1.2 HTTP 缓存策略
先来个默认使用 HTTP 缓存策略的例子.
- (void)example_1{
NSURL *url = [NSURL URLWithString:@"http://via.placeholder.com/50x50.jpg"];
//默认使用 HTTP缓存策略来进行缓存
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
[[[NSURLSession sharedSession] dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
if (error) {
NSLog(@"error warning : %@",error);
}else{
//从缓存当中读取数据!
NSData *tempData = data;
NSString *responseStr = [[NSString alloc] initWithData:tempData encoding:NSUTF8StringEncoding];
NSLog(@"response:%@",response);
}
}] resume];
}
打印结果:
response:<NSHTTPURLResponse: 0x600002ff7380> { URL: http://via.placeholder.com/50x50.jpg } { Status Code: 200, Headers {
"Accept-Ranges" = (
bytes
);
"Cache-Control" = (
"max-age=604800"
);
Connection = (
"keep-alive"
);
"Content-Length" = (
807
);
"Content-Type" = (
"image/jpeg"
);
Date = (
"Wed, 18 Sep 2019 06:32:38 GMT"
);
Etag = (
"\"5d5c8aae-327\""
);
Expires = (
"Wed, 25 Sep 2019 06:32:38 GMT"
);
"Last-Modified" = (
"Wed, 21 Aug 2019 00:05:02 GMT"
);
Server = (
"nginx/1.6.2"
);
"X-Cache" = (
L1
);
} }
其实在 HTTP 中,控制缓存开关的字段有两个:Pragma
和 Cache-Control
. Pragma
是旧产物,已经逐步抛弃,有些网站为了向下兼容还保留了这两个字段. 在此就不介绍了.
Cache-Control
在请求中使用 Cache-Control
时,它可选的值有:
缓存校验
在缓存中,我们需要一个机制来验证缓存是否有效。比如服务器的资源更新了,客户端需要及时刷新缓存;又或者客户端的资源过了有效期,但服务器上的资源还是旧的,此时并不需要重新发送。缓存校验就是用来解决这些问题的,
在 HTTP 1.1 中,我们主要关注下
Last-Modified
和etag
这两个字段。
Last-Modified
服务端在返回资源时,会将该资源的最后更改时间通过 Last-Modified
字段返回给客户端。客户端下次请求时通过 If-Modified-Since
或者 If-Unmodified-Since
带上 Last-Modified
,服务端检查该时间是否与服务器的最后修改时间一致:
- 如果一致,则返回 304 状态码,不返回资源;
- 如果不一致则返回 200 和修改后的资源,并带上新的时间。
If-Modified-Since
和 If-Unmodified-Since
的区别是:
If-Modified-Since
:告诉服务器如果时间一致,返回状态码304
If-Unmodified-Since
:告诉服务器如果时间不一致,返回状态码412
Etag
单纯的以修改时间来判断还是有缺陷,比如文件的最后修改时间变了,但内容没变。对于这样的情况,我们可以使用 Etag
来处理。
Etag
机制:
服务器通过某个算法对资源进行计算,取得一串值(类似于文件的
hash
值),之后将该值通过Etag
返回给客户端,客户端下次请求时通过If-None-Match
或If-Match
带上该值,服务器对该值进行对比校验:如果一致则不要返回资源。
If-None-Match
和 If-Match
的区别是:
If-None-Match
:告诉服务器如果一致,返回状态码304
,不一致则返回资源If-Match
:告诉服务器如果不一致,返回状态码412
既生 Last-Modified 何生 Etag ?
你可能会觉得使用 Last-Modified
已经足以让客户端知道本地的缓存副本是否足够新,为什么还需要 Etag
(实体标识)呢?HTTP 1.1 中 Etag
的出现主要是为了解决几个 Last-Modified
比较难解决的问题:
-
Last-Modified
标注的最后修改只能精确到秒级,如果某些文件在 1 秒钟以内,被修改多次的话,它将不能准确标注文件的修改时间 -
如果某些文件会被定期生成,当有时内容并没有任何变化,但
Last-Modified
却改变了,导致文件没法使用缓存 -
有可能存在服务器没有准确获取文件修改时间,或者与代理服务器时间不一致等情形
Etag
是服务器自动生成或者由开发者生成的对应资源在服务器端的唯一标识符,能够更加准确的控制缓存。Last-Modified
与ETag
是可以一起使用的,服务器会优先验证ETag
,一致的情况下,才会继续比对Last-Modified
,最后才决定是否返回 304。
HTTP 缓存机制总结
缓存开关是: pragma
, cache-control
.
缓存校验有:Expires
, Last-Modified
, etag
.
从整个流程来看 , 他们如下图: ( 图片源自网络 : 浏览器加载 HTTP 缓存机制 )
- 第一次请求 :
- 再次请求 :
2.1.3 HTTP 缓存内容查看
说了这么多 , 我们刚刚写的案例到底缓存了什么内容呢 . 加个断点 获取一下沙盒 , 然后我们打开文件夹看下 :
- 获取路径
- 查看沙盒文件
- 使用 DB 查看工具 , 我这里用的是
DB Browser for SQLite
- 选择第一张表
cfurl_cache_blob_data
浏览数据. - 选择第一条
response_object
右边导出二进制bin
文件 , 使用终端直接cat
命令查看文件.
可以看到 : 本次 HTTP 请求信息都被存储到这个数据库表中.
再继续查看你会发现所有的信息都在各个表中存储完毕.
2.1.4 使用其他缓存策略
还是之前的案例 , 我们稍微修改一下:
- (void)example_1{
NSURL *url = [NSURL URLWithString:@"http://via.placeholder.com/50x50.jpg"];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url cachePolicy:NSURLRequestReloadIgnoringCacheData timeoutInterval:15.0];
//默认使用 HTTP缓存策略来进行缓存
// NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];\
//比对服务,资源是否更新
if (self.lastModified) {
[request setValue:self.lastModified forHTTPHeaderField:@"If-Modified-Since"];
}
// if (self.etag) {
// [request setValue:self.etag forHTTPHeaderField:@"If-None-Match"];
// }
[[[NSURLSession sharedSession] dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
if (error) {
NSLog(@"error warning : %@",error);
}else{
//从缓存当中读取数据!
NSData *tempData = data;
NSString *responseStr = [[NSString alloc] initWithData:tempData encoding:NSUTF8StringEncoding];
self.lastModified = [(NSHTTPURLResponse *)response allHeaderFields][@"Last-Modified"];
// self.etag = [(NSHTTPURLResponse *)response allHeaderFields][@"Etag"];
NSLog(@"response:%@",response);
}
}] resume];
}
- (IBAction)reloadDataAction:(id)sender {
[self example_1];
}
运行 , 第一次加载 , 返回 200
, 点击 reload
再加载一次 . 打印如下:
lastModified
或者 etag
的用法.
3. SDWebImage 中 NSURLCache 与自身缓存机制处理
直接打开 SDWebImage
源码 . 来到 SDWebImageDownloader.m
的 如下方法中.
- (nullable SDWebImageDownloadToken *)downloadImageWithURL:(nullable NSURL *)url
options:(SDWebImageDownloaderOptions)options
progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock {
__weak SDWebImageDownloader *wself = self;
return [self addProgressCallback:progressBlock completedBlock:completedBlock forURL:url createCallback:^SDWebImageDownloaderOperation *{
__strong __typeof (wself) sself = wself;
NSTimeInterval timeoutInterval = sself.downloadTimeout;
if (timeoutInterval == 0.0) {
timeoutInterval = 15.0;
}
// In order to prevent from potential duplicate caching (NSURLCache + SDImageCache) we disable the cache for image requests if told otherwise
NSURLRequestCachePolicy cachePolicy = options & SDWebImageDownloaderUseNSURLCache ? NSURLRequestUseProtocolCachePolicy : NSURLRequestReloadIgnoringLocalCacheData;
NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url
cachePolicy:cachePolicy
timeoutInterval:timeoutInterval];
/*以下省略...*/
}
里面有这么一段值得注意的 :
NSURLRequestCachePolicy cachePolicy = options & SDWebImageDownloaderUseNSURLCache ? NSURLRequestUseProtocolCachePolicy : NSURLRequestReloadIgnoringLocalCacheData;
也就是说 , 除非用户指定了 options
是 UseNSURLCache
, 否则 SD
会默认忽略掉 NSURLCache
.
这么做的目的 上面也写了注释 , 防止多次缓存 , 避免 SD
自己实现的缓存和 NSURLCache
多次缓存造成资源浪费 .
同样的 , 在 SDWebImageDownloaderOperation.m
里, NSURLSessionDataDelegate
的代理方法 willCacheResponse
中 也可以看出来:
- (void)URLSession:(NSURLSession *)session
dataTask:(NSURLSessionDataTask *)dataTask
willCacheResponse:(NSCachedURLResponse *)proposedResponse
completionHandler:(void (^)(NSCachedURLResponse *cachedResponse))completionHandler {
NSCachedURLResponse *cachedResponse = proposedResponse;
if (self.request.cachePolicy == NSURLRequestReloadIgnoringLocalCacheData) {
// Prevents caching of responses
cachedResponse = nil;
}
if (completionHandler) {
completionHandler(cachedResponse);
}
}
当设置 NSURLRequestReloadIgnoringLocalCacheData
策略 , 会忽略 NSURLCache
的缓存 .
3.1 SDWebImage中下载选项
typedef NS_OPTIONS(NSUInteger, SDWebImageDownloaderOptions) {
SDWebImageDownloaderLowPriority = 1 << 0,
SDWebImageDownloaderProgressiveDownload = 1 << 1,
SDWebImageDownloaderUseNSURLCache = 1 << 2,
SDWebImageDownloaderIgnoreCachedResponse = 1 << 3,
SDWebImageDownloaderContinueInBackground = 1 << 4,
SDWebImageDownloaderHandleCookies = 1 << 5,
SDWebImageDownloaderAllowInvalidSSLCertificates = 1 << 6,
SDWebImageDownloaderHighPriority = 1 << 7,
SDWebImageDownloaderScaleDownLargeImages = 1 << 8,
};
那我们同样来搜一下 SDWebImageDownloaderIgnoreCachedResponse
这个 options
时 , SD
做了哪些处理.
- 第一个搜索结果 :
SDWebImageDownloaderOperation.m
的start
方法中有这么一段:
if (self.options & SDWebImageDownloaderIgnoreCachedResponse) {
// Grab the cached data for later check
NSCachedURLResponse *cachedResponse = [[NSURLCache sharedURLCache] cachedResponseForRequest:self.request];
if (cachedResponse) {
self.cachedData = cachedResponse.data;
}
}
加载 NSURLCache
的磁盘缓存数据 , 以便下面做判断用.
- 第二个搜索结果 :
SDWebImageDownloaderOperation.m
的didCompleteWithError
代理方法中 有一段:
if (self.options & SDWebImageDownloaderIgnoreCachedResponse && [self.cachedData isEqualToData:imageData]) {
// call completion block with nil
[self callCompletionBlocksWithImage:nil imageData:nil error:nil finished:YES];
}
这里就直接返回了 nil
, 也就是说 SDWebImageDownloaderIgnoreCachedResponse
这个 options
的机制就是当 image
是从 NSURLCache
获取到的时候 , 它会返回 nil
.
以上就是关于 NSURLCache
以及 SDWebImage
和 HTTP
的缓存策略分析. 如有错误 , 欢迎指正 .