天天都在用的 SDWebImage, 你瞭解它的緩存策略嗎?

SDWebImage 相信對大多數開發者來說,都是一個不陌生的名字。它除了幫助我們讀取網絡圖片,還會處理這些圖片的緩存。它的緩存機制到底是什麼樣的呢,讓我給跟大家嘮叨嘮叨,希望你能有收穫。

基本結構

閒言少敘,咱們這就開始。 首先咱們來看看 SDWebImage 的整體結構:

有一個專門的 Cache 分類用來處理圖片的緩存。 這裏面也有兩個類 SDImageCache 和 SDImageCacheConfig。 大部分的緩存處理都在 SDImageCache 這個類中實現。其他幾個文件夾咱們分別有個字的功能,因爲咱們這次專門討論緩存策略,所以其他內容暫時略過。

Memory 和 Disk 雙緩存

首先,SDWebImage 的圖片緩存採用的是 Memory 和 Disk 雙重 Cache 機制, 聽起來挺高大上吧。其實也不復雜。

我們先來看看 Memory Cache,貼一段 SDImageCache 的代碼:

@interface SDImageCache ()

#pragma mark - Properties
@property (strong, nonatomic, nonnull) NSCache *memCache;

...

這裏我們發現, 有一個叫做 memCache 的屬性,它是一個 NSCache 對象,用於實現我們對圖片的 Memory Cache。 SDWebImage 還專門實現了一個叫做 AutoPurgeCache 的類, 相比於普通的 NSCache, 它提供了一個在內存緊張時候釋放緩存的能力:

@interface AutoPurgeCache : NSCache
@end

@implementation AutoPurgeCache

- (nonnull instancetype)init {
    self = [super init];
    if (self) {
#if SD_UIKIT
        [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(removeAllObjects) name:UIApplicationDidReceiveMemoryWarningNotification object:nil];
#endif
    }
    return self;
}

其實就是接受系統的內存警告通知,然後清除掉自身的圖片緩存。 這裏大家比較少見的一個類應該是 NSCache 了。 簡單來說,它是一個類似於 NSDictionary 的集合類,用於在內存中存儲我們要緩存的數據。詳細信息大家可以參考官方文檔:https://developer.apple.com/reference/foundation/nscache

說完 Memory Cache, 我們再來說說 Disk Cache,也就是文件緩存。

SDWebImage 會將圖片存放到 NSCachesDirectory 目錄中:

- (nullable NSString *)makeDiskCachePath:(nonnull NSString*)fullNamespace {
    NSArray<NSString *> *paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);
    return [paths[0] stringByAppendingPathComponent:fullNamespace];
}

然後爲每一個緩存文件生成一個 md5 文件名, 存放到文件中。

整體機制

爲了節約篇幅,提升大家的閱讀體驗,這裏儘量少貼大段代碼。 我們前面介紹了 SDWebImage 同時使用內存和硬盤兩種緩存。 那麼我們來看看當使用 SDWebImage 讀取圖片時候的完整流程。 我們一般會使用 SDWebImage 對 UIKit 的擴展,直接加載圖片:

[imageView sd_setImageWithURL:[NSURL URLWithString:@"http://swiftcafe.io/images/qrcode.jpg"]];

首先這個 Category 方法 sd_setImageWithURL 內部會調用 SDWebImageManager 的 downloadImageWithURL 方法來處理這個圖片 URL:

id <SDWebImageOperation> operation = [SDWebImageManager.sharedManager downloadImageWithURL:url options:options progress:progressBlock completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
            ...
}];

SDWebImageManager 內部的 downloadImageWithURL 方法會先使用我們前面提到的 SDImageCache 類的 queryDiskCacheForKey 方法,查詢圖片緩存:

operation.cacheOperation = [self.imageCache queryDiskCacheForKey:key done:^(UIImage *image, SDImageCacheType cacheType) {

    ...
}];

再來看 queryDiskCacheForKey 方法內部, 先會查詢 Memory Cache :

UIImage *image = [self imageFromMemoryCacheForKey:key];
if (image) {
    doneBlock(image, SDImageCacheTypeMemory);
    return nil;
}

如果 Memory Cache 查找不到, 就會查詢 Disk Cache:

dispatch_async(self.ioQueue, ^{
    if (operation.isCancelled) {
        return;
    }

    @autoreleasepool {
        UIImage *diskImage = [self diskImageForKey:key];
        if (diskImage && self.shouldCacheImagesInMemory) {
            NSUInteger cost = SDCacheCostForImage(diskImage);
            [self.memCache setObject:diskImage forKey:key cost:cost];
        }

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

查詢 Disk Cache 的時候有一個小插曲,就是如果 Disk Cache 查詢成功,還會把得到的圖片再次設置到 Memory Cache 中。 這樣做可以最大化那些高頻率展現圖片的效率。

如果緩存查詢成功, 那麼就會直接返回緩存數據。 如果不成功,接下來就開始請求網絡:

id <SDWebImageOperation> subOperation = [self.imageDownloader downloadImageWithURL:url options:downloaderOptions progress:progressBlock completed:^(UIImage *downloadedImage, NSData *data, NSError *error, BOOL finished) {
}

請求網絡使用的是 imageDownloader 屬性,這個示例專門負責下載圖片數據。 如果下載失敗, 會把失敗的圖片地址寫入 failedURLs 集合:

if (   error.code != NSURLErrorNotConnectedToInternet
    && error.code != NSURLErrorCancelled
    && error.code != NSURLErrorTimedOut
    && error.code != NSURLErrorInternationalRoamingOff
    && error.code != NSURLErrorDataNotAllowed
    && error.code != NSURLErrorCannotFindHost
    && error.code != NSURLErrorCannotConnectToHost) {
    @synchronized (self.failedURLs) {
        [self.failedURLs addObject:url];
    }
}

爲什麼要有這個 failedURLs 呢, 因爲 SDWebImage 默認會有一個對上次加載失敗的圖片拒絕再次加載的機制。 也就是說,一張圖片在本次會話加載失敗了,如果再次加載就會直接拒絕。SDWebImage 這樣做可能是爲了提高性能吧。這個機制可能會容易被大家忽略,所以這裏特意提一下,說不定哪天遇到一些奇怪問題時候,這個知識點會幫你快速定位問題~

如果下載圖片成功了,接下來就會使用 [self.imageCache storeImage] 方法將它寫入緩存,並且調用 completedBlock 告訴前端顯示圖片:

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

dispatch_main_sync_safe(^{
    if (strongOperation && !strongOperation.isCancelled) {
        completedBlock(downloadedImage, nil, SDImageCacheTypeNone, finished, url);
    }
});

好了,到此爲止 SDWebImage 的整體圖片加載流程就都走完了。 由於要控制篇幅,我這裏只挑了最重點的幾個節點寫出來,SDWebImage 的完整機制肯定會更全面,先幫大家疏通思路。

是否要重試失敗的 URL

SDWebImage 的整體圖片處理流程咱們體驗了一遍。 那麼有哪些重要的點對咱們使用它會有幫助呢? 我幫大家整理了一些。

你可以在加載圖片的時候設置 SDWebImageRetryFailed 標記,這樣 SDWebImage 就會加載之前失敗過的圖片了。 記得我們前面提到的 failedURLs 屬性了吧,這個屬性是在內存中存儲的,如果圖片加載失敗, SDWebImage 會在本次 APP 會話中都不再重試這張圖片了。當然這個加載失敗是有條件的,如果是超時失敗,不記在內。

總之,如果你更需要圖片的可用性,而不是這一點點的性能優化,那麼你就可以帶上 SDWebImageRetryFailed 標記:

[_image sd_setImageWithURL:[NSURL URLWithString:@"http://swiftcafe.io/images/qrcodexx.jpg"] placeholderImage:nil options:SDWebImageRetryFailed];

Disk 緩存清理策略

SDWebImage 會在每次 APP 結束的時候執行清理任務。 清理緩存的規則分兩步進行。 第一步先清除掉過期的緩存文件。 如果清除掉過期的緩存之後,空間還不夠。 那麼就繼續按文件時間從早到晚排序,先清除最早的緩存文件,直到剩餘空間達到要求。

具體點,SDWebImage 是怎麼控制哪些緩存過期,以及剩餘空間多少纔夠呢? 通過兩個屬性:

@interface SDImageCache : NSObject

@property (assign, nonatomic) NSInteger maxCacheAge;
@property (assign, nonatomic) NSUInteger maxCacheSize;

maxCacheAge 是文件緩存的時長, SDWebImage 會註冊兩個通知:

[[NSNotificationCenter defaultCenter] addObserver:self
                                                 selector:@selector(cleanDisk)
                                                     name:UIApplicationWillTerminateNotification
                                                   object:nil];

[[NSNotificationCenter defaultCenter] addObserver:self
                                         selector:@selector(backgroundCleanDisk)
                                             name:UIApplicationDidEnterBackgroundNotification
                                           object:nil];

分別在應用進入後臺和結束的時候,遍歷所有的緩存文件,如果緩存文件超過 maxCacheAge 中指定的時長,就會被刪除掉。

同樣的, maxCacheSize 控制 SDImageCache 所允許的最大緩存空間。 如果清理完過期文件後緩存空間依然沒達到 maxCacheSize 的要求, 那麼就會繼續清理舊文件,直到緩存空間達到要求爲止。

瞭解了這個機制對我們有什麼幫助呢? 我們來繼續講解,我們平時在使用 SDWebImage 的時候是沒接觸過它們的。那麼以此推理,它們一定有默認值,也確實有:

static const NSInteger kDefaultCacheMaxCacheAge = 60 * 60 * 24 * 7; // 1 week

上面是 maxCacheAge 的默認值,註釋上寫的很清楚,緩存一週。 再來看看 maxCacheSize。 翻了一遍 SDWebImage 的代碼,並沒有對 maxCacheSize 設置默認值。 這就意味着 SDWebImage 在默認情況下不會對緩存空間設限制。

這一點可以在 SDWebImage 清理緩存的代碼中求證:

if (self.maxCacheSize > 0 && currentCacheSize > self.maxCacheSize) {
    //清理緩存代碼
}

說明一下, 上面代碼中的 currentCacheSize 變量代表當前圖片緩存佔用的空間。 從這裏可以看出, 只有在 maxCacheSize 大於 0 並且當前緩存空間大於 maxCacheSize 的時候才進行第二步的緩存清理。

這意味着什麼呢? 其實就是 SDWebImage 在默認情況下是不對我們的緩存大小設限制的,理論上,APP 中的圖片緩存可以佔滿整個設備。

SDWebImage 的這個特性還是比較容易被大家忽略的,如果你開發的類似信息流的 APP,應該會加載大量的圖片,如果這時候按照默認機制,緩存尺寸是沒有限制的,並且默認的緩存週期是一週。 就很容易造成應用存儲空間佔用偏大的問題。

那麼可能有人會說了,現在 iPhone 的存儲空間都很大,多緩存一點也不是問題吧? 但你是否知道 iOS 上還有一個用量查詢的功能呢。在設置項中用戶可以查看每個 APP 的空間使用情況, 如果你的 APP 佔用空間比較大的話,就很容易成爲用戶的卸載目標,這應該是需要關注的一個細節。

另外,過多的佔用緩存空間其實並不一定有用。大部分情況是一些圖片被緩存下來後,很少再被重複展現。所以合理的規劃緩存空間尺寸還是很有必要的。可以這樣設置:

[SDImageCache sharedImageCache].maxCacheSize = 1024 * 1024 * 50;    // 50M

maxCacheSize 是以字節來表示的,我們上面的計算代表 50M 的最大緩存空間。 把這行代碼寫在你的 APP 啓動的時候,這樣 SDWebImage 在清理緩存的時候,就會清理多餘的緩存文件了。

結語

這次跟大家聊了聊 SDWebImage 整體流程,以及它的緩存策略。SDWebImage 是一個歷時很久的開源庫,並且它不斷的保持着更新。 雖然是一個並不算很複雜的開源庫,但仔細研讀一下它的代碼, 會發現它的內部很多機制設計的還是很巧妙的。 爲了保證大家的閱讀體驗,儘量控制文章的篇幅,這裏儘量選出對大家最有幫助的內容和大家分享。這篇文章結構整理花了幾天時間,仔細篩選最重要的內容。想達到的效果就是,讓他讀起來不累,但卻能很快抓住重點,讓大家得到有用的信息。也希望大家對閱讀體驗能夠提出反饋,幫助我給大家創造更好的內容。

如果想了解 SDWebImage 更詳細的內容,那麼它的 Github 主頁就是最好的地方了,在這裏也貼出來,供大家參考: https://github.com/rs/SDWebImage

更多精彩內容可關注微信公衆號:
swift-cafe

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章