SDWebImage這個第三方庫有多厲害账千,從它的GitHub上過萬的Star就可以看出來贝次。一直以來都想好好拜讀它的源碼成洗,但之前每次都看得頭昏腦脹的五督,最后都是不了了之。方知武俠小說中修為沒到瓶殃,強練絕世秘籍會導(dǎo)致走火入魔的說法并不是無稽之談充包。
最近項目沒有這么緊張,又靜下心來遥椿,好好研讀了幾遍基矮。終于看出了一點點門道,所以寫篇筆記記錄一下冠场。話不多說愈捅,進入正題。先來一張流程圖壓壓驚:
本文采用講解主體邏輯慈鸠,貼出源碼蓝谨,并在源碼中添加注釋的方法;同時會把比較有特色的點青团,結(jié)合自己的理解譬巫,稍作分析。作為iOS碼農(nóng)界的小學(xué)生督笆,能力有限芦昔,水平一般(腦補郭德綱的聲音。娃肿。咕缎。)。如有不對之處料扰,還望指正凭豪。
為已有的類添加方法,毫無疑問應(yīng)該首先想到類別(Category)這種方法晒杈。那直接進入到UIImageView+WebCache.m文件中嫂伞,看到一系列的方法,其實最終都是走到了UIView+WebCache.m的的這個方法中:
- (void)sd_internalSetImageWithURL:(nullable NSURL *)url
placeholderImage:(nullable UIImage *)placeholder
options:(SDWebImageOptions)options
operationKey:(nullable NSString *)operationKey
setImageBlock:(nullable SDSetImageBlock)setImageBlock
progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
completed:(nullable SDExternalCompletionBlock)completedBlock
context:(nullable NSDictionary<NSString *, id> *)context;
為什么要這么設(shè)計呢拯钻?因為前面流程圖上說了帖努,不光UIImageView有擴展,UIButton也有擴展方法粪般,那么把最終的實現(xiàn)放到他們的共同父類UIView的類別中拼余,也就順理成章了。方法實現(xiàn)中亩歹,一進來是這么兩行代碼:
NSString *validOperationKey = operationKey ?: NSStringFromClass([self class]);
[self sd_cancelImageLoadOperationWithKey:validOperationKey];
這個validOperationKey其實就是為了緩存或者從緩存中查找operation當(dāng)作key用的匙监;如果外部沒有傳入的話寡润,就默認(rèn)的取類名。先用這個key取消可能正在操作的operation舅柜,避免后續(xù)的回調(diào)混亂,保證這個Imageview或者Button只存在一個請求圖片的操作躲惰。進入sd_cancelImageLoadOperationWithKey:方法的內(nèi)部致份,可以看到是在UIView+WebCacheOperation.m中實現(xiàn)的。這里將key和operation映射保存在動態(tài)綁定的SDOperationsDictionary中础拨,名字是dictionary氮块,實際上用到的是NSMapTable。類似于字典诡宗,但比字典更靈活的一個類滔蝉。這篇文章說的比較詳細。
接下來是動態(tài)綁定url到當(dāng)前對象上塔沃;如果設(shè)置的options不是延遲設(shè)置占位圖的話蝠引,就在主線程回調(diào)設(shè)置占位圖:
//動態(tài)綁定url到當(dāng)前對象上
objc_setAssociatedObject(self, &imageURLKey, url, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
//如果設(shè)置的options不是延遲設(shè)置占位圖的話,就在主線程回調(diào)設(shè)置占位圖
if (!(options & SDWebImageDelayPlaceholder)) {
dispatch_main_async_safe(^{
[self sd_setImage:placeholder imageData:nil basedOnClassOrViaCustomSetImageBlock:setImageBlock];
});
}
接下來的這個最外層的if else的大邏輯就是蛀柴,如果傳入的url不為nil的話螃概,就走下面一大段邏輯;否則發(fā)起錯誤回調(diào)鸽疾〉跬荩看看url不為空后續(xù)的邏輯,對照著注釋應(yīng)該比較清楚:
#if SD_UIKIT
// 檢查activityView是否可用
if ([self sd_showActivityIndicatorView]) {
//添加菊花控件
[self sd_addActivityIndicator];
}
#endif
// 初始化sd_imageProgress的總?cè)蝿?wù)數(shù)和任務(wù)完成數(shù)
self.sd_imageProgress.totalUnitCount = 0;
self.sd_imageProgress.completedUnitCount = 0;
//取到manager制肮,如果外部傳入了就用傳入的值
SDWebImageManager *manager;
if ([context valueForKey:SDWebImageExternalCustomManagerKey]) {
manager = (SDWebImageManager *)[context valueForKey:SDWebImageExternalCustomManagerKey];
} else {
manager = [SDWebImageManager sharedManager];
}
//弱引用self冒窍,防止引用循環(huán)
__weak __typeof(self)wself = self;
//把傳進來的progressBlock封裝一下,后續(xù)生成operation時使用
SDWebImageDownloaderProgressBlock combinedProgressBlock = ^(NSInteger receivedSize, NSInteger expectedSize, NSURL * _Nullable targetURL) {
wself.sd_imageProgress.totalUnitCount = expectedSize;
wself.sd_imageProgress.completedUnitCount = receivedSize;
if (progressBlock) {
progressBlock(receivedSize, expectedSize, targetURL);
}
};
其中的SD_UIKIT宏定義后面會頻繁出現(xiàn)豺鼻,其實就類似一個bool值综液,在iOS和tvOS中為真,其他系統(tǒng)下為假:
#if TARGET_OS_IOS || TARGET_OS_TV
#define SD_UIKIT 1
#else
#define SD_UIKIT 0
#endif
然后是生成operation的實現(xiàn):
id <SDWebImageOperation> operation = [manager loadImageWithURL:url options:options progress:combinedProgressBlock completed:^(UIImage *image, NSData *data, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
__strong __typeof (wself) sself = wself;
if (!sself) { return; }
#if SD_UIKIT
[sself sd_removeActivityIndicator];
#endif
...
代碼太長了沒有截完儒飒,大體邏輯就是manager使用傳進來的url意乓,options,completedBlock约素,和上面生成的combinedProgressBlock參數(shù)届良,生成一個operation,并把它和key映射保存在動態(tài)綁定的SDOperationsDictionary中圣猎。我們不管生成operation的細節(jié)士葫,先來看看它的回調(diào)block實現(xiàn)。
block一進來是對weakSelf的強引用送悔,因為前面對self進行了弱引用慢显。這里說個比較有意思的點爪模。以前最開始看到block實現(xiàn)中這種先weak再strong的做法我其實非常不理解,疑惑的是這樣做到底會不會增加該對象的引用計數(shù)荚藻?如果會的話這跟不做轉(zhuǎn)換有什么區(qū)別屋灌?當(dāng)時百度google了一大堆,硬是沒有看懂应狱,所以這個問題拖了很久共郭,面試還被問到過。后來有一次突然看到一篇文章里面說疾呻,block里面的strong引用weakSelf除嘹,是為了防止多線程切換的時候,weakSelf被提前釋放了岸蜗,后續(xù)再訪問該對象的時候尉咕,引起野指針崩潰才這么做的。我突然豁然開朗璃岳,原來先weak后strong的目的其實有兩個:
- 先weak引用當(dāng)前對象年缎,是為了讓block捕獲的對象是一個弱引用的對象。這樣就打破了引用循環(huán)铃慷,防止雙方都強引用對方晦款,形成引用循環(huán),導(dǎo)致內(nèi)存泄漏枚冗。
- block內(nèi)部的strong是為了增加對象的引用計數(shù)缓溅,保證該對象在block內(nèi)部是一直存在的,防止在多線程切換的時候?qū)ο蟊惶崆搬尫帕尬拢罄m(xù)訪問導(dǎo)致野指針崩潰坛怪。
有時候不得不感嘆,能夠在特定的時間節(jié)點碰到對的人或物股囊,是多么幸運的一件事情袜匿。
好了,我們繼續(xù)來看回調(diào)block的實現(xiàn)細節(jié),主要是一些條件判斷和回調(diào)處理稚疹,沒有什么值得特別說明的居灯,對照注釋應(yīng)該是挺好理解的:
#if SD_UIKIT
//如果前面添加了菊花控件,這里先移除
[sself sd_removeActivityIndicator];
#endif
// 如果操作完成内狗,沒有錯誤怪嫌,而且progress沒有更新的話,手動將其置為Unknown狀態(tài)柳沙。
if (finished && !error && sself.sd_imageProgress.totalUnitCount == 0 && sself.sd_imageProgress.completedUnitCount == 0) {
sself.sd_imageProgress.totalUnitCount = SDWebImageProgressUnitCountUnknown;
sself.sd_imageProgress.completedUnitCount = SDWebImageProgressUnitCountUnknown;
}
//如果已經(jīng)完成岩灭,或者options設(shè)置了不自動賦值圖片選項的話,就需要執(zhí)行完成回調(diào)
BOOL shouldCallCompletedBlock = finished || (options & SDWebImageAvoidAutoSetImage);
//如果(圖片存在 且 options設(shè)置了不自動賦值圖片這個選項的話) 或者 (圖片不存在 且 options沒有設(shè)置延遲顯示placeholder圖片選項)的話赂鲤,就不要將回調(diào)中的image設(shè)置給當(dāng)前控件噪径。
BOOL shouldNotSetImage = ((image && (options & SDWebImageAvoidAutoSetImage)) ||
(!image && !(options & SDWebImageDelayPlaceholder)));
//為callCompletedBlockClojure賦值
SDWebImageNoParamsBlock callCompletedBlockClojure = ^{
if (!sself) { return; }
if (!shouldNotSetImage) {
[sself sd_setNeedsLayout];
}
if (completedBlock && shouldCallCompletedBlock) {
completedBlock(image, error, cacheType, url);
}
};
if (shouldNotSetImage) {
dispatch_main_async_safe(callCompletedBlockClojure);
return;
}
UIImage *targetImage = nil;
NSData *targetData = nil;
if (image) {
targetImage = image;
targetData = data;
} else if (options & SDWebImageDelayPlaceholder) {
targetImage = placeholder;
targetData = nil;
}
#if SD_UIKIT || SD_MAC
// 檢查是否需要執(zhí)行圖片轉(zhuǎn)換
SDWebImageTransition *transition = nil;
//如果已經(jīng)完成 且 (options設(shè)置了強制轉(zhuǎn)換選項 或者 緩存類型為SDImageCacheTypeNone柱恤,即沒有命中緩存,是從網(wǎng)絡(luò)獲取的圖片)的話找爱,就取UIView+WebCache.h頭文件中定義的sd_imageTransition轉(zhuǎn)換策略梗顺。其實是提供了自定義圖片轉(zhuǎn)換的功能,SD默認(rèn)這個屬性是nil车摄。
if (finished && (options & SDWebImageForceTransition || cacheType == SDImageCacheTypeNone)) {
transition = sself.sd_imageTransition;
}
#endif
//不同宏定義下執(zhí)行響應(yīng)的設(shè)置方法
dispatch_main_async_safe(^{
#if SD_UIKIT || SD_MAC
[sself sd_setImage:targetImage imageData:targetData basedOnClassOrViaCustomSetImageBlock:setImageBlock transition:transition cacheType:cacheType imageURL:imageURL];
#else
[sself sd_setImage:targetImage imageData:targetData basedOnClassOrViaCustomSetImageBlock:setImageBlock];
#endif
callCompletedBlockClojure();
});
說完了block回調(diào)寺谤,我們進入manager生成operation的方法中一探究竟,即這個方法:
- (id <SDWebImageOperation>)loadImageWithURL:(nullable NSURL *)url
options:(SDWebImageOptions)options
progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
completed:(nullable SDInternalCompletionBlock)completedBlock
這個方法的實現(xiàn)代碼比較多,我們先把它拆分成以下幾個點來逐一研究:
- url參數(shù)判斷轉(zhuǎn)換
- 判斷緩存策略练般,生成operation的緩存查詢操作
- 在operation的緩存查詢回調(diào)中看是否需要下載
看一下第一部分,我在代碼中加了注釋锈候,也是比較好理解的:
//先是一個斷言薄料,指明完成回調(diào)是必要的參數(shù),否則調(diào)用這個方法是沒有意義的
NSAssert(completedBlock != nil, @"If you mean to prefetch the image, use -[SDWebImagePrefetcher prefetchURLs] instead");
// 防止常見的把NSString當(dāng)作NSURL傳入的錯誤
if ([url isKindOfClass:NSString.class]) {
url = [NSURL URLWithString:(NSString *)url];
}
if (![url isKindOfClass:NSURL.class]) {
url = nil;
}
SDWebImageCombinedOperation *operation = [SDWebImageCombinedOperation new];
//operation引用了manager泵琳,方便它內(nèi)部使用摄职。注意operation的manager屬性是弱引用,防止引用循環(huán)获列。
operation.manager = self;
//查詢是否是之前請求過的但是失敗了的url谷市,這里用了信號量做鎖,下面會詳細說一下
BOOL isFailedUrl = NO;
if (url) {
LOCK(self.failedURLsLock);
isFailedUrl = [self.failedURLs containsObject:url];
UNLOCK(self.failedURLsLock);
}
//如果url長度為空 或者 (options沒有設(shè)置失敗后重試選項 且 是之前請求失敗的url) 的話击孩,調(diào)用完成回調(diào)迫悠,回傳error。callCompletionBlockForOperation:方法其實沒做什么事巩梢,最終使用的是一個安全調(diào)用block的宏创泄。
if (url.absoluteString.length == 0 || (!(options & SDWebImageRetryFailed) && isFailedUrl)) {
[self callCompletionBlockForOperation:operation completion:completedBlock error:[NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorFileDoesNotExist userInfo:nil] url:url];
return operation;
}
這里比較有意思的是橙喘,查詢是否是之前請求過的失敗url時抑淫,使用了信號量加鎖∠臃停縱觀整個SD忌警,很多地方都使用了這種方法替代互斥鎖來保證線程安全搁拙。把信號量當(dāng)做鎖其實用法也比較簡單,看manager 的 init 方法里法绵,初始化了兩個信號量箕速,都是當(dāng)做鎖來用的。
- (nonnull instancetype)initWithCache:(nonnull SDImageCache *)cache downloader:(nonnull SDWebImageDownloader *)downloader {
if ((self = [super init])) {
_imageCache = cache;
_imageDownloader = downloader;
_failedURLs = [NSMutableSet new];
_failedURLsLock = dispatch_semaphore_create(1);
_runningOperations = [NSMutableSet new];
_runningOperationsLock = dispatch_semaphore_create(1);
}
return self;
}
//再看LOCK 和 UNLOCK是什么
#define LOCK(lock) dispatch_semaphore_wait(lock, DISPATCH_TIME_FOREVER);
#define UNLOCK(lock) dispatch_semaphore_signal(lock);
就是初始化一個為1的dispatch_semaphore_t變量朋譬,當(dāng)前operation搶占到資源時弧满,先調(diào)用dispatch_semaphore_wait方法將信號量減1,此時dispatch_semaphore_t變量為零此熬,如果再有其他operation想要獲取該變量庭呜,就只能排隊等著滑进,啥時候前一個operation跑完了dispatch_semaphore_signal方法,將信號量加了1募谎。后面的operation才能獲取到該信號量進行下一步扶关。當(dāng)然信號量是一種比較底層的同步機制,不光是當(dāng)鎖用這么簡單数冬。這篇文章有各種鎖的說明和比較节槐。
接下來是判斷緩存策略,生成operation的緩存查詢操作:
//先將當(dāng)前operation添加到正在進行的所有operation的無序集合中拐纱,也用到了前面說的信號量加鎖
LOCK(self.runningOperationsLock);
[self.runningOperations addObject:operation];
UNLOCK(self.runningOperationsLock);
//生成后續(xù)查詢和存儲的key铜异,如果用戶自定義了生成key的方法,SD就使用用戶自定義的秸架,否則默認(rèn)為url的absoluteString
NSString *key = [self cacheKeyForURL:url];
//獲取緩存options
SDImageCacheOptions cacheOptions = 0;
if (options & SDWebImageQueryDataWhenInMemory) cacheOptions |= SDImageCacheQueryDataWhenInMemory;
if (options & SDWebImageQueryDiskSync) cacheOptions |= SDImageCacheQueryDiskSync;
if (options & SDWebImageScaleDownLargeImages) cacheOptions |= SDImageCacheScaleDownLargeImages;
//弱引用當(dāng)前operation
__weak SDWebImageCombinedOperation *weakOperation = operation;
operation.cacheOperation = [self.imageCache queryCacheOperationForKey:key options:cacheOptions done:^(UIImage *cachedImage, NSData *cachedData, SDImageCacheType cacheType) {
__strong __typeof(weakOperation) strongOperation = weakOperation;
//如果當(dāng)前operation不存在或者已經(jīng)被取消揍庄,則將其從正在進行的operations無序集合中安全的移除,也用到信號量加鎖
if (!strongOperation || strongOperation.isCancelled) {
[self safelyRemoveOperationFromRunning:strongOperation];
return;
}
// 如果 (options沒有設(shè)置只從緩存中獲取圖片選項) 且 (沒有命中緩存 或者 options設(shè)置了刷新緩存選項) 且 (manager的delegate沒有實現(xiàn)imageManager:shouldDownloadImageForURL代理方法 或者 實現(xiàn)了該方法东抹,返回YES) 的話蚂子,就需要下載圖片
BOOL shouldDownload = (!(options & SDWebImageFromCacheOnly))
&& (!cachedImage || options & SDWebImageRefreshCached)
&& (![self.delegate respondsToSelector:@selector(imageManager:shouldDownloadImageForURL:)] || [self.delegate imageManager:self shouldDownloadImageForURL:url]);
if (shouldDownload) {
if (cachedImage && options & SDWebImageRefreshCached) {
// 如果命中了緩存,且options設(shè)置了刷新緩存選項缭黔,那么先執(zhí)行完成回調(diào)食茎,再去請求圖片,以刷新NSURLCache的緩存
[self callCompletionBlockForOperation:strongOperation completion:completedBlock image:cachedImage data:cachedData error:nil cacheType:cacheType finished:YES url:url];
}
// 設(shè)置后續(xù)downloadToken的options
SDWebImageDownloaderOptions downloaderOptions = 0;
if (options & SDWebImageLowPriority) downloaderOptions |= SDWebImageDownloaderLowPriority;
if (options & SDWebImageProgressiveDownload) downloaderOptions |= SDWebImageDownloaderProgressiveDownload;
if (options & SDWebImageRefreshCached) downloaderOptions |= SDWebImageDownloaderUseNSURLCache;
if (options & SDWebImageContinueInBackground) downloaderOptions |= SDWebImageDownloaderContinueInBackground;
if (options & SDWebImageHandleCookies) downloaderOptions |= SDWebImageDownloaderHandleCookies;
if (options & SDWebImageAllowInvalidSSLCertificates) downloaderOptions |= SDWebImageDownloaderAllowInvalidSSLCertificates;
if (options & SDWebImageHighPriority) downloaderOptions |= SDWebImageDownloaderHighPriority;
if (options & SDWebImageScaleDownLargeImages) downloaderOptions |= SDWebImageDownloaderScaleDownLargeImages;
if (cachedImage && options & SDWebImageRefreshCached) {
downloaderOptions &= ~SDWebImageDownloaderProgressiveDownload;
downloaderOptions |= SDWebImageDownloaderIgnoreCachedResponse;
}
//設(shè)置operation的downloadToken
__weak typeof(strongOperation) weakSubOperation = strongOperation;
strongOperation.downloadToken = [self.imageDownloader downloadImageWithURL:url options:downloaderOptions progress:progressBlock completed:^(UIImage *downloadedImage, NSData *downloadedData, NSError *error, BOOL finished) {
xxx(代碼沒截完)
這段實現(xiàn)的大體邏輯就是馏谨,生成operation以后别渔,設(shè)置它的緩存查詢,在緩存查詢的回調(diào)中檢查是否需要下載圖片惧互,如果需要的話钠糊,再設(shè)置它的downloadToken。也就是先查詢緩存壹哺,如果沒有命中或者設(shè)置了刷新緩存選項的話抄伍,就去下載圖片。那么我們SDImageCache這個工具類中管宵,緩存查詢方法是怎么實現(xiàn)的截珍,對照著注釋看應(yīng)該是比較清楚了:
- (nullable NSOperation *)queryCacheOperationForKey:(nullable NSString *)key options:(SDImageCacheOptions)options done:(nullable SDCacheQueryCompletedBlock)doneBlock {
if (!key) {
if (doneBlock) {
doneBlock(nil, nil, SDImageCacheTypeNone);
}
return nil;
}
// 先從內(nèi)存緩存中查詢圖片,即從SDImageCache的SDMemoryCache類型的memCache屬性中查詢箩朴,SDMemoryCache繼承自NSCache岗喉,我記得比較早的SD版本,memCache使用的是NSDictionary炸庞。NSCache對比字典有什么優(yōu)勢呢钱床?主要有兩點,一是NSCache是線程安全的埠居,另一個是NSCache在內(nèi)存緊張時查牌,會自動清理部分無用數(shù)據(jù)事期。
UIImage *image = [self imageFromMemoryCacheForKey:key];
//如果內(nèi)存中命中了圖片,且options沒有設(shè)置內(nèi)存中有數(shù)據(jù)仍舊查詢磁盤緩存的選項的話纸颜,就直接執(zhí)行完成回調(diào)兽泣,并且返回nil。
BOOL shouldQueryMemoryOnly = (image && !(options & SDImageCacheQueryDataWhenInMemory));
if (shouldQueryMemoryOnly) {
if (doneBlock) {
doneBlock(image, nil, SDImageCacheTypeMemory);
}
return nil;
}
//如果需要進入磁盤查詢胁孙,先設(shè)置好它的回調(diào)block
NSOperation *operation = [NSOperation new];
void(^queryDiskBlock)(void) = ^{
if (operation.isCancelled) {
// do not call the completion if cancelled
return;
}
@autoreleasepool {
NSData *diskData = [self diskImageDataBySearchingAllPathsForKey:key];
UIImage *diskImage;
SDImageCacheType cacheType = SDImageCacheTypeDisk;
if (image) {
// 圖片是從內(nèi)存緩存中命中的
diskImage = image;
cacheType = SDImageCacheTypeMemory;
} else if (diskData) {
// 圖片是從磁盤緩存中命中的
diskImage = [self diskImageForKey:key data:diskData options:options];
if (diskImage && self.config.shouldCacheImagesInMemory) {
//如果SDImageCache的config設(shè)置了shouldCacheImagesInMemory屬性唠倦,那么將從磁盤命中的圖片保存到內(nèi)存中,方便下次使用涮较。SD默認(rèn)將該屬性置為YES
NSUInteger cost = SDCacheCostForImage(diskImage);
[self.memCache setObject:diskImage forKey:key cost:cost];
}
}
//如果options設(shè)置的是同步查詢稠鼻,就直接執(zhí)行完成回調(diào);否則狂票,將回調(diào)異步提交到主隊列候齿。
if (doneBlock) {
if (options & SDImageCacheQueryDiskSync) {
doneBlock(diskImage, diskData, cacheType);
} else {
dispatch_async(dispatch_get_main_queue(), ^{
doneBlock(diskImage, diskData, cacheType);
});
}
}
}
};
if (options & SDImageCacheQueryDiskSync) {
//如果options設(shè)置的是同步查詢,就直接執(zhí)行queryDiskBlock
queryDiskBlock();
} else {
//否則苫亦,將block提交到自己的IO隊列毛肋,SDImageCache初始化時將該隊列指定為了串行怨咪,只能一個接一個的執(zhí)行回調(diào)
dispatch_async(self.ioQueue, queryDiskBlock);
}
return operation;
}
可以看到SD使用的是內(nèi)存和磁盤的二級緩存屋剑,先查詢內(nèi)存,如果命中就直接返回诗眨,沒有命中的話再查詢磁盤緩存唉匾;如果磁盤緩存命中,默認(rèn)會將圖片設(shè)置到內(nèi)存緩存中匠楚,方便下次使用巍膘。同時回調(diào)有同步和異步兩種選擇。緩存查詢這塊的大體邏輯已經(jīng)講完了芋簿,我們順便來看看SDImageCache類中峡懈,關(guān)于緩存清理這部分的實現(xiàn)邏輯。它在初始化的時候就注冊了App將要銷毀和進入后臺的通知与斤,接到通知以后會自動清理內(nèi)存,調(diào)用刪除文件的方法:
- (void)deleteOldFilesWithCompletionBlock:(nullable SDWebImageNoParamsBlock)completionBlock {
dispatch_async(self.ioQueue, ^{
NSURL *diskCacheURL = [NSURL fileURLWithPath:self.diskCachePath isDirectory:YES];
// 確定文件的查詢鍵肪康,是AccessDate 還是ModificationDate
NSURLResourceKey cacheContentDateKey = NSURLContentModificationDateKey;
switch (self.config.diskCacheExpireType) {
case SDImageCacheConfigExpireTypeAccessDate:
cacheContentDateKey = NSURLContentAccessDateKey;
break;
case SDImageCacheConfigExpireTypeModificationDate:
cacheContentDateKey = NSURLContentModificationDateKey;
break;
default:
break;
}
//查詢?nèi)齻€信息,是否是文件夾撩穿,存入緩存的時間磷支,文件大小
NSArray<NSString *> *resourceKeys = @[NSURLIsDirectoryKey, cacheContentDateKey, NSURLTotalFileAllocatedSizeKey];
// 枚舉當(dāng)前路徑下的所有文件
NSDirectoryEnumerator *fileEnumerator = [self.fileManager enumeratorAtURL:diskCacheURL
includingPropertiesForKeys:resourceKeys
options:NSDirectoryEnumerationSkipsHiddenFiles
errorHandler:NULL];
//過期時間,SD默認(rèn)的文件過期時間是一個星期食寡,如果想自定義的話雾狈,可以在SDImageCacheConfig中修改
NSDate *expirationDate = [NSDate dateWithTimeIntervalSinceNow:-self.config.maxCacheAge];
NSMutableDictionary<NSURL *, NSDictionary<NSString *, id> *> *cacheFiles = [NSMutableDictionary dictionary];
NSUInteger currentCacheSize = 0;
// 這里的for循環(huán)有兩個目的
// 1. 將每個過期文件的URL添加到urlsToDelete數(shù)組中,后續(xù)統(tǒng)一移除對應(yīng)的文件
// 2. 將每個文件的信息跟URL對應(yīng)抵皱,存到cacheFiles字典中善榛,后面會用到
NSMutableArray<NSURL *> *urlsToDelete = [[NSMutableArray alloc] init];
for (NSURL *fileURL in fileEnumerator) {
NSError *error;
NSDictionary<NSString *, id> *resourceValues = [fileURL resourceValuesForKeys:resourceKeys error:&error];
if (error || !resourceValues || [resourceValues[NSURLIsDirectoryKey] boolValue]) {
continue;
}
NSDate *modifiedDate = resourceValues[cacheContentDateKey];
if ([[modifiedDate laterDate:expirationDate] isEqualToDate:expirationDate]) {
[urlsToDelete addObject:fileURL];
continue;
}
NSNumber *totalAllocatedSize = resourceValues[NSURLTotalFileAllocatedSizeKey];
currentCacheSize += totalAllocatedSize.unsignedIntegerValue;
cacheFiles[fileURL] = resourceValues;
}
//移除對應(yīng)的文件
for (NSURL *fileURL in urlsToDelete) {
[self.fileManager removeItemAtURL:fileURL error:nil];
}
// 如果用戶設(shè)置了最大磁盤緩存尺寸辩蛋,且當(dāng)前緩存尺寸超過了設(shè)置的最大值。注意SD默認(rèn)是沒有設(shè)置最大磁盤緩存的锭弊。
if (self.config.maxCacheSize > 0 && currentCacheSize > self.config.maxCacheSize) {
// 設(shè)置此次的目標(biāo)尺寸為最大尺寸的一半
const NSUInteger desiredCacheSize = self.config.maxCacheSize / 2;
// 根據(jù)文件修改時間將其排序堪澎,最老的文件在最前面,也就是說最先刪除
NSArray<NSURL *> *sortedFiles = [cacheFiles keysSortedByValueWithOptions:NSSortConcurrent
usingComparator:^NSComparisonResult(id obj1, id obj2) {
return [obj1[NSURLContentModificationDateKey] compare:obj2[NSURLContentModificationDateKey]];
}];
// Delete files until we fall below our desired cache size.
for (NSURL *fileURL in sortedFiles) {
if ([self.fileManager removeItemAtURL:fileURL error:nil]) {
NSDictionary<NSString *, id> *resourceValues = cacheFiles[fileURL];
NSNumber *totalAllocatedSize = resourceValues[NSURLTotalFileAllocatedSizeKey];
currentCacheSize -= totalAllocatedSize.unsignedIntegerValue;
if (currentCacheSize < desiredCacheSize) {
break;
}
}
}
}
if (completionBlock) {
dispatch_async(dispatch_get_main_queue(), ^{
completionBlock();
});
}
});
}
所以每次App進入后臺味滞,SD都會檢查樱蛤,如果磁盤中存在過期文件則刪除;同時如果用戶設(shè)置了最大磁盤緩存尺寸剑鞍,且已經(jīng)使用的磁盤大小超過了這個閾值昨凡,會以最大值的一半作為此次清理的目標(biāo),從最老的文件開始刪蚁署,直到達到目標(biāo)尺寸便脊。
說完緩存查詢這部分,我們回到operation生成downloadToken這里光戈。其實調(diào)用的是SDWebImageDownloader這個工具類來生成downloadToken:
- (nullable SDWebImageDownloadToken *)downloadImageWithURL:(nullable NSURL *)url
options:(SDWebImageDownloaderOptions)options
progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock {
__weak SDWebImageDownloader *wself = self;
return [self addProgressCallback:progressBlock completedBlock:completedBlock forURL:url createCallback:^SDWebImageDownloaderOperation *{
__strong __typeof (wself) sself = wself;
NSTimeInterval timeoutInterval = sself.downloadTimeout;
該方法中最主要的是實現(xiàn)了名叫createCallback的回調(diào)block哪痰,因為其實一個URL對應(yīng)一個下載操作,如果多個控件使用了同一個URL下載久妆,是沒有必要下載多次的晌杰。所以SD會在addProgressCallback:completedBlock:forURL:createCallback:方法中判斷是否已經(jīng)存在了當(dāng)前URL對應(yīng)的下載操作,不存在的話再調(diào)用createCallback創(chuàng)建筷弦。創(chuàng)建下載操作其實就是按部就班的設(shè)置超時時間(SD默認(rèn)15秒)肋演、根據(jù)緩存策略生成request、設(shè)置request的頭信息烂琴;然后根據(jù)request創(chuàng)建對應(yīng)的SDWebImageDownloaderOperation爹殊,然后把SDWebImageDownloader這個工具類的證書驗證及操作優(yōu)先級等屬性賦值給它生成的每個SDWebImageDownloaderOperation。在方法的最后奸绷,有這樣一個if判斷:
if (sself.executionOrder == SDWebImageDownloaderLIFOExecutionOrder) {
// 用戶可以設(shè)置下載操作的執(zhí)行順序梗夸,如果設(shè)置了LIFO(Last In First Out)的話,會將前一個下載操作依賴當(dāng)前下載操作号醉,保證了最后生成的下載操作會最先執(zhí)行
[sself.lastAddedOperation addDependency:operation];
sself.lastAddedOperation = operation;
}
這里稍微引申一下反症,iOS中最常用的多線程編程方法應(yīng)該就是NSOperation和GCD了吧,這里可以看到NSOperation對比GCD的一個優(yōu)點:添加依賴非常方便扣癣。當(dāng)然還有另外的優(yōu)點比如提交的操作可以取消惰帽,可以設(shè)置操作的優(yōu)先級等。所以要根據(jù)不同的應(yīng)用場景選擇最合適的工具父虑。我們接著看addProgressCallback:completedBlock:forURL:createCallback:方法:
- (nullable SDWebImageDownloadToken *)addProgressCallback:(SDWebImageDownloaderProgressBlock)progressBlock
completedBlock:(SDWebImageDownloaderCompletedBlock)completedBlock
forURL:(nullable NSURL *)url
createCallback:(SDWebImageDownloaderOperation *(^)(void))createCallback {
// 因為URL后續(xù)會當(dāng)做保存下載操作的字典查詢的key该酗,所以必須保證不為空。
if (url == nil) {
if (completedBlock != nil) {
completedBlock(nil, nil, nil, NO);
}
return nil;
}
//同步查詢字典中是否存在該url對應(yīng)的操作
LOCK(self.operationsLock);
SDWebImageDownloaderOperation *operation = [self.URLOperations objectForKey:url];
//如果不存在 或者 操作已經(jīng)標(biāo)記為完成
if (!operation || operation.isFinished) {
operation = createCallback();
__weak typeof(self) wself = self;
operation.completionBlock = ^{
__strong typeof(wself) sself = wself;
if (!sself) {
return;
}
//operation的完成回調(diào)中會將自己從URLOperations字典中移除
LOCK(sself.operationsLock);
[sself.URLOperations removeObjectForKey:url];
UNLOCK(sself.operationsLock);
};
[self.URLOperations setObject:operation forKey:url];
[self.downloadQueue addOperation:operation];
}
UNLOCK(self.operationsLock);
//將progressBlock和completeBlock賦值給cancelToken
id downloadOperationCancelToken = [operation addHandlersForProgress:progressBlock completed:completedBlock];
//生成downloadToken
SDWebImageDownloadToken *token = [SDWebImageDownloadToken new];
token.downloadOperation = operation;
token.url = url;
token.downloadOperationCancelToken = downloadOperationCancelToken;
return token;
}
可以看到,從最外層傳入的progressBlock 和 completeBlock 最終都賦值給了cancelToken呜魄,我們進入該方法看看:
- (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];
LOCK(self.callbacksLock);
[self.callbackBlocks addObject:callbacks];
UNLOCK(self.callbacksLock);
return callbacks;
}
就是將兩個回調(diào)先用字典裝起來悔叽,然后添加到callbackBlocks數(shù)組中。這里我剛開始看的時候有個疑問爵嗅,不是一個url對應(yīng)一個下載嗎娇澎,為什么SDWebImageDownloaderOperation內(nèi)部的回調(diào)會是個數(shù)組呢?后來才想明白睹晒,就跟上面說的一樣趟庄,如果多個控件短時間內(nèi)加載同一個url,先加載的那個控件生成了一個下載操作伪很,后續(xù)就沒必要再生成下載操作了戚啥,但是回調(diào)是必須區(qū)分的,因為每個控件的完成回調(diào)中锉试,會把圖片賦值給當(dāng)前控件猫十。所以內(nèi)部的回調(diào)要用一個數(shù)組來裝載,圖片下載完成以后依次調(diào)用每個回調(diào)呆盖。
最終的下載操作都是SDWebImageDownloaderOperation這個類實現(xiàn)的拖云。我們知道使用NSOperation實現(xiàn)多線程的話,只有兩種方法应又,一是使用它的子類:NSInvocationOperation 或者 NSBlockOperation宙项;另外就是自定義一個類,繼承自NSOperation丁频,覆寫它的start方法杉允∫靥可以看到SD使用的是后面一個方法席里。我們看看它的start方法里都做了些什么:
- (void)start {
//一般都是在start方法開始的時候就檢測當(dāng)前操作是否被取消。
@synchronized (self) {
if (self.isCancelled) {
self.finished = YES;
[self reset];
return;
}
//iOS和tvOS都可以在App進入后臺后向系統(tǒng)申請額外的操作時間
#if SD_UIKIT
Class UIApplicationClass = NSClassFromString(@"UIApplication");
BOOL hasApplication = UIApplicationClass && [UIApplicationClass respondsToSelector:@selector(sharedApplication)];
if (hasApplication && [self shouldContinueWhenAppEntersBackground]) {
__weak __typeof__ (self) wself = self;
UIApplication * app = [UIApplicationClass performSelector:@selector(sharedApplication)];
self.backgroundTaskId = [app beginBackgroundTaskWithExpirationHandler:^{
__strong __typeof (wself) sself = wself;
if (sself) {
[sself cancel];
[app endBackgroundTask:sself.backgroundTaskId];
sself.backgroundTaskId = UIBackgroundTaskInvalid;
}
}];
}
#endif
NSURLSession *session = self.unownedSession;
if (!session) {
NSURLSessionConfiguration *sessionConfig = [NSURLSessionConfiguration defaultSessionConfiguration];
sessionConfig.timeoutIntervalForRequest = 15;
//如果外部沒有賦值session給它拢驾,那么就自己在內(nèi)部生成一個奖磁,并賦值給ownedSession,
session = [NSURLSession sessionWithConfiguration:sessionConfig
delegate:self
delegateQueue:nil];
self.ownedSession = session;
}
if (self.options & SDWebImageDownloaderIgnoreCachedResponse) {
// Grab the cached data for later check
NSURLCache *URLCache = session.configuration.URLCache;
if (!URLCache) {
URLCache = [NSURLCache sharedURLCache];
}
NSCachedURLResponse *cachedResponse;
// SD特別指明了 URLCache 的 cachedResponseForRequest:方法不是線程安全的
@synchronized (URLCache) {
cachedResponse = [URLCache cachedResponseForRequest:self.request];
}
if (cachedResponse) {
self.cachedData = cachedResponse.data;
}
}
self.dataTask = [session dataTaskWithRequest:self.request];
self.executing = YES;
}
if (self.dataTask) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wunguarded-availability"
if ([self.dataTask respondsToSelector:@selector(setPriority:)]) {
//設(shè)定dataTask的優(yōu)先級
if (self.options & SDWebImageDownloaderHighPriority) {
self.dataTask.priority = NSURLSessionTaskPriorityHigh;
} else if (self.options & SDWebImageDownloaderLowPriority) {
self.dataTask.priority = NSURLSessionTaskPriorityLow;
}
}
#pragma clang diagnostic pop
[self.dataTask resume];
//調(diào)用progress回調(diào)
for (SDWebImageDownloaderProgressBlock progressBlock in [self callbacksForKey:kProgressCallbackKey]) {
progressBlock(0, NSURLResponseUnknownLength, self.request.URL);
}
__weak typeof(self) weakSelf = self;
//回調(diào)主線程發(fā)送開始下載的通知
dispatch_async(dispatch_get_main_queue(), ^{
[[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStartNotification object:weakSelf];
});
} else {
//如果沒有生成dataTask繁疤,則調(diào)用完成回調(diào)咖为,并傳遞error
[self callCompletionBlocksWithError:[NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorUnknown userInfo:@{NSLocalizedDescriptionKey : @"Task can't be initialized"}]];
[self done];
return;
}
#if SD_UIKIT
Class UIApplicationClass = NSClassFromString(@"UIApplication");
if(!UIApplicationClass || ![UIApplicationClass respondsToSelector:@selector(sharedApplication)]) {
return;
}
if (self.backgroundTaskId != UIBackgroundTaskInvalid) {
UIApplication * app = [UIApplication performSelector:@selector(sharedApplication)];
[app endBackgroundTask:self.backgroundTaskId];
self.backgroundTaskId = UIBackgroundTaskInvalid;
}
#endif
}
最后,我們再來看看SDWebImageManager中調(diào)用imageDownloader工具類生成downloadToken的完成回調(diào)中做了什么事情:
__weak typeof(strongOperation) weakSubOperation = strongOperation;
strongOperation.downloadToken = [self.imageDownloader downloadImageWithURL:url options:downloaderOptions progress:progressBlock completed:^(UIImage *downloadedImage, NSData *downloadedData, NSError *error, BOOL finished) {
__strong typeof(weakSubOperation) strongSubOperation = weakSubOperation;
if (!strongSubOperation || strongSubOperation.isCancelled) {
// 如果strongSubOperation為空稠腊,或者被取消了躁染,什么都不做
} else if (error) {
//如果產(chǎn)生錯誤,則執(zhí)行完成回調(diào)架忌,并回傳錯誤
[self callCompletionBlockForOperation:strongSubOperation completion:completedBlock error:error url:url];
BOOL shouldBlockFailedURL;
// 檢查是否需要將當(dāng)前的url放到請求失敗的url數(shù)組中
if ([self.delegate respondsToSelector:@selector(imageManager:shouldBlockFailedURL:withError:)]) {
shouldBlockFailedURL = [self.delegate imageManager:self shouldBlockFailedURL:url withError:error];
} else {
shouldBlockFailedURL = ( error.code != NSURLErrorNotConnectedToInternet
&& error.code != NSURLErrorCancelled
&& error.code != NSURLErrorTimedOut
&& error.code != NSURLErrorInternationalRoamingOff
&& error.code != NSURLErrorDataNotAllowed
&& error.code != NSURLErrorCannotFindHost
&& error.code != NSURLErrorCannotConnectToHost
&& error.code != NSURLErrorNetworkConnectionLost);
}
if (shouldBlockFailedURL) {
LOCK(self.failedURLsLock);
[self.failedURLs addObject:url];
UNLOCK(self.failedURLsLock);
}
}
else {
//一切正常的話就走到這里
if ((options & SDWebImageRetryFailed)) {
//如果options設(shè)置了SDWebImageRetryFailed選項吞彤,就把當(dāng)前url從failedURLs中移除。因為有可能多次請求一個url,前面請求失敗的話饰恕,就被添加到這個數(shù)組中了挠羔。請求成功的時候需要移除。
LOCK(self.failedURLsLock);
[self.failedURLs removeObject:url];
UNLOCK(self.failedURLsLock);
}
BOOL cacheOnDisk = !(options & SDWebImageCacheMemoryOnly);
// SD自己的manager是默認(rèn)實現(xiàn)了縮放處理的埋嵌,如果使用的是用戶自己的manager就走下面這步進行縮放處理
if (self != [SDWebImageManager sharedManager] && self.cacheKeyFilter && downloadedImage) {
downloadedImage = [self scaledImageForKey:key image:downloadedImage];
}
if (options & SDWebImageRefreshCached && cachedImage && !downloadedImage) {
// Image refresh hit the NSURLCache cache, do not call the completion block
} else if (downloadedImage && (!downloadedImage.images || (options & SDWebImageTransformAnimatedImage)) && [self.delegate respondsToSelector:@selector(imageManager:transformDownloadedImage:withURL:)]) {
//異步執(zhí)行圖片轉(zhuǎn)換操作
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
UIImage *transformedImage = [self.delegate imageManager:self transformDownloadedImage:downloadedImage withURL:url];
if (transformedImage && finished) {
BOOL imageWasTransformed = ![transformedImage isEqual:downloadedImage];
NSData *cacheData;
// pass nil if the image was transformed, so we can recalculate the data from the image
if (self.cacheSerializer) {
cacheData = self.cacheSerializer(transformedImage, (imageWasTransformed ? nil : downloadedData), url);
} else {
cacheData = (imageWasTransformed ? nil : downloadedData);
}
[self.imageCache storeImage:transformedImage imageData:cacheData forKey:key toDisk:cacheOnDisk completion:nil];
}
[self callCompletionBlockForOperation:strongSubOperation completion:completedBlock image:transformedImage data:downloadedData error:nil cacheType:SDImageCacheTypeNone finished:finished url:url];
});
} else {
if (downloadedImage && finished) {
//如果用戶自定義了圖片的緩存處理方法
if (self.cacheSerializer) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
NSData *cacheData = self.cacheSerializer(downloadedImage, downloadedData, url);
[self.imageCache storeImage:downloadedImage imageData:cacheData forKey:key toDisk:cacheOnDisk completion:nil];
});
} else {
//將圖片存入緩存
[self.imageCache storeImage:downloadedImage imageData:downloadedData forKey:key toDisk:cacheOnDisk completion:nil];
}
}
//調(diào)用最終的完成回調(diào)
[self callCompletionBlockForOperation:strongSubOperation completion:completedBlock image:downloadedImage data:downloadedData error:nil cacheType:SDImageCacheTypeNone finished:finished url:url];
}
}
if (finished) {
//如果操作完成破加,將當(dāng)前操作從保存的正在運行的操作數(shù)組中移除
[self safelyRemoveOperationFromRunning:strongSubOperation];
}
}];
至此,SD的所有主流程我們都梳理了一遍雹嗦。這一路的參數(shù)傳遞及各種block回調(diào)范舀,剛開始看的時候確實會比較懵逼。但是只要靜下心來了罪,對照著源碼耐心研讀尿背,最終一定會融會貫通的。當(dāng)然捶惜,SD還有一些其他的模塊田藐,我自己也沒有仔細去看,就不班門弄斧了吱七。第三方源碼的解讀確實是比較花時間汽久,特別是想自己寫一篇比較全面的總結(jié)得時候就更加需要耐心了。一不小心這篇總結(jié)就差不多花了我周末兩天時間踊餐,已經(jīng)周日下午三點多景醇,是時候抓住周末的尾巴啦~就醬,溜了溜了吝岭。三痰。。
參考資料:
https://knightsj.github.io/2018/02/03/SDWebImage%E6%BA%90%E7%A0%81%E8%A7%A3%E6%9E%90/
http://www.reibang.com/p/9e97c11aeea9