SDWebImage:缓存不更新问题

背景介绍

现在的App是用weex开发的,<image>组件只要提供src属性(url字符长),剩下的由Native组件实现图片的下载和显示。
最近出现了一个问题:后台换了某张图片的内容,但是手机上的图片没有同步更新,还是老的。
weex没有提供图片下载的实现,只是通过demo的方式推荐使用SDWebImage,我们当然是依样画葫芦用SDWebImage来做了。
上面的问题,原因是虽然后台图片内容换了,但是url还是老的,手机就用了缓存,没有从后台更新图片。
想进一步搞清楚为什么使用缓存,而不更新,那么就需要学习一下SDWebImage的具体实现了。

这里介绍的是工程中用的SDWebImage相关内容,跟目前最新的版本可能存在差异。

实现下载

基本上是按照demo来做的:

- (id<WXImageOperationProtocol>)downloadImageWithURL:(NSString *)url imageFrame:(CGRect)imageFrame userInfo:(NSDictionary *)userInfo completed:(void(^)(UIImage *image,  NSError *error, BOOL finished))completedBlock {
    return (id<WXImageOperationProtocol>)[[SDWebImageManager sharedManager] downloadImageWithURL:[NSURL URLWithString:url] options:SDWebImageRetryFailed progress:nil completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
        if (completedBlock) {
            completedBlock(image, error, finished);
        }
    }];
}

调用的接口

工程中的SDWebImage是以源码的方式直接加入的,没有用CocoaPod之类的包管理工具。这里用的也是最基础的功能,接口也不会大变,先把调用的接口类型搞清楚。

函数API

- (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url
                                         options:(SDWebImageOptions)options
                                        progress:(SDWebImageDownloaderProgressBlock)progressBlock
                                       completed:(SDWebImageCompletionWithFinishedBlock)completedBlock;

选项参数

typedef NS_OPTIONS(NSUInteger, SDWebImageOptions) {
    /**
     * By default, when a URL fail to be downloaded, the URL is blacklisted so the library won't keep trying.
     * This flag disable this blacklisting.
     */
    SDWebImageRetryFailed = 1 << 0,

    /**
     * By default, image downloads are started during UI interactions, this flags disable this feature,
     * leading to delayed download on UIScrollView deceleration for instance.
     */
    SDWebImageLowPriority = 1 << 1,

    /**
     * This flag disables on-disk caching
     */
    SDWebImageCacheMemoryOnly = 1 << 2,

    /**
     * This flag enables progressive download, the image is displayed progressively during download as a browser would do.
     * By default, the image is only displayed once completely downloaded.
     */
    SDWebImageProgressiveDownload = 1 << 3,

    /**
     * Even if the image is cached, respect the HTTP response cache control, and refresh the image from remote location if needed.
     * The disk caching will be handled by NSURLCache instead of SDWebImage leading to slight performance degradation.
     * This option helps deal with images changing behind the same request URL, e.g. Facebook graph api profile pics.
     * If a cached image is refreshed, the completion block is called once with the cached image and again with the final image.
     *
     * Use this flag only if you can't make your URLs static with embeded cache busting parameter.
     */
    SDWebImageRefreshCached = 1 << 4,

    /**
     * In iOS 4+, continue the download of the image if the app goes to background. This is achieved by asking the system for
     * extra time in background to let the request finish. If the background task expires the operation will be cancelled.
     */
    SDWebImageContinueInBackground = 1 << 5,

    /**
     * Handles cookies stored in NSHTTPCookieStore by setting
     * NSMutableURLRequest.HTTPShouldHandleCookies = YES;
     */
    SDWebImageHandleCookies = 1 << 6,

    /**
     * Enable to allow untrusted SSL ceriticates.
     * Useful for testing purposes. Use with caution in production.
     */
    SDWebImageAllowInvalidSSLCertificates = 1 << 7,

    /**
     * By default, image are loaded in the order they were queued. This flag move them to
     * the front of the queue and is loaded immediately instead of waiting for the current queue to be loaded (which 
     * could take a while).
     */
    SDWebImageHighPriority = 1 << 8,

    /**
     * By default, placeholder images are loaded while the image is loading. This flag will delay the loading
     * of the placeholder image until after the image has finished loading.
     */
    SDWebImageDelayPlaceholder = 1 << 9,

    /**
     * We usually don't call transformDownloadedImage delegate method on animated images,
     * as most transformation code would mangle it.
     * Use this flag to transform them anyway.
     */
    SDWebImageTransformAnimatedImage = 1 << 10,
};

  • SDWebImageRetryFailed是现在的参数,表示就算下载失败也会再次尝试(不把下载失败的的url加入黑名单)
  • SDWebImageCacheMemoryOnly这个参数对解决这个问题有帮助,只用内存缓存,不用磁盘缓存,App关了再开,肯定会重新下载,不会出现服务器和手机缓存图片不一致的情况。
  • SDWebImageRefreshCached,这个参数就是为了解决url没变但是服务器图片改变的问题,很适合当前的场景。方案就是磁盘缓存不自己实现了,直接使用NSURLCache。记得AFNetworking的大神Matt就曾经嘲笑过SDWebImage的缓存是多此一举,还不如系统的NSURLCache好用。

进度参数

typedef void(^SDWebImageDownloaderProgressBlock)(NSInteger receivedSize, NSInteger expectedSize);

完成函数

typedef void(^SDWebImageCompletionWithFinishedBlock)(UIImage *image, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL);

这里cacheType参数指明图片的来源:网络、内存缓存、磁盘缓存

typedef NS_ENUM(NSInteger, SDImageCacheType) {
    /**
     * The image wasn't available the SDWebImage caches, but was downloaded from the web.
     */
    SDImageCacheTypeNone,
    /**
     * The image was obtained from the disk cache.
     */
    SDImageCacheTypeDisk,
    /**
     * The image was obtained from the memory cache.
     */
    SDImageCacheTypeMemory
};

过程简介

整个过程,包括查询缓存,下载图片,下载后更新缓存等,都包含在下面这个函数中:

- (NSOperation *)queryDiskCacheForKey:(NSString *)key done:(SDWebImageQueryCompletedBlock)doneBlock {
    if (!doneBlock) {
        return nil;
    }

    if (!key) {
        doneBlock(nil, SDImageCacheTypeNone);
        return nil;
    }

    // First check the in-memory cache...
    UIImage *image = [self imageFromMemoryCacheForKey:key];
    if (image) {
        doneBlock(image, SDImageCacheTypeMemory);
        return nil;
    }

    NSOperation *operation = [NSOperation new];
    dispatch_async(self.ioQueue, ^{
        if (operation.isCancelled) {
            return;
        }

        @autoreleasepool {
            UIImage *diskImage = [self diskImageForKey:key];
            if (diskImage) {
                CGFloat cost = diskImage.size.height * diskImage.size.width * diskImage.scale;
                [self.memCache setObject:diskImage forKey:key cost:cost];
            }

            dispatch_async(dispatch_get_main_queue(), ^{
                doneBlock(diskImage, SDImageCacheTypeDisk);
            });
        }
    });

    return operation;
}

  • 先查内存缓存,程序注释里也有
  • 内存缓存没有,再查磁盘缓存,磁盘缓存比较耗时,放在一个单独的队列中,self.ioQueue,还用了单独的@autoreleasepool {}
  • 这个队列是串行队列,看他的定义就可以了
// Create IO serial queue
_ioQueue = dispatch_queue_create("com.hackemist.SDWebImageCache", DISPATCH_QUEUE_SERIAL);

  • 缓存是以key-value的字典形式的保存的,key是图片的url
- (NSString *)cacheKeyForURL:(NSURL *)url {
    if (self.cacheKeyFilter) {
        return self.cacheKeyFilter(url);
    }
    else {
        return [url absoluteString];
    }
}

用户输入url字符串,组装成NSURL进行图片下载,有抽取出url字符串作为缓存图片的的key

  • 下载和存缓存的过程都在这个函数的doneBlock参数中,他的类型定义:
typedef void(^SDWebImageQueryCompletedBlock)(UIImage *image, SDImageCacheType cacheType);

关于SDWebImageCacheMemoryOnly参数

如何实现只用内存缓存,而不用硬盘缓存的呢?相关的代码有如下几处。
第1处:将这个标志转换为是否保存磁盘缓存的标志

BOOL cacheOnDisk = !(options & SDWebImageCacheMemoryOnly);

第2处:下载完成后,调用存缓存函数

if (downloadedImage && finished) {
    [self.imageCache storeImage:downloadedImage recalculateFromImage:NO imageData:data forKey:key toDisk:cacheOnDisk];
}

第3处:根据标志,决定是否存磁盘缓存

- (void)storeImage:(UIImage *)image recalculateFromImage:(BOOL)recalculate imageData:(NSData *)imageData forKey:(NSString *)key toDisk:(BOOL)toDisk {
    if (!image || !key) {
        return;
    }

    [self.memCache setObject:image forKey:key cost:image.size.height * image.size.width * image.scale];

    if (toDisk) {
        dispatch_async(self.ioQueue, ^{
            // 存磁盘缓存相关代码
        });
    }
}

所以,SDWebImageCacheMemoryOnly这个标志决定了是否保存磁盘缓存。至于查询缓存这块逻辑,不受影响:先查内存缓存,再查磁盘缓存(只是没有而已),然后再下载保存缓存。

关于SDWebImageRefreshCached参数

从这个参数的解释来看,如果设置了这个参数,那么服务端改了之后,客户端会同步更新,能够解决我们开头提出的问题。是真的吗?相关的代码有如下几处。
第1处:

if (image && options & SDWebImageRefreshCached) {
    dispatch_main_sync_safe(^{
        // If image was found in the cache bug SDWebImageRefreshCached is provided, notify about the cached image
        // AND try to re-download it in order to let a chance to NSURLCache to refresh it from server.
        completedBlock(image, nil, cacheType, YES, url);
    });
}

就像他注释中写的,如果在缓存中找到了图片,先用起来再说,然后让NSURLCache从服务器下载更新。

第2处:转化为下载参数

SDWebImageDownloaderOptions downloaderOptions = 0;
if (options & SDWebImageRefreshCached) downloaderOptions |= SDWebImageDownloaderUseNSURLCache;

typedef NS_OPTIONS(NSUInteger, SDWebImageDownloaderOptions) {
    SDWebImageDownloaderLowPriority = 1 << 0,
    SDWebImageDownloaderProgressiveDownload = 1 << 1,

    /**
     * By default, request prevent the of NSURLCache. With this flag, NSURLCache
     * is used with default policies.
     */
    SDWebImageDownloaderUseNSURLCache = 1 << 2,

    /**
     * Call completion block with nil image/imageData if the image was read from NSURLCache
     * (to be combined with `SDWebImageDownloaderUseNSURLCache`).
     */

    SDWebImageDownloaderIgnoreCachedResponse = 1 << 3,
    /**
     * In iOS 4+, continue the download of the image if the app goes to background. This is achieved by asking the system for
     * extra time in background to let the request finish. If the background task expires the operation will be cancelled.
     */

    SDWebImageDownloaderContinueInBackground = 1 << 4,

    /**
     * Handles cookies stored in NSHTTPCookieStore by setting 
     * NSMutableURLRequest.HTTPShouldHandleCookies = YES;
     */
    SDWebImageDownloaderHandleCookies = 1 << 5,

    /**
     * Enable to allow untrusted SSL ceriticates.
     * Useful for testing purposes. Use with caution in production.
     */
    SDWebImageDownloaderAllowInvalidSSLCertificates = 1 << 6,

    /**
     * Put the image in the high priority queue.
     */
    SDWebImageDownloaderHighPriority = 1 << 7,
};

第3处:在生成NSMutableURLRequest的时候设置缓存策略

// In order to prevent from potential duplicate caching (NSURLCache + SDImageCache) we disable the cache for image requests if told otherwise
NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url 
                                                            cachePolicy:(options & SDWebImageDownloaderUseNSURLCache ? NSURLRequestUseProtocolCachePolicy : NSURLRequestReloadIgnoringLocalCacheData) 
                                                        timeoutInterval:timeoutInterval];

如果设置了这个标志,那么用默认的协议(Http)缓存策略NSURLRequestUseProtocolCachePolicy
如果没有设置这个表示标志,那么不用NSURLCache的缓存NSURLRequestReloadIgnoringLocalCacheData。也就是用SDWebImage自己写的内存缓存和磁盘缓存。

Http协议的默认缓存

在第一次请求到服务器资源的时候,服务器需要使用Cache-Control这个响应头来指定缓存策略,它的格式如下:Cache-Control:max-age=xxxx,这个头指指明缓存过期的时间。

  • 默认情况下,Cache-Control:max-age=5,也就是说缓存的有效时间是5秒。所以作者说换成这个,服务器改了,客户端会自动更新。5秒的缓存时间,跟没缓存也差不多了。
  • 为了这个特性起效果,有的建议将服务器设置为不缓存。也就是Cache-Control:max-age=no-cache或者Cache-Control:max-age=no-store

SDWebImageRefreshCached参数设置之后,会怎么样?

  • 不使用SDWebImage提供的内存缓存和硬盘缓存
  • 采用NSURLCache提供的缓存,有效时间只有5秒
  • 图片不一致的问题是解决了,不过效果跟不使用缓存差别不大
  • 个人建议这个参数还是不要用为好,为了一个小特性,丢掉了SDWebImage最核心的特色。

解决方案

方案1

后台给的url中增加字段,表示图片是否更新,比如增加一个timestamp字段.图片更新了,就更新下这个字段;
对客户端来说,只要这个timestamp字段变了,整个url就不一样了,就会从网络取图片。比如http://xxx/xx? timestamp=xxx
也可以添加图片文件的md5来表示文件是否更新,比如http://xxx/xx? md5=xxx。并且md5比时间戳要好,这是强校验。时间戳在服务器回滚或者服务器重启的时候会有特殊的逻辑。不过大多数时候时间戳也够用了。
====这个方案客户端不用改,后台改动也不会太大。====强烈推荐

方案2

客户端修改缓存策略,只用内存缓存,不用磁盘缓存。就是设置SDWebImageCacheMemoryOnly参数。
这个方案的好处是服务端不用改,客户端改动很少。
但是问题是程序关闭又打开之后,缓存就没了,需要访问网络,重新加载图片,缓存性能下降很多

方案3

客户端修改缓存时间。目前的缓存有效时间为7天,有点长;可以修改为一个经验值,比如1天?1小时?
这个方案的好处是服务端不用改,客户端也改动很少,缓存性能下降程度比方案二要小一点;
缺点是:在缓存时间内,不一致的问题还是存在的,问题只是减轻,并没有消除

方案4

客户端不用现在的第三方库(SDWebImage),(设置SDWebImageCacheMemoryOnly参数方案不推荐),采用系统API实现(NSURLCache)。服务端利用Http的头部字段进行缓存控制。
Cache-Control:可以设定缓存有效时间,默认是5s,具体时间由服务端设置。设置一个经验值,1天?1小时?
Last-Modified/If-Modified-Since:时间戳。有更新服务端就返回200,客户端下载,更新图片;没更新,服务端就返回304,客户端使用本地缓存。
Etag/If-None-Match:标签,一般用MD5值。有更新服务端就返回200,客户端下载,更新图片;没更新,服务端就返回304,客户端使用本地缓存。
这个方案的优点是:服务端控制缓存,并且既有全局控制(缓存有效时间),又有特定的控制(时间戳或者MD5标签)
缺点:客户端不能利用成熟的第三方库,需要自己实现图片缓存,非主流用法。服务端改动也非常大。====不推荐

备注:

选方案1的应该普遍一点,比较简单;
选方案4也是可以的,不过要求服务端客户端配合开发,并且也没有必要用SDWebImage,直接用系统API来做就是了。

参考文章

NSURLCache详解和使用

iOS网络缓存扫盲篇

SDWebImage

作者:老章888
链接:https://www.jianshu.com/p/145ffa299eff

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,444评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,421评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,036评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,363评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,460评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,502评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,511评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,280评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,736评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,014评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,190评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,848评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,531评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,159评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,411评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,067评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,078评论 2 352

推荐阅读更多精彩内容