SDWebImage 相信對大多數(shù)開發(fā)者來說罐韩,都是一個(gè)不陌生的名字闷祥。它除了幫助我們讀取網(wǎng)絡(luò)圖片夷蚊,還會處理這些圖片的緩存构挤。它的緩存機(jī)制到底是什么樣的呢,讓我給跟大家嘮叨嘮叨惕鼓,希望你能有收獲儿倒。
基本結(jié)構(gòu)
閑言少敘,咱們這就開始。 首先咱們來看看 SDWebImage 的整體結(jié)構(gòu):
有一個(gè)專門的 Cache 分類用來處理圖片的緩存夫否。 這里面也有兩個(gè)類 SDImageCache 和 SDImageCacheConfig彻犁。 大部分的緩存處理都在 SDImageCache 這個(gè)類中實(shí)現(xiàn)。其他幾個(gè)文件夾咱們分別有個(gè)字的功能凰慈,因?yàn)樵蹅冞@次專門討論緩存策略汞幢,所以其他內(nèi)容暫時(shí)略過。
Memory 和 Disk 雙緩存
首先微谓,SDWebImage 的圖片緩存采用的是 Memory 和 Disk 雙重 Cache 機(jī)制森篷, 聽起來挺高大上吧。其實(shí)也不復(fù)雜豺型。
我們先來看看 Memory Cache仲智,貼一段 SDImageCache 的代碼:
@interface SDImageCache ()
#pragma mark - Properties
@property (strong, nonatomic, nonnull) NSCache *memCache;
這里我們發(fā)現(xiàn), 有一個(gè)叫做 memCache 的屬性姻氨,它是一個(gè) NSCache 對象钓辆,用于實(shí)現(xiàn)我們對圖片的 Memory Cache。 SDWebImage 還專門實(shí)現(xiàn)了一個(gè)叫做 AutoPurgeCache 的類肴焊, 相比于普通的 NSCache前联, 它提供了一個(gè)在內(nèi)存緊張時(shí)候釋放緩存的能力:
@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;
}
其實(shí)就是接受系統(tǒng)的內(nèi)存警告通知,然后清除掉自身的圖片緩存娶眷。 這里大家比較少見的一個(gè)類應(yīng)該是 NSCache 了似嗤。 簡單來說,它是一個(gè)類似于 NSDictionary 的集合類届宠,用于在內(nèi)存中存儲我們要緩存的數(shù)據(jù)烁落。詳細(xì)信息大家可以參考官方文檔: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];
}
然后為每一個(gè)緩存文件生成一個(gè) md5 文件名, 存放到文件中幌羞。
整體機(jī)制
為了節(jié)約篇幅寸谜,提升大家的閱讀體驗(yàn),這里盡量少貼大段代碼属桦。 我們前面介紹了 SDWebImage 同時(shí)使用內(nèi)存和硬盤兩種緩存熊痴。 那么我們來看看當(dāng)使用 SDWebImage 讀取圖片時(shí)候的完整流程。 我們一般會使用 SDWebImage 對 UIKit 的擴(kuò)展聂宾,直接加載圖片:
[imageView sd_setImageWithURL:[NSURL URLWithString:@"http://swiftcafe.io/images/qrcode.jpg"]];
首先這個(gè) Category 方法 sd_setImageWithURL 內(nèi)部會調(diào)用 SDWebImageManager 的 downloadImageWithURL 方法來處理這個(gè)圖片 URL:
id <SDWebImageOperation> operation = [SDWebImageManager.sharedManager downloadImageWithURL:url options:options progress:progressBlock completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
}];
SDWebImageManager 內(nèi)部的 downloadImageWithURL 方法會先使用我們前面提到的 SDImageCache 類的 queryDiskCacheForKey 方法果善,查詢圖片緩存:
operation.cacheOperation = [self.imageCache queryDiskCacheForKey:key done:^(UIImage *image, SDImageCacheType cacheType) {
}];
再來看 queryDiskCacheForKey 方法內(nèi)部, 先會查詢 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 的時(shí)候有一個(gè)小插曲巾陕,就是如果 Disk Cache 查詢成功讨跟,還會把得到的圖片再次設(shè)置到 Memory Cache 中。 這樣做可以最大化那些高頻率展現(xiàn)圖片的效率鄙煤。
如果緩存查詢成功晾匠, 那么就會直接返回緩存數(shù)據(jù)。 如果不成功梯刚,接下來就開始請求網(wǎng)絡(luò):
id <SDWebImageOperation> subOperation = [self.imageDownloader downloadImageWithURL:url options:downloaderOptions progress:progressBlock completed:^(UIImage *downloadedImage, NSData *data, NSError *error, BOOL finished) {
}
請求網(wǎng)絡(luò)使用的是 imageDownloader 屬性凉馆,這個(gè)示例專門負(fù)責(zé)下載圖片數(shù)據(jù)。 如果下載失敗亡资, 會把失敗的圖片地址寫入 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];
}
}
為什么要有這個(gè) failedURLs 呢澜共, 因?yàn)?SDWebImage 默認(rèn)會有一個(gè)對上次加載失敗的圖片拒絕再次加載的機(jī)制。 也就是說锥腻,一張圖片在本次會話加載失敗了嗦董,如果再次加載就會直接拒絕。SDWebImage 這樣做可能是為了提高性能吧瘦黑。這個(gè)機(jī)制可能會容易被大家忽略京革,所以這里特意提一下,說不定哪天遇到一些奇怪問題時(shí)候供璧,這個(gè)知識點(diǎn)會幫你快速定位問題~
如果下載圖片成功了,接下來就會使用 [self.imageCache storeImage] 方法將它寫入緩存冻记,并且調(diào)用 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 的整體圖片加載流程就都走完了。 由于要控制篇幅冗栗,我這里只挑了最重點(diǎn)的幾個(gè)節(jié)點(diǎn)寫出來演顾,SDWebImage 的完整機(jī)制肯定會更全面,先幫大家疏通思路隅居。
是否要重試失敗的 URL
SDWebImage 的整體圖片處理流程咱們體驗(yàn)了一遍钠至。 那么有哪些重要的點(diǎn)對咱們使用它會有幫助呢? 我?guī)痛蠹艺砹艘恍?/p>
你可以在加載圖片的時(shí)候設(shè)置 SDWebImageRetryFailed 標(biāo)記胎源,這樣 SDWebImage 就會加載之前失敗過的圖片了棉钧。 記得我們前面提到的 failedURLs 屬性了吧,這個(gè)屬性是在內(nèi)存中存儲的涕蚤,如果圖片加載失敗宪卿, SDWebImage 會在本次 APP 會話中都不再重試這張圖片了。當(dāng)然這個(gè)加載失敗是有條件的万栅,如果是超時(shí)失敗佑钾,不記在內(nèi)。
總之烦粒,如果你更需要圖片的可用性休溶,而不是這一點(diǎn)點(diǎn)的性能優(yōu)化,那么你就可以帶上 SDWebImageRetryFailed 標(biāo)記:
[_image sd_setImageWithURL:[NSURL URLWithString:@"http://swiftcafe.io/images/qrcodexx.jpg"] placeholderImage:nil options:SDWebImageRetryFailed];
Disk 緩存清理策略
SDWebImage 會在每次 APP 結(jié)束的時(shí)候執(zhí)行清理任務(wù)。 清理緩存的規(guī)則分兩步進(jìn)行兽掰。 第一步先清除掉過期的緩存文件芭碍。 如果清除掉過期的緩存之后,空間還不夠禾进。 那么就繼續(xù)按文件時(shí)間從早到晚排序豁跑,先清除最早的緩存文件,直到剩余空間達(dá)到要求泻云。
具體點(diǎn)艇拍,SDWebImage 是怎么控制哪些緩存過期,以及剩余空間多少才夠呢宠纯? 通過兩個(gè)屬性:
@interface SDImageCache : NSObject
@property (assign, nonatomic) NSInteger maxCacheAge;
@property (assign, nonatomic) NSUInteger maxCacheSize;
maxCacheAge 是文件緩存的時(shí)長卸夕, SDWebImage 會注冊兩個(gè)通知:
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(cleanDisk)
name:UIApplicationWillTerminateNotification
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(backgroundCleanDisk)
name:UIApplicationDidEnterBackgroundNotification
object:nil];
分別在應(yīng)用進(jìn)入后臺和結(jié)束的時(shí)候,遍歷所有的緩存文件婆瓜,如果緩存文件超過 maxCacheAge 中指定的時(shí)長快集,就會被刪除掉。
同樣的廉白, maxCacheSize 控制 SDImageCache 所允許的最大緩存空間个初。 如果清理完過期文件后緩存空間依然沒達(dá)到 maxCacheSize 的要求, 那么就會繼續(xù)清理舊文件猴蹂,直到緩存空間達(dá)到要求為止院溺。
了解了這個(gè)機(jī)制對我們有什么幫助呢? 我們來繼續(xù)講解,我們平時(shí)在使用 SDWebImage 的時(shí)候是沒接觸過它們的磅轻。那么以此推理珍逸,它們一定有默認(rèn)值,也確實(shí)有:
static const NSInteger kDefaultCacheMaxCacheAge = 60 * 60 * 24 * 7; // 1 week
上面是 maxCacheAge 的默認(rèn)值聋溜,注釋上寫的很清楚谆膳,緩存一周。 再來看看 maxCacheSize撮躁。 翻了一遍 SDWebImage 的代碼漱病,并沒有對 maxCacheSize 設(shè)置默認(rèn)值。 這就意味著 SDWebImage 在默認(rèn)情況下不會對緩存空間設(shè)限制把曼。
這一點(diǎn)可以在 SDWebImage 清理緩存的代碼中求證:
if (self.maxCacheSize > 0 && currentCacheSize > self.maxCacheSize) {
//清理緩存代碼
}
說明一下缨称, 上面代碼中的 currentCacheSize 變量代表當(dāng)前圖片緩存占用的空間。 從這里可以看出祝迂, 只有在 maxCacheSize 大于 0 并且當(dāng)前緩存空間大于 maxCacheSize 的時(shí)候才進(jìn)行第二步的緩存清理睦尽。
這意味著什么呢? 其實(shí)就是 SDWebImage 在默認(rèn)情況下是不對我們的緩存大小設(shè)限制的型雳,理論上当凡,APP 中的圖片緩存可以占滿整個(gè)設(shè)備山害。
SDWebImage 的這個(gè)特性還是比較容易被大家忽略的,如果你開發(fā)的類似信息流的 APP沿量,應(yīng)該會加載大量的圖片浪慌,如果這時(shí)候按照默認(rèn)機(jī)制,緩存尺寸是沒有限制的朴则,并且默認(rèn)的緩存周期是一周权纤。 就很容易造成應(yīng)用存儲空間占用偏大的問題。
那么可能有人會說了乌妒,現(xiàn)在 iPhone 的存儲空間都很大汹想,多緩存一點(diǎn)也不是問題吧? 但你是否知道 iOS 上還有一個(gè)用量查詢的功能呢。在設(shè)置項(xiàng)中用戶可以查看每個(gè) APP 的空間使用情況撤蚊, 如果你的 APP 占用空間比較大的話古掏,就很容易成為用戶的卸載目標(biāo),這應(yīng)該是需要關(guān)注的一個(gè)細(xì)節(jié)侦啸。
另外槽唾,過多的占用緩存空間其實(shí)并不一定有用。大部分情況是一些圖片被緩存下來后光涂,很少再被重復(fù)展現(xiàn)庞萍。所以合理的規(guī)劃緩存空間尺寸還是很有必要的⊥牛可以這樣設(shè)置:
[SDImageCache sharedImageCache].maxCacheSize = 1024 * 1024 * 50; // 50M
maxCacheSize 是以字節(jié)來表示的钝计,我們上面的計(jì)算代表 50M 的最大緩存空間。 把這行代碼寫在你的 APP 啟動(dòng)的時(shí)候服赎,這樣 SDWebImage 在清理緩存的時(shí)候葵蒂,就會清理多余的緩存文件了交播。
結(jié)語
這次跟大家聊了聊 SDWebImage 整體流程重虑,以及它的緩存策略。SDWebImage 是一個(gè)歷時(shí)很久的開源庫秦士,并且它不斷的保持著更新缺厉。 雖然是一個(gè)并不算很復(fù)雜的開源庫,但仔細(xì)研讀一下它的代碼隧土, 會發(fā)現(xiàn)它的內(nèi)部很多機(jī)制設(shè)計(jì)的還是很巧妙的提针。 為了保證大家的閱讀體驗(yàn),盡量控制文章的篇幅曹傀,這里盡量選出對大家最有幫助的內(nèi)容和大家分享辐脖。這篇文章結(jié)構(gòu)整理花了幾天時(shí)間,仔細(xì)篩選最重要的內(nèi)容皆愉。想達(dá)到的效果就是嗜价,讓他讀起來不累艇抠,但卻能很快抓住重點(diǎn),讓大家得到有用的信息久锥。也希望大家對閱讀體驗(yàn)?zāi)軌蛱岢龇答伡矣伲瑤椭医o大家創(chuàng)造更好的內(nèi)容。
如果想了解 SDWebImage 更詳細(xì)的內(nèi)容瑟由,那么它的 Github 主頁就是最好的地方了絮重,在這里也貼出來,供大家參考: https://github.com/rs/SDWebImage歹苦。