网络图片缓存刷新问题

为了提高客户端内容加载速度与效率,在加载网络图片时,会使用缓存,而SDWedImage可以说是iOS 网络图片加载的事实标准了。但它并没有一种较好的缓存策略,解决URL不变图片改变的情况。

问题

一般来说,网络图片改变,URL会跟着改变,所以绝大部分的使用场景里,使用SDWedImage的默认缓存策略是不会有问题的。但有时候后台换了图片就是不换名字,而且你会发现这些图片在浏览器(可能是wap端)是会跟着改变的。嗯,这时候如果管技术的不够6,后台就成功卸锅了。

解决思路

SDWebImage默认的缓存策略,是根据url这个key寻找本地是否存在缓存,存在就加载,不发任何请求。
当然,还是有可选的缓存策略里,比如SDWebImageRefreshCached这个选项,这个选项其实就是使用NSURLCache,然而这个自带的缓存策略最优选也不过是每次都下载图片回来刷新。

从网页端入手

从框架本身找不到思路,不妨看看浏览器是怎么实现这种资源刷新的,先抓包看看

第一次加载图片

a942be28b5f51299

第二次

8c693bd84067868e

资源改变

d778d7c380f1c3ff

可以看到,加载过的图片再去加载就会返回304,而304就是代表服务器判定客户端存在资源缓存,而且缓存在有效期内,让客户端加载缓存即可。

服务器如何判定304

主要是根据请求头种的 if-non-match 和 if-modified-since,当这两个字段有任何一个不匹配都需要重新下载资源,而浏览器中的这两个请求头字段是根据上次请求这个url时,服务器返回的响应头中的etag和last-modified生成的。

c3bab01c7513e438

d430c9e095b98d68

我这次使用的是nginx,默认返回的last-modified字段是文件的创建时间,对时间的判断是“是否相等”,而不是“大于小于”。暂时没有网上所说的有些公司为了压榨服务器性能,把etag验证关了的情况。也就是说完全可以根据请求头里的这两个字段作文章了。

代码实现

由上分析可知,需要缓存每次图片请求的响应头的etag和last-modified,那么就在sdwebimage的响应方法里面作缓存,我使用aspect这个框架去hook这个方法,代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
[SDWebImageDownloader aspect_hookSelector:@selector(URLSession:dataTask:didReceiveResponse:completionHandler:)
withOptions:AspectPositionBefore
usingBlock:^(id<AspectInfo> info, NSURLSession *session, NSURLSessionDataTask *dataTask, NSURLResponse *response, SessionDisposition completionHandler) {
NSLog(@"%@", response.URL);
YYCache *cache = [[YYCache alloc] initWithName:@"SDHEADER"];
// 转换NSURL为字符串,作为缓存的key
NSString *url = [response.URL absoluteString];
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse*)response;
if ([response respondsToSelector:@selector(allHeaderFields)]) {
NSDictionary *dictionary = [httpResponse allHeaderFields];
[cache setObject:dictionary forKey:url];
NSLog(@"%@", dictionary[@"Last-Modified"]);
}
}error:nil];

接着每次进行网络图片请求时,加上对应的请求头,这个sdwedimage有提供对应的接口方法,初衷可能是给一些图片下载有验证的服务器用的,这里用也正好合适,封装成一个方法,在app启动时调用即可,代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
- (void)setURLImage {
SDWebImageDownloader *imgDownloader = SDWebImageManager.sharedManager.imageDownloader;
imgDownloader.headersFilter = ^NSDictionary *(NSURL *url, NSDictionary *headers) {

NSString *imgKey = [SDWebImageManager.sharedManager cacheKeyForURL:url];
NSMutableDictionary *mutableHeaders = [headers mutableCopy];
YYCache *cache = [[YYCache alloc] initWithName:@"SDHEADER"];
NSDictionary *oldheaders = (NSDictionary *)[cache objectForKey:imgKey];
[mutableHeaders setValue:oldheaders[@"Last-Modified"] forKey:@"If-Modified-Since"];
[mutableHeaders setValue:oldheaders[@"Etag"] forKey:@"If-None-Match"];

return mutableHeaders;
};

至此,给sdwedimage实现了一种与浏览器一致的缓存策略。

补充一点,为什么 NSURLCache 做不到这一点,原因在于,它没有缓存 last-modified,而是缓存上一次请求时间,但服务器判断的是与文件的创建的时间是否相等,所以造成每次都不相等,需要重新下载的情况。

除了上述解决方法,还可以使用 NSURLProtocol 添加监听拦截来作文章,不过这个方法对项目影响太大,个人不建议使用。