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。
相關文章
- SDWebImage 快取機制(筆記)Web快取筆記
- 快取策略快取
- Flutter 的快取策略Flutter快取
- RN的快取策略探索快取
- Java Integer的快取策略Java快取
- HTTP - 快取策略HTTP快取
- Web 快取機制 與 快取策略Web快取
- iOS 除SDWebImage之外清理記憶體中快取iOSWeb記憶體快取
- AFNetworking和YTKNetwork的快取策略快取
- 秒懂前端的快取策略前端快取
- PWA常見的快取策略快取
- Web 專案的快取策略Web快取
- http快取策略以及強快取和協商快取淺析HTTP快取
- 瀏覽器快取策略瀏覽器快取
- okhttp之旅(十一)--快取策略HTTP快取
- 我理解的瀏覽器快取策略瀏覽器快取
- Redis篇:持久化、淘汰策略,快取失效策略Redis持久化快取
- 快取更新的四種策略及選取建議快取
- 快取策略之瀏覽器快取瀏覽器
- 輕鬆理解HTTP快取策略HTTP快取
- WKWebView的快取策略不支援POST請求!!!WebView快取
- 深入剖析瀏覽器快取策略瀏覽器快取
- 深度詳解GaussDB bufferpool快取策略快取
- 徹底弄懂瀏覽器快取策略瀏覽器快取
- 前端效能優化之HTTP快取策略前端優化HTTP快取
- (五)Redis 快取異常、應對策略Redis快取
- OkHttp3.0解析——談談內部的快取策略HTTP快取
- 前端網路程式設計之快取策略前端程式設計快取
- 簡易筆記:瀏覽器快取策略筆記瀏覽器快取
- Redis-6-三種快取讀寫策略Redis快取
- 配置Redis作為快取(六種淘汰策略)Redis快取
- 微服務複雜查詢之快取策略微服務快取
- 雲伺服器:Apache快取策略設定伺服器Apache快取
- Redis快取刪除驅逐策略的工作方式 - codemancersRedis快取
- 工程化——前端靜態資源快取策略前端快取
- 詳解快取更新策略及如何選擇快取
- LRU快取替換策略及C#實現快取C#
- Redis的快取穿透、快取雪崩、快取擊穿的區別Redis快取穿透