基本結(jié)構(gòu)
閑言少敘绷耍,咱們這就開始。 首先咱們來看看 SDWebImage 的整體結(jié)構(gòu):
有一個專門的 Cache 分類用來處理圖片的緩存。 這里面也有兩個類 SDImageCache 和 SDImageCacheConfig渐扮。 大部分的緩存處理都在 SDImageCache 這個類中實現(xiàn)窃躲。其他幾個文件夾咱們分別有個字的功能计贰,因為咱們這次專門討論緩存策略,所以其他內(nèi)容暫時略過蒂窒。
Memory 和 Disk 雙緩存
首先躁倒,SDWebImage 的圖片緩存采用的是 Memory 和 Disk 雙重 Cache 機制, 聽起來挺高大上吧洒琢。其實也不復(fù)雜秧秉。?
我們先來看看 Memory Cache,貼一段 SDImageCache 的代碼:
@interfaceSDImageCache ()
#pragma mark - Properties
@property(strong, nonatomic, nonnull) NSCache *memCache;
...
這里我們發(fā)現(xiàn)衰抑, 有一個叫做 memCache 的屬性象迎,它是一個 NSCache 對象,用于實現(xiàn)我們對圖片的 Memory Cache呛踊。 SDWebImage 還專門實現(xiàn)了一個叫做 AutoPurgeCache 的類砾淌, 相比于普通的 NSCache, 它提供了一個在內(nèi)存緊張時候釋放緩存的能力:
@interfaceAutoPurgeCache:NSCache
@end
@implementationAutoPurgeCache
- (nonnullinstancetype)init {
self= [superinit];
if(self) {
#if SD_UIKIT
[[NSNotificationCenterdefaultCenter] addObserver:selfselector:@selector(removeAllObjects) name:UIApplicationDidReceiveMemoryWarningNotificationobject:nil];
#endif
}
returnself;
}
其實就是接受系統(tǒng)的內(nèi)存警告通知恋技,然后清除掉自身的圖片緩存拇舀。 這里大家比較少見的一個類應(yīng)該是 NSCache 了。 簡單來說蜻底,它是一個類似于 NSDictionary 的集合類骄崩,用于在內(nèi)存中存儲我們要緩存的數(shù)據(jù)。詳細信息大家可以參考官方文檔:https://developer.apple.com/reference/foundation/nscache薄辅。
說完 Memory Cache要拂, 我們再來說說 Disk Cache,也就是文件緩存站楚。?
SDWebImage 會將圖片存放到 NSCachesDirectory 目錄中:
- (nullableNSString*)makeDiskCachePath:(nonnullNSString*)fullNamespace {
NSArray *paths =NSSearchPathForDirectoriesInDomains(NSCachesDirectory,NSUserDomainMask,YES);
return[paths[0] stringByAppendingPathComponent:fullNamespace];
}
然后為每一個緩存文件生成一個 md5 文件名, 存放到文件中脱惰。
整體機制
為了節(jié)約篇幅,提升大家的閱讀體驗窿春,這里盡量少貼大段代碼拉一。 我們前面介紹了 SDWebImage 同時使用內(nèi)存和硬盤兩種緩存。 那么我們來看看當(dāng)使用 SDWebImage 讀取圖片時候的完整流程旧乞。 我們一般會使用 SDWebImage 對 UIKit 的擴展蔚润,直接加載圖片:
[imageViewsd_setImageWithURL:[NSURLURLWithString:@"http://swiftcafe.io/images/qrcode.jpg"]];
首先這個 Category 方法 sd_setImageWithURL 內(nèi)部會調(diào)用 SDWebImageManager 的 downloadImageWithURL 方法來處理這個圖片 URL:
id operation = [SDWebImageManager.sharedManager downloadImageWithURL:url options:options progress:progressBlock completed:^(UIImage*image,NSError*error, SDImageCacheType cacheType,BOOLfinished,NSURL*imageURL) {
...
}];
SDWebImageManager 內(nèi)部的 downloadImageWithURL 方法會先使用我們前面提到的 SDImageCache 類的 queryDiskCacheForKey 方法,查詢圖片緩存:
operation.cacheOperation = [self.imageCache queryDiskCacheForKey:key done:^(UIImage *image, SDImageCacheTypecacheType) {
...
}];
再來看 queryDiskCacheForKey 方法內(nèi)部尺栖, 先會查詢 Memory Cache :
UIImage *image= [self imageFromMemoryCacheForKey:key];
if(image) {
doneBlock(image, SDImageCacheTypeMemory);
returnnil;
}
如果 Memory Cache 查找不到嫡纠, 就會查詢 Disk Cache:?
dispatch_async(self.ioQueue, ^{
if (operation.isCancelled) {
return;
}
@autoreleasepool {
UIImage *diskImage = [selfdiskImageForKey: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 查詢成功,還會把得到的圖片再次設(shè)置到 Memory Cache 中除盏。 這樣做可以最大化那些高頻率展現(xiàn)圖片的效率叉橱。
如果緩存查詢成功, 那么就會直接返回緩存數(shù)據(jù)者蠕。 如果不成功窃祝,接下來就開始請求網(wǎng)絡(luò):
id subOperation = [self.imageDownloader downloadImageWithURL:url options:downloaderOptions progress:progressBlock completed:^(UIImage*downloadedImage,NSData*data,NSError*error,BOOLfinished) {
}
請求網(wǎng)絡(luò)使用的是 imageDownloader 屬性,這個示例專門負責(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];
}
}
為什么要有這個 failedURLs 呢, 因為 SDWebImage 默認(rèn)會有一個對上次加載失敗的圖片拒絕再次加載的機制泻仙。 也就是說,一張圖片在本次會話加載失敗了量没,如果再次加載就會直接拒絕玉转。SDWebImage 這樣做可能是為了提高性能吧。這個機制可能會容易被大家忽略殴蹄,所以這里特意提一下究抓,說不定哪天遇到一些奇怪問題時候,這個知識點會幫你快速定位問題~
如果下載圖片成功了袭灯,接下來就會使用 [self.imageCache storeImage] 方法將它寫入緩存刺下,并且調(diào)用 completedBlock 告訴前端顯示圖片:
if(downloadedImage && finished) {
[self.imageCachestoreImage:downloadedImagerecalculateFromImage:NOimageData:dataforKey:keytoDisk:cacheOnDisk];
}
dispatch_main_sync_safe(^{
if(strongOperation && !strongOperation.isCancelled) {
completedBlock(downloadedImage, nil, SDImageCacheTypeNone, finished, url);
}
});
好了,到此為止 SDWebImage 的整體圖片加載流程就都走完了稽荧。 由于要控制篇幅橘茉,我這里只挑了最重點的幾個節(jié)點寫出來,SDWebImage 的完整機制肯定會更全面姨丈,先幫大家疏通思路畅卓。
是否要重試失敗的 URL
SDWebImage 的整體圖片處理流程咱們體驗了一遍。 那么有哪些重要的點對咱們使用它會有幫助呢蟋恬? 我?guī)痛蠹艺砹艘恍?/p>
你可以在加載圖片的時候設(shè)置 SDWebImageRetryFailed 標(biāo)記翁潘,這樣 SDWebImage 就會加載之前失敗過的圖片了。 記得我們前面提到的 failedURLs 屬性了吧歼争,這個屬性是在內(nèi)存中存儲的拜马,如果圖片加載失敗, SDWebImage 會在本次 APP 會話中都不再重試這張圖片了沐绒。當(dāng)然這個加載失敗是有條件的俩莽,如果是超時失敗,不記在內(nèi)洒沦。
總之豹绪,如果你更需要圖片的可用性,而不是這一點點的性能優(yōu)化,那么你就可以帶上 SDWebImageRetryFailed 標(biāo)記:
[_imagesd_setImageWithURL:[NSURLURLWithString:@"http://swiftcafe.io/images/qrcodexx.jpg"]placeholderImage:niloptions:SDWebImageRetryFailed];
Disk 緩存清理策略
SDWebImage 會在每次 APP 結(jié)束的時候執(zhí)行清理任務(wù)瞒津。 清理緩存的規(guī)則分兩步進行蝉衣。 第一步先清除掉過期的緩存文件。 如果清除掉過期的緩存之后巷蚪,空間還不夠病毡。 那么就繼續(xù)按文件時間從早到晚排序,先清除最早的緩存文件屁柏,直到剩余空間達到要求啦膜。
具體點,SDWebImage 是怎么控制哪些緩存過期淌喻,以及剩余空間多少才夠呢僧家? 通過兩個屬性:
@interfaceSDImageCache : 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];
分別在應(yīng)用進入后臺和結(jié)束的時候裸删,遍歷所有的緩存文件八拱,如果緩存文件超過 maxCacheAge 中指定的時長,就會被刪除掉涯塔。
同樣的肌稻, maxCacheSize 控制 SDImageCache 所允許的最大緩存空間。 如果清理完過期文件后緩存空間依然沒達到 maxCacheSize 的要求匕荸, 那么就會繼續(xù)清理舊文件爹谭,直到緩存空間達到要求為止。
了解了這個機制對我們有什么幫助呢? 我們來繼續(xù)講解榛搔,我們平時在使用 SDWebImage 的時候是沒接觸過它們的诺凡。那么以此推理,它們一定有默認(rèn)值药薯,也確實有:
static const NSInteger kDefaultCacheMaxCacheAge =60*60*24*7;// 1 week
上面是 maxCacheAge 的默認(rèn)值绑洛,注釋上寫的很清楚,緩存一周童本。 再來看看 maxCacheSize真屯。 翻了一遍 SDWebImage 的代碼,并沒有對 maxCacheSize 設(shè)置默認(rèn)值穷娱。 這就意味著 SDWebImage 在默認(rèn)情況下不會對緩存空間設(shè)限制绑蔫。
這一點可以在 SDWebImage 清理緩存的代碼中求證:
if(self.maxCacheSize >0&& currentCacheSize >self.maxCacheSize) {
//清理緩存代碼
}
說明一下, 上面代碼中的 currentCacheSize 變量代表當(dāng)前圖片緩存占用的空間泵额。 從這里可以看出配深, 只有在 maxCacheSize 大于 0 并且當(dāng)前緩存空間大于 maxCacheSize 的時候才進行第二步的緩存清理。
這意味著什么呢嫁盲? 其實就是 SDWebImage 在默認(rèn)情況下是不對我們的緩存大小設(shè)限制的篓叶,理論上,APP 中的圖片緩存可以占滿整個設(shè)備。
SDWebImage 的這個特性還是比較容易被大家忽略的缸托,如果你開發(fā)的類似信息流的 APP左敌,應(yīng)該會加載大量的圖片,如果這時候按照默認(rèn)機制俐镐,緩存尺寸是沒有限制的矫限,并且默認(rèn)的緩存周期是一周。 就很容易造成應(yīng)用存儲空間占用偏大的問題佩抹。
那么可能有人會說了叼风,現(xiàn)在 iPhone 的存儲空間都很大,多緩存一點也不是問題吧? 但你是否知道 iOS 上還有一個用量查詢的功能呢棍苹。在設(shè)置項中用戶可以查看每個 APP 的空間使用情況无宿, 如果你的 APP 占用空間比較大的話,就很容易成為用戶的卸載目標(biāo)枢里,這應(yīng)該是需要關(guān)注的一個細節(jié)懈贺。
另外,過多的占用緩存空間其實并不一定有用坡垫。大部分情況是一些圖片被緩存下來后,很少再被重復(fù)展現(xiàn)画侣。所以合理的規(guī)劃緩存空間尺寸還是很有必要的冰悠。可以這樣設(shè)置:
[SDImageCache sharedImageCache].maxCacheSize =1024*1024*50;// 50M
maxCacheSize 是以字節(jié)來表示的配乱,我們上面的計算代表 50M 的最大緩存空間溉卓。 把這行代碼寫在你的 APP 啟動的時候,這樣 SDWebImage 在清理緩存的時候搬泥,就會清理多余的緩存文件了桑寨。
結(jié)語
這次跟大家聊了聊 SDWebImage 整體流程,以及它的緩存策略忿檩。SDWebImage 是一個歷時很久的開源庫尉尾,并且它不斷的保持著更新。 雖然是一個并不算很復(fù)雜的開源庫燥透,但仔細研讀一下它的代碼沙咏, 會發(fā)現(xiàn)它的內(nèi)部很多機制設(shè)計的還是很巧妙的。 為了保證大家的閱讀體驗班套,盡量控制文章的篇幅肢藐,這里盡量選出對大家最有幫助的內(nèi)容和大家分享。這篇文章結(jié)構(gòu)整理花了幾天時間吱韭,仔細篩選最重要的內(nèi)容吆豹。想達到的效果就是,讓他讀起來不累,但卻能很快抓住重點痘煤,讓大家得到有用的信息凑阶。也希望大家對閱讀體驗?zāi)軌蛱岢龇答仯瑤椭医o大家創(chuàng)造更好的內(nèi)容速勇。
如果想了解 SDWebImage 更詳細的內(nèi)容晌砾,那么它的 Github 主頁就是最好的地方了,在這里也貼出來烦磁,供大家參考:?https://github.com/rs/SDWebImage养匈。
轉(zhuǎn)載:http://swiftcafe.io/2017/02/19/sdimage-cache/