iOS NSCache & NSURLCache 机制原理探究 (二)

2,938 阅读8分钟

上篇文章 我们已经针对 NSCache 的底层了解了其具体的实现机制和淘汰策略 . 由于文章太长 , 不利于阅读 . 那么这篇文章 , 我们就 NSURLCache 以及 SDWebImage 中的缓存处理机制进行探究讲解.

目录我就继续上篇文章的来了 , 以便比较阅读.

2. NSURLCache

2.1 介绍

先把官方文档奉上 NSURLCache

首先我们都知道 , 使用 NSURLCache 进行请求数据的缓存时 , 同时本身默认也会有缓存的处理. 那么我们需要做什么 ? 原生默认做了什么 ?

The NSURLCache class implements the caching of responses to URL load requests by mapping NSURLRequest objects to NSCachedURLResponse 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 中,控制缓存开关的字段有两个:PragmaCache-Control . Pragma 是旧产物,已经逐步抛弃,有些网站为了向下兼容还保留了这两个字段. 在此就不介绍了.

Cache-Control

在请求中使用 Cache-Control 时,它可选的值有:

在响应中使用Cache-Control 时,它可选的值有:

缓存校验

在缓存中,我们需要一个机制来验证缓存是否有效。比如服务器的资源更新了,客户端需要及时刷新缓存;又或者客户端的资源过了有效期,但服务器上的资源还是旧的,此时并不需要重新发送。缓存校验就是用来解决这些问题的,

HTTP 1.1 中,我们主要关注下 Last-Modifiedetag 这两个字段。

Last-Modified

服务端在返回资源时,会将该资源的最后更改时间通过 Last-Modified 字段返回给客户端。客户端下次请求时通过 If-Modified-Since 或者 If-Unmodified-Since 带上 Last-Modified ,服务端检查该时间是否与服务器的最后修改时间一致:

  • 如果一致,则返回 304 状态码,不返回资源;
  • 如果不一致则返回 200 和修改后的资源,并带上新的时间。

If-Modified-SinceIf-Unmodified-Since 的区别是:

If-Modified-Since:告诉服务器如果时间一致,返回状态码 304

If-Unmodified-Since:告诉服务器如果时间不一致,返回状态码 412

Etag

单纯的以修改时间来判断还是有缺陷,比如文件的最后修改时间变了,但内容没变。对于这样的情况,我们可以使用 Etag 来处理。

Etag 机制:

服务器通过某个算法对资源进行计算,取得一串值(类似于文件的 hash 值),之后将该值通过 Etag 返回给客户端,客户端下次请求时通过 If-None-MatchIf-Match 带上该值,服务器对该值进行对比校验:如果一致则不要返回资源。

If-None-MatchIf-Match 的区别是:

  • If-None-Match:告诉服务器如果一致,返回状态码 304,不一致则返回资源
  • If-Match:告诉服务器如果不一致,返回状态码 412

既生 Last-Modified 何生 Etag ?

你可能会觉得使用 Last-Modified 已经足以让客户端知道本地的缓存副本是否足够新,为什么还需要 Etag(实体标识)呢?HTTP 1.1Etag 的出现主要是为了解决几个 Last-Modified 比较难解决的问题:

  1. Last-Modified 标注的最后修改只能精确到秒级,如果某些文件在 1 秒钟以内,被修改多次的话,它将不能准确标注文件的修改时间

  2. 如果某些文件会被定期生成,当有时内容并没有任何变化,但 Last-Modified 却改变了,导致文件没法使用缓存

  3. 有可能存在服务器没有准确获取文件修改时间,或者与代理服务器时间不一致等情形

Etag 是服务器自动生成或者由开发者生成的对应资源在服务器端的唯一标识符,能够更加准确的控制缓存。Last-ModifiedETag 是可以一起使用的,服务器会优先验证 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;

也就是说 , 除非用户指定了 optionsUseNSURLCache, 否则 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.mstart 方法中有这么一段:
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.mdidCompleteWithError 代理方法中 有一段:
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 以及 SDWebImageHTTP 的缓存策略分析. 如有错误 , 欢迎指正 .