SDWebImage:緩存不更新問題

背景介紹

現(xiàn)在的App是用weex開發(fā)的,<image>組件只要提供src屬性(url字符長(zhǎng)),剩下的由Native組件實(shí)現(xiàn)圖片的下載和顯示。
最近出現(xiàn)了一個(gè)問題:后臺(tái)換了某張圖片的內(nèi)容,但是手機(jī)上的圖片沒有同步更新,還是老的。
weex沒有提供圖片下載的實(shí)現(xiàn),只是通過demo的方式推薦使用SDWebImage,我們當(dāng)然是依樣畫葫蘆用SDWebImage來做了。
上面的問題,原因是雖然后臺(tái)圖片內(nèi)容換了,但是url還是老的,手機(jī)就用了緩存,沒有從后臺(tái)更新圖片。
想進(jìn)一步搞清楚為什么使用緩存,而不更新,那么就需要學(xué)習(xí)一下SDWebImage的具體實(shí)現(xiàn)了。

這里介紹的是工程中用的SDWebImage相關(guān)內(nèi)容,跟目前最新的版本可能存在差異。

實(shí)現(xiàn)下載

基本上是按照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);
        }
    }];
}

調(diào)用的接口

工程中的SDWebImage是以源碼的方式直接加入的,沒有用CocoaPod之類的包管理工具。這里用的也是最基礎(chǔ)的功能,接口也不會(huì)大變,先把調(diào)用的接口類型搞清楚。

函數(shù)API

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

選項(xiàng)參數(shù)

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是現(xiàn)在的參數(shù),表示就算下載失敗也會(huì)再次嘗試(不把下載失敗的的url加入黑名單)
  • SDWebImageCacheMemoryOnly這個(gè)參數(shù)對(duì)解決這個(gè)問題有幫助,只用內(nèi)存緩存,不用磁盤緩存,App關(guān)了再開,肯定會(huì)重新下載,不會(huì)出現(xiàn)服務(wù)器和手機(jī)緩存圖片不一致的情況。
  • SDWebImageRefreshCached,這個(gè)參數(shù)就是為了解決url沒變但是服務(wù)器圖片改變的問題,很適合當(dāng)前的場(chǎng)景。方案就是磁盤緩存不自己實(shí)現(xiàn)了,直接使用NSURLCache。記得AFNetworking的大神Matt就曾經(jīng)嘲笑過SDWebImage的緩存是多此一舉,還不如系統(tǒng)的NSURLCache好用。

進(jìn)度參數(shù)

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

完成函數(shù)

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

這里cacheType參數(shù)指明圖片的來源:網(wǎng)絡(luò)、內(nèi)存緩存、磁盤緩存

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
};

過程簡(jiǎn)介

整個(gè)過程,包括查詢緩存,下載圖片,下載后更新緩存等,都包含在下面這個(gè)函數(shù)中:

- (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;
}
  • 先查內(nèi)存緩存,程序注釋里也有
  • 內(nèi)存緩存沒有,再查磁盤緩存,磁盤緩存比較耗時(shí),放在一個(gè)單獨(dú)的隊(duì)列中,self.ioQueue,還用了單獨(dú)的@autoreleasepool {}
  • 這個(gè)隊(duì)列是串行隊(duì)列,看他的定義就可以了
// 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進(jìn)行圖片下載,有抽取出url字符串作為緩存圖片的的key

  • 下載和存緩存的過程都在這個(gè)函數(shù)的doneBlock參數(shù)中,他的類型定義:
typedef void(^SDWebImageQueryCompletedBlock)(UIImage *image, SDImageCacheType cacheType);

關(guān)于SDWebImageCacheMemoryOnly參數(shù)

如何實(shí)現(xiàn)只用內(nèi)存緩存,而不用硬盤緩存的呢?相關(guān)的代碼有如下幾處。
第1處:將這個(gè)標(biāo)志轉(zhuǎn)換為是否保存磁盤緩存的標(biāo)志

BOOL cacheOnDisk = !(options & SDWebImageCacheMemoryOnly);

第2處:下載完成后,調(diào)用存緩存函數(shù)

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

第3處:根據(jù)標(biāo)志,決定是否存磁盤緩存

- (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, ^{
            // 存磁盤緩存相關(guān)代碼
        });
    }
}

所以,SDWebImageCacheMemoryOnly這個(gè)標(biāo)志決定了是否保存磁盤緩存。至于查詢緩存這塊邏輯,不受影響:先查內(nèi)存緩存,再查磁盤緩存(只是沒有而已),然后再下載保存緩存。

關(guān)于SDWebImageRefreshCached參數(shù)

從這個(gè)參數(shù)的解釋來看,如果設(shè)置了這個(gè)參數(shù),那么服務(wù)端改了之后,客戶端會(huì)同步更新,能夠解決我們開頭提出的問題。是真的嗎?相關(guān)的代碼有如下幾處。
第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從服務(wù)器下載更新。

第2處:轉(zhuǎn)化為下載參數(shù)

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的時(shí)候設(shè)置緩存策略

// 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];

如果設(shè)置了這個(gè)標(biāo)志,那么用默認(rèn)的協(xié)議(Http)緩存策略NSURLRequestUseProtocolCachePolicy
如果沒有設(shè)置這個(gè)表示標(biāo)志,那么不用NSURLCache的緩存NSURLRequestReloadIgnoringLocalCacheData。也就是用SDWebImage自己寫的內(nèi)存緩存和磁盤緩存。

Http協(xié)議的默認(rèn)緩存

在第一次請(qǐng)求到服務(wù)器資源的時(shí)候,服務(wù)器需要使用Cache-Control這個(gè)響應(yīng)頭來指定緩存策略,它的格式如下:Cache-Control:max-age=xxxx,這個(gè)頭指指明緩存過期的時(shí)間。

  • 默認(rèn)情況下,Cache-Control:max-age=5,也就是說緩存的有效時(shí)間是5秒。所以作者說換成這個(gè),服務(wù)器改了,客戶端會(huì)自動(dòng)更新。5秒的緩存時(shí)間,跟沒緩存也差不多了。
  • 為了這個(gè)特性起效果,有的建議將服務(wù)器設(shè)置為不緩存。也就是Cache-Control:max-age=no-cache或者Cache-Control:max-age=no-store

SDWebImageRefreshCached參數(shù)設(shè)置之后,會(huì)怎么樣?

  • 不使用SDWebImage提供的內(nèi)存緩存和硬盤緩存
  • 采用NSURLCache提供的緩存,有效時(shí)間只有5秒
  • 圖片不一致的問題是解決了,不過效果跟不使用緩存差別不大
  • 個(gè)人建議這個(gè)參數(shù)還是不要用為好,為了一個(gè)小特性,丟掉了SDWebImage最核心的特色。

解決方案

方案1

后臺(tái)給的url中增加字段,表示圖片是否更新,比如增加一個(gè)timestamp字段.圖片更新了,就更新下這個(gè)字段;
對(duì)客戶端來說,只要這個(gè)timestamp字段變了,整個(gè)url就不一樣了,就會(huì)從網(wǎng)絡(luò)取圖片。比如http://xxx/xx? timestamp=xxx
也可以添加圖片文件的md5來表示文件是否更新,比如http://xxx/xx? md5=xxx。并且md5比時(shí)間戳要好,這是強(qiáng)校驗(yàn)。時(shí)間戳在服務(wù)器回滾或者服務(wù)器重啟的時(shí)候會(huì)有特殊的邏輯。不過大多數(shù)時(shí)候時(shí)間戳也夠用了。
====這個(gè)方案客戶端不用改,后臺(tái)改動(dòng)也不會(huì)太大。====強(qiáng)烈推薦

方案2

客戶端修改緩存策略,只用內(nèi)存緩存,不用磁盤緩存。就是設(shè)置SDWebImageCacheMemoryOnly參數(shù)。
這個(gè)方案的好處是服務(wù)端不用改,客戶端改動(dòng)很少。
但是問題是程序關(guān)閉又打開之后,緩存就沒了,需要訪問網(wǎng)絡(luò),重新加載圖片,緩存性能下降很多

方案3

客戶端修改緩存時(shí)間。目前的緩存有效時(shí)間為7天,有點(diǎn)長(zhǎng);可以修改為一個(gè)經(jīng)驗(yàn)值,比如1天?1小時(shí)?
這個(gè)方案的好處是服務(wù)端不用改,客戶端也改動(dòng)很少,緩存性能下降程度比方案二要小一點(diǎn);
缺點(diǎn)是:在緩存時(shí)間內(nèi),不一致的問題還是存在的,問題只是減輕,并沒有消除

方案4

客戶端不用現(xiàn)在的第三方庫(SDWebImage),(設(shè)置SDWebImageCacheMemoryOnly參數(shù)方案不推薦),采用系統(tǒng)API實(shí)現(xiàn)(NSURLCache)。服務(wù)端利用Http的頭部字段進(jìn)行緩存控制。
Cache-Control:可以設(shè)定緩存有效時(shí)間,默認(rèn)是5s,具體時(shí)間由服務(wù)端設(shè)置。設(shè)置一個(gè)經(jīng)驗(yàn)值,1天?1小時(shí)?
Last-Modified/If-Modified-Since:時(shí)間戳。有更新服務(wù)端就返回200,客戶端下載,更新圖片;沒更新,服務(wù)端就返回304,客戶端使用本地緩存。
Etag/If-None-Match:標(biāo)簽,一般用MD5值。有更新服務(wù)端就返回200,客戶端下載,更新圖片;沒更新,服務(wù)端就返回304,客戶端使用本地緩存。
這個(gè)方案的優(yōu)點(diǎn)是:服務(wù)端控制緩存,并且既有全局控制(緩存有效時(shí)間),又有特定的控制(時(shí)間戳或者M(jìn)D5標(biāo)簽)
缺點(diǎn):客戶端不能利用成熟的第三方庫,需要自己實(shí)現(xiàn)圖片緩存,非主流用法。服務(wù)端改動(dòng)也非常大。====不推薦

備注:

選方案1的應(yīng)該普遍一點(diǎn),比較簡(jiǎn)單;
選方案4也是可以的,不過要求服務(wù)端客戶端配合開發(fā),并且也沒有必要用SDWebImage,直接用系統(tǒng)API來做就是了。

參考文章

NSURLCache詳解和使用

iOS網(wǎng)絡(luò)緩存掃盲篇

SDWebImage

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容