這篇文章通過(guò)SD的源碼,梳理下SD框架下圖片加載的流程。
SD的大致流程相信各位已經(jīng)很熟悉了。(需要加載圖片時(shí),首先查看本地有沒(méi)有存,如果沒(méi)有就去下載,然后緩存到本地,最后再顯示出來(lái)。)我們這里根據(jù)官方提供的流程圖的步驟詳細(xì)分下主要流程。
一、流程:
展開(kāi)之前,我們看一下官方給出的流程圖:
由圖可見(jiàn),SD把加載圖片一共分為了10個(gè)步驟。接下來(lái)我們根據(jù)步驟進(jìn)行詳細(xì)的解讀。
二、流程分解:
1-2-3:無(wú)論我們調(diào)用UI層的哪個(gè)方法,首先都會(huì)進(jìn)入U(xiǎn)I層的總方法sd_internalSetImageWithURL由該方法做一些簡(jiǎn)單的配置,如初始化SDWebImageContext(就是個(gè)NSDictionary,默認(rèn)配置為空)、初始化imageProgress(用于顯示下載進(jìn)度)后,進(jìn)入管理類(lèi)SDWebImageManager的loadImageWithURL方法,這個(gè)方法依然是做一些輔助工作后進(jìn)入SDWebImageManager的callCacheProcessForOperation方法,看名字就知道,緩存相關(guān)的程序,由此開(kāi)始。
4-5:callCacheProcessForOperation里直接調(diào)用了SDImageCache的queryImageForKey方法。SDImageCache作為緩存類(lèi),負(fù)責(zé)緩存相關(guān)的工作的重要類(lèi),日常代碼中甚至可能需要碰到直接操作該類(lèi)的情況。流程由queryImageForKey進(jìn)入queryCacheOperationForKey過(guò)程中也是做了一些配置。然后進(jìn)入查詢(xún)階段的核心邏輯。
--①-- 首先SDImageCache會(huì)先查詢(xún)內(nèi)存中有沒(méi)有圖片。查詢(xún)途徑為通過(guò)SDMemoryCache類(lèi)調(diào)用objectForKey方法。
SDMemoryCache是NSCache的子類(lèi),該類(lèi)最大的特點(diǎn)是直接把數(shù)據(jù)緩存進(jìn)內(nèi)存中,且無(wú)論是查詢(xún)還是存儲(chǔ)速度都特別的快。但是該類(lèi)最大的缺點(diǎn)是內(nèi)存的釋放由系統(tǒng)控制,程序員無(wú)法手動(dòng)控制。
objectForKey的源碼如下:
- (id)objectForKey:(id)key {
id obj = [super objectForKey:key];
if (!self.config.shouldUseWeakMemoryCache) {
return obj;
}
if (key && !obj) {
// Check weak cache
SD_LOCK(_weakCacheLock);
obj = [self.weakCache objectForKey:key];
SD_UNLOCK(_weakCacheLock);
if (obj) {
// Sync cache
NSUInteger cost = 0;
if ([obj isKindOfClass:[UIImage class]]) {
cost = [(UIImage *)obj sd_memoryCost];
}
[super setObject:obj forKey:key cost:cost];
}
}
return obj;
}
簡(jiǎn)單來(lái)說(shuō)如果SDMemoryCache里沒(méi)有查到結(jié)果,就再去weakCache里查,如果查到了就再往SDMemoryCache里存一份,用以保證下次查詢(xún)的速度。
關(guān)于這個(gè)weakCache類(lèi),其實(shí)是NSMapTable類(lèi)的實(shí)例。往簡(jiǎn)單了說(shuō),你可以把NSMapTable當(dāng)成NSDictionary。NSDictionary只能以字符串作為key而NSMapTable能以對(duì)象作為key。(NSMapTable的詳細(xì)介紹可以看這里。)
在這里,就已經(jīng)用到了兩層存儲(chǔ),既SDMemoryCache與NSMapTable。SDMemoryCache的優(yōu)點(diǎn)是快但不穩(wěn)定,NSMapTable的優(yōu)點(diǎn)是穩(wěn)定,但不是那么的快。兩者相輔相成,最大程度保證數(shù)據(jù)的持久性與敏捷性。如果到這里還未查到數(shù)據(jù),則再進(jìn)入從磁盤(pán)查數(shù)據(jù)的流程。
--②-- 磁盤(pán)查找源碼先貼一下
@autoreleasepool {
NSData *diskData = [self diskImageDataBySearchingAllPathsForKey:key]
diskImage = [self diskImageForKey:key data:diskData options:options context:context];
[self.memoryCache setObject:diskImage forKey:key cost:cost];
}
這里只貼了4行核心代碼。第一行建個(gè)自動(dòng)釋放池。第二行磁盤(pán)查找,第三行nsdata轉(zhuǎn)UIImage,第四行往內(nèi)存里存一份。我們對(duì)這四個(gè)核心步驟挨個(gè)分析:
第一行:這里之所以搞個(gè)自動(dòng)釋放池,不少人認(rèn)為是因?yàn)檫@里創(chuàng)建了較多的變量需要及時(shí)釋放。但是仔細(xì)看下代碼,創(chuàng)建的變量并不算多,這個(gè)理由非常牽強(qiáng)。真實(shí)的原因是這里的變量數(shù)據(jù)較大,才需要及時(shí)釋放,而不是因?yàn)樽兞刻?。通過(guò)上面的代碼你就能看到,這里最少有兩個(gè)大數(shù)據(jù)變量,就是圖片數(shù)據(jù)對(duì)應(yīng)的diskData與diskData。如果不對(duì)這兩個(gè)大數(shù)據(jù)變量及時(shí)用autoreleasepool釋放,如果當(dāng)前同時(shí)下載的圖片很多,就會(huì)導(dǎo)致這里堆積大量數(shù)據(jù)占用用內(nèi),導(dǎo)出內(nèi)存不足甚至閃退。同樣的操作在下載的過(guò)程中也出現(xiàn)過(guò)。
第二行:就是從磁盤(pán)里查數(shù)據(jù)了,這里稍稍需要注意的是SD在磁盤(pán)上,默認(rèn)是把數(shù)據(jù)存在了NSCachesDirectory上的,源碼如下。
+ (nullable NSString *)userCacheDirectory {
NSArray<NSString *> *paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);
return paths.firstObject;
}
第三行:之所以有個(gè)復(fù)雜的轉(zhuǎn)換過(guò)程,是因?yàn)檫@個(gè)這個(gè)轉(zhuǎn)換是根據(jù)Options與context進(jìn)行定制的。我們知道SD支持顯示縮略圖與自定義圖片大小的功能,就是用這個(gè)方法進(jìn)行定制的。
第四行:是往內(nèi)存里存數(shù)據(jù)。SD會(huì)根據(jù)是否需要把從磁盤(pán)查到的數(shù)據(jù)往內(nèi)存里存一份。從而進(jìn)一步保證了查詢(xún)了速度。順序是先往NSCache(既上文提到的SDMemoryCache)存,再往NSMapTable里存。
6-7:如果本地都沒(méi)有,就進(jìn)入了網(wǎng)絡(luò)下載的環(huán)節(jié)。此時(shí)會(huì)進(jìn)入callDownloadProcessForOperation方法。這個(gè)方法里經(jīng)過(guò)一系列配置后,進(jìn)入requestImageWithURL方法而后進(jìn)入downloadImageWithURL方法(ps:看這命名,多磨的嚴(yán)謹(jǐn))。在這個(gè)方法里會(huì)把下載任務(wù)封裝進(jìn)NSOpration的子類(lèi)SDWebImageDownloaderOperation中,通過(guò)隊(duì)列進(jìn)行下載。這個(gè)隊(duì)列(downloadQueue)的最大并發(fā)數(shù)默認(rèn)為6個(gè),并通過(guò)一個(gè)監(jiān)聽(tīng)(addObserver)實(shí)時(shí)監(jiān)聽(tīng)并發(fā)數(shù)的改變并跟著改變。
加入隊(duì)列后就進(jìn)入了NSOpration的子類(lèi)SDWebImageDownloaderOperation中了。(關(guān)于自定義NSOpration與NSOperationQueue后邊會(huì)專(zhuān)門(mén)寫(xiě)一個(gè)文章進(jìn)行介紹)
在SDWebImageDownloaderOperation中,使用了NSURLSession的dataTaskWithRequest方法進(jìn)行下載。到這大家就都知道請(qǐng)求的結(jié)果肯定是通過(guò)代理回調(diào)回去了。
額外插一句,感覺(jué)這里一個(gè)比較有意思的操作。作者把progressBlock(過(guò)程回調(diào))與completedBlock(結(jié)果回調(diào))都放進(jìn)了各自的固定key的字典里(SDCallbacksDictionary),然后又把字典放進(jìn)了叫callbackBlocks的數(shù)組里。需要用時(shí)根據(jù)key從數(shù)組里取不同的字典對(duì)應(yīng)的回調(diào)。代碼如下
- (nullable id)addHandlersForProgress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock {
SDCallbacksDictionary *callbacks = [NSMutableDictionary new];
if (progressBlock) callbacks[kProgressCallbackKey] = [progressBlock copy];
if (completedBlock) callbacks[kCompletedCallbackKey] = [completedBlock copy];
@synchronized (self) {
[self.callbackBlocks addObject:callbacks];
}
return callbacks;
}
個(gè)人感覺(jué)可能沒(méi)有必要這樣做,就兩個(gè)block直接讓類(lèi)持有不就好了嘛。不知道這里用到了什么設(shè)計(jì)思路或者設(shè)計(jì)模式,了解的還請(qǐng)不吝賜教。
7-8-9-10:下載完成后,進(jìn)入存儲(chǔ)階段callStoreCacheProcessForOperation順序是先往內(nèi)存里存然后再存磁盤(pán)。最后把UIImage回調(diào)給UI層。
三、結(jié)語(yǔ)
流程分享完畢后,這里其實(shí)可以看出來(lái),SD為了保證數(shù)據(jù)的速度與持久性,其實(shí)把每個(gè)圖片都存了3次。兩次在內(nèi)存(NSCache與NSMapTable)一次在磁盤(pán)(NSCachesDirectory)。這個(gè)雖然比較穩(wěn)妥,但是也確實(shí)占用了不少空間。