SDWebImage的快取策略

powerx_yc發表於2018-08-22

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

基本結構

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

6751932-e39aa7f47c27c4c5.png
ssd.png

有一個專門的 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

轉載:http://swiftcafe.io/2017/02/19/sdimage-cache/

相關文章