SDWebImage5.0進(jìn)行了一次架構(gòu)上的改進(jìn),因?yàn)檗D(zhuǎn)Swift之后,一直沒用到SDWebImage,所以也沒怎么關(guān)注。最近有空剛好學(xué)習(xí)一下SDWebImage5.11的源碼。
一、流程架構(gòu)圖
流程架構(gòu)圖

SDWebImage對(duì)UIButton,UIImageView,NSButton,UIView進(jìn)行了拓展,并對(duì)外提供了接口。無論對(duì)UIButton,UIImageView還是NSButton調(diào)用
sd_setImageWithURL的時(shí)候,最終都會(huì)調(diào)用到UIView拓展類的sd_internalSetImageWithURL方法。前面的拓展都只是對(duì)外的接口,主要邏輯處理放在
SDWebImageManager里面。他相當(dāng)于一個(gè)調(diào)度中心,如果需要緩存(讀跟?。?,他就會(huì)調(diào)用SDImageCache,如果需要下載,就會(huì)調(diào)用SDWebImageDownloader。類似我們MVP模式下的Presenter,收到View拓展接口相關(guān)的參數(shù)后,根據(jù)不同業(yè)務(wù)傳遞給cache跟downloader處理,最后將處理完的數(shù)據(jù)通過block回調(diào)給接口。
最后還有一些工具,沒有在流程圖中畫出來,這里說明一下:
Decoder:做一些編解碼操作,針對(duì)不同類型的圖片進(jìn)行不同的操作。
Transform:從緩存或下載轉(zhuǎn)換圖像加載的轉(zhuǎn)換器協(xié)議。
AnimatedImage:可以替代UIImageView,支持gif
Utils:存放一些枚舉,Define,還有菊花器
Categories:對(duì)需要的類進(jìn)行拓展,大部分是UIImage
Private:一些私人方法
二、代碼部分
1. UIView+WebCache
直接找到
sd_internalSetImageWithURL方法,這是入口進(jìn)來后第一個(gè)處理的方法,處理內(nèi)容如下:a. 拿到舊的operation(任務(wù)),取消其操作,并從
SDOperationsDictionary移除。然后創(chuàng)建新的加載任務(wù),并加入到SDOperationsDictionary中。b. 處理進(jìn)度條,重置進(jìn)度條
c. 處理菊花器
d. 創(chuàng)建SDWebImageManager,并調(diào)用
loadImageWithURL加載圖片
我們先看sd_internalSetImageWithURL方法里面的代碼
a、取消之前的任務(wù)
/*
*通過SDWebImageContextSetImageOperationKey拿到SDOperationsDictionary的key:validOperationKey(說白了這里就是二維字典)
*在通過validOperationKey拿到對(duì)應(yīng)的Operation(任務(wù)),對(duì)任務(wù)進(jìn)行取消之類的相關(guān)操作
*/
NSString *validOperationKey = context[SDWebImageContextSetImageOperationKey];
if (!validOperationKey) {
// pass through the operation key to downstream, which can used for tracing operation or image view class
validOperationKey = NSStringFromClass([self class]);
// 對(duì)context進(jìn)行深拷貝,轉(zhuǎn)為可變字典
SDWebImageMutableContext *mutableContext = [context mutableCopy];
// 將當(dāng)前類對(duì)象名稱裝載進(jìn)mutableContext
mutableContext[SDWebImageContextSetImageOperationKey] = validOperationKey;
// 將mutableContext轉(zhuǎn)回不可變字典context
context = [mutableContext copy];
}
// 將validOperationKey存儲(chǔ)起來
self.sd_latestOperationKey = validOperationKey;
// 如果這個(gè)key存在任務(wù),則取消任務(wù),且從SDOperationsDictionary移除
[self sd_cancelImageLoadOperationWithKey:validOperationKey];
// 將url存儲(chǔ)起來
self.sd_imageURL = url;
這段代碼主要是拿到validOperationKey,并傳給sd_cancelImage方法,sd_cancelImage的邏輯也很簡單,通過validOperationKey,在SDOperationsDictionary里面拿到對(duì)應(yīng)的任務(wù),并取消。下面是sd_cancelImage的代碼:
- (void)sd_cancelImageLoadOperationWithKey:(nullable NSString *)key {
if (key) {
// Cancel in progress downloader from queue
SDOperationsDictionary *operationDictionary = [self sd_operationDictionary];
id<SDWebImageOperation> operation;
// 拿到對(duì)應(yīng)的任務(wù)operation
@synchronized (self) {
operation = [operationDictionary objectForKey:key];
}
if (operation) {
if ([operation conformsToProtocol:@protocol(SDWebImageOperation)]) {
// 如果存在并遵循SDWebImageOperation代理,則取消任務(wù)
[operation cancel];
}
// 最后將任務(wù)移除
@synchronized (self) {
[operationDictionary removeObjectForKey:key];
}
}
}
}
b、占位圖顯示
// 是否需要延遲加載占位圖
if (!(options & SDWebImageDelayPlaceholder)) {
// 主線程顯示占位圖
dispatch_main_async_safe(^{
[self sd_setImage:placeholder imageData:nil basedOnClassOrViaCustomSetImageBlock:setImageBlock cacheType:SDImageCacheTypeNone imageURL:url];
});
}
c、進(jìn)度條,菊花器處理邏輯
if (url) {
// reset the progress
// 重置進(jìn)度條
NSProgress *imageProgress = objc_getAssociatedObject(self, @selector(sd_imageProgress));
if (imageProgress) {
imageProgress.totalUnitCount = 0;
imageProgress.completedUnitCount = 0;
}
#if SD_UIKIT || SD_MAC
// check and start image indicator
// 有菊花器就轉(zhuǎn)菊花
[self sd_startImageIndicator];
id<SDWebImageIndicator> imageIndicator = self.sd_imageIndicator;
#endif
// 拿到當(dāng)前manager,沒有就創(chuàng)建,有的話就將context的移除,防止循環(huán)引用
SDWebImageManager *manager = context[SDWebImageContextCustomManager];
if (!manager) {
manager = [SDWebImageManager sharedManager];
} else {
// remove this manager to avoid retain cycle (manger -> loader -> operation -> context -> manager)
SDWebImageMutableContext *mutableContext = [context mutableCopy];
mutableContext[SDWebImageContextCustomManager] = nil;
context = [mutableContext copy];
}
// 對(duì)進(jìn)度條進(jìn)行處理,如果菊花器是進(jìn)度條類型的,那就讓進(jìn)度條跑起來,回調(diào)進(jìn)度block
SDImageLoaderProgressBlock combinedProgressBlock = ^(NSInteger receivedSize, NSInteger expectedSize, NSURL * _Nullable targetURL) {
if (imageProgress) {
imageProgress.totalUnitCount = expectedSize;
imageProgress.completedUnitCount = receivedSize;
}
#if SD_UIKIT || SD_MAC
if ([imageIndicator respondsToSelector:@selector(updateIndicatorProgress:)]) {
double progress = 0;
if (expectedSize != 0) {
progress = (double)receivedSize / expectedSize;
}
progress = MAX(MIN(progress, 1), 0); // 0.0 - 1.0
dispatch_async(dispatch_get_main_queue(), ^{
[imageIndicator updateIndicatorProgress:progress];
});
}
#endif
if (progressBlock) {
progressBlock(receivedSize, expectedSize, targetURL);
}
};
注意這里并沒有直接調(diào)用combinedProgressBlock處理進(jìn)度條,而是在下面加載圖片的時(shí)候?qū)?code>combinedProgressBlock扔過去處理。
d、通過SDWebImageManager調(diào)用加載圖片的方法
這里調(diào)用了SDWebImageManager的圖片加載方法,將一些必要參數(shù)傳遞過去,接下來就是SDWebImageManager的事情了。
[manager loadImageWithURL:url options:options context:context progress:combinedProgressBlock completed:^(UIImage *image, NSData *data, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL)
2. SDWebImageManager
直接找到
loadImageWithURL方法,這個(gè)方法主要是對(duì)url的一些判斷,context與options的預(yù)處理,內(nèi)容如下:a. 先判斷url的可行性
b. 對(duì)
context,options進(jìn)行預(yù)處理,并放到result里面c. 調(diào)用
callCacheProcessForOperation判斷是否有緩存,如果有則進(jìn)入ImageCache拿到緩存數(shù)據(jù),如果沒有則進(jìn)入callDownloadProcessForOperation方法進(jìn)一步判斷如何下載先看看這些步驟的源碼,看完再看
callCacheProcessForOperation做了些什么
a、判斷url的可行性
// Invoking this method without a completedBlock is pointless
NSAssert(completedBlock != nil, @"If you mean to prefetch the image, use -[SDWebImagePrefetcher prefetchURLs] instead");
// Very common mistake is to send the URL using NSString object instead of NSURL. For some strange reason, Xcode won't
// throw any warning for this type mismatch. Here we failsafe this error by allowing URLs to be passed as NSString.
if ([url isKindOfClass:NSString.class]) {
url = [NSURL URLWithString:(NSString *)url];
}
// Prevents app crashing on argument type error like sending NSNull instead of NSURL
if (![url isKindOfClass:NSURL.class]) {
url = nil;
}
// 可以把operation當(dāng)做是一個(gè)任務(wù),一個(gè)執(zhí)行著讀取圖片(緩存跟加載器的組合)操作的任務(wù)
SDWebImageCombinedOperation *operation = [SDWebImageCombinedOperation new];
operation.manager = self;
BOOL isFailedUrl = NO;
// 檢測當(dāng)前url是否在failedURLs列表中
if (url) {
//os_unfair_lock的宏定義
// 加鎖,防止多個(gè)線程對(duì)failedURLs操作,引起的數(shù)據(jù)問題
SD_LOCK(_failedURLsLock);
isFailedUrl = [self.failedURLs containsObject:url];
SD_UNLOCK(_failedURLsLock);
}
// 如果url為nil,且未設(shè)置SDWebImageRetryFailed,url在failedURLs列表中,執(zhí)行失敗回調(diào)
// SDWebImageRetryFailed為失敗鏈接重試,默認(rèn)是不會(huì)重試
if (url.absoluteString.length == 0 || (!(options & SDWebImageRetryFailed) && isFailedUrl)) {
NSString *description = isFailedUrl ? @"Image url is blacklisted" : @"Image url is nil";
NSInteger code = isFailedUrl ? SDWebImageErrorBlackListed : SDWebImageErrorInvalidURL;
[self callCompletionBlockForOperation:operation completion:completedBlock error:[NSError errorWithDomain:SDWebImageErrorDomain code:code userInfo:@{NSLocalizedDescriptionKey : description}] url:url];
return operation;
}
這里注釋應(yīng)該很清楚了,就是判斷url的可行性,跟url是否在失敗列表里面,如果在的,且options沒有SDWebImageRetryFailed的話,就直接失敗回調(diào)。值得注意的是SD的鎖在iOS10以上用的是os_unfair_lock,iOS10以下用的是OSSpinLockLock(這個(gè)鎖存在任務(wù)優(yōu)先級(jí)問題,已經(jīng)被淘汰了)
b、對(duì)context,options進(jìn)行預(yù)處理,并放到result里面
// 將當(dāng)前operation加入到runningOperations(正在運(yùn)行的operation)
// 加鎖,防止多個(gè)線程對(duì)runningOperations進(jìn)行操作
SD_LOCK(_runningOperationsLock);
[self.runningOperations addObject:operation];
SD_UNLOCK(_runningOperationsLock);
// Preprocess the options and context arg to decide the final the result for manager
// 對(duì)context進(jìn)行預(yù)處理,然后將處理的context跟options包裝到result里面。
/* 里面對(duì)context的處理包括,SDWebImageContextImageTransformer、SDWebImageContextCacheKeyFilter、SDWebImageContextCacheSerializer。分別查看外面是否自定義這3個(gè)key的context,如果有就使用,沒有就使用SD默認(rèn)的。除了SDWebImageContextCacheKeyFilter(緩存url的key)默認(rèn)是本身的url,其他2個(gè)都是nil
*/
SDWebImageOptionsResult *result = [self processedResultForURL:url options:options context:context];
這里先將任務(wù)加入到正在執(zhí)行的列表里面,然后再對(duì)context進(jìn)行預(yù)處理,源代碼是沒有對(duì)options進(jìn)行說明處理的,然后將context跟options放入result里面。context的處理源代碼就不貼出來了,大概就是對(duì)SDWebImageContextImageTransformer、SDWebImageContextCacheKeyFilter、SDWebImageContextCacheSerializer這3個(gè)進(jìn)行一個(gè)判斷,看是否有自定義的傳過來,沒有就用默認(rèn)的。
c、callCacheProcessForOperation的調(diào)用
這里主要是判斷要到哪里去取數(shù)據(jù),ImageCache,還是去下載,接下來就進(jìn)入這個(gè)方法看一下。
這里主要是判斷任務(wù)是否該走緩存查詢,或者直接下載。如果是緩存查詢,就進(jìn)入
SDImageCache里面進(jìn)行緩存查詢,且在此處理緩存結(jié)果的回調(diào)。否則就調(diào)用callDownloadProcessForOperation進(jìn)入下一步判斷。①. 拿到
imageCache,拿到緩存類型queryCacheType②. 通過
options判斷,走緩存還是下載。如果走緩存,則調(diào)用SDImageCache里面的queryImageForKey(開始進(jìn)入SDImageCache的邏輯);如果走下載,則調(diào)用callDownloadProcessForOperation開始下載前的一些處理。
①、拿到imageCache,拿到緩存類型queryCacheType
// Grab the image cache to use
// 查看是否有傳進(jìn)來的自定義緩存對(duì)象,沒有就用默認(rèn)的imageCache
id<SDImageCache> imageCache;
if ([context[SDWebImageContextImageCache] conformsToProtocol:@protocol(SDImageCache)]) {
imageCache = context[SDWebImageContextImageCache];
} else {
imageCache = self.imageCache;
}
// Get the query cache type
// 查看緩存類型,默認(rèn)是all,如果有傳進(jìn)來的就用傳進(jìn)來的
SDImageCacheType queryCacheType = SDImageCacheTypeAll;
if (context[SDWebImageContextQueryCacheType]) {
queryCacheType = [context[SDWebImageContextQueryCacheType] integerValue];
}
②、通過options,判斷緩存查找,還是下載
// Check whether we should query cache
// SD_OPTIONS_CONTAINS為與運(yùn)算,當(dāng)options為SDWebImageFromLoaderOnly時(shí)為true(或者全是1也可以)
// 注意這里是取反,也就是設(shè)置了SDWebImageFromLoaderOnly后是不走緩存,直接下載
BOOL shouldQueryCache = !SD_OPTIONS_CONTAINS(options, SDWebImageFromLoaderOnly);
if (shouldQueryCache) {
// 拿到緩存的key
NSString *key = [self cacheKeyForURL:url context:context];
@weakify(operation);
// 緩存查詢,并返回緩存任務(wù)
operation.cacheOperation = [imageCache queryImageForKey:key options:options context:context cacheType:queryCacheType completion:^(UIImage * _Nullable cachedImage, NSData * _Nullable cachedData, SDImageCacheType cacheType) {
@strongify(operation);
// 如果沒有執(zhí)行的任務(wù),或者任務(wù)被取消了
if (!operation || operation.isCancelled) {
// Image combined operation cancelled by user
// 拋出錯(cuò)誤
[self callCompletionBlockForOperation:operation completion:completedBlock error:[NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorCancelled userInfo:@{NSLocalizedDescriptionKey : @"Operation cancelled by user during querying the cache"}] url:url];
// 安全的移除任務(wù)
[self safelyRemoveOperationFromRunning:operation];
return;
} else if (context[SDWebImageContextImageTransformer] && !cachedImage) {
// 沒拿到緩存圖片,且圖片有經(jīng)過Transformer轉(zhuǎn)化,那就去查詢?cè)紙D片緩存
//有機(jī)會(huì)去查詢?cè)季彺? // Have a chance to query original cache instead of downloading
[self callOriginalCacheProcessForOperation:operation url:url options:options context:context progress:progressBlock completed:completedBlock];
return;
}
// Continue download process
// 走下載流程
[self callDownloadProcessForOperation:operation url:url options:options context:context cachedImage:cachedImage cachedData:cachedData cacheType:cacheType progress:progressBlock completed:completedBlock];
}];
} else {// 走下載流程
// Continue download process
[self callDownloadProcessForOperation:operation url:url options:options context:context cachedImage:nil cachedData:nil cacheType:SDImageCacheTypeNone progress:progressBlock completed:completedBlock];
}
這里解釋一下key是怎么拿(SDWebImage的緩存key是怎么樣的),邏輯在這個(gè)方法里面cacheKeyForURL,代碼就不貼出來了,說一下大概邏輯。
a、
SDWebImage的context里面有個(gè)SDWebImageContextCacheKeyFilter,里面存儲(chǔ)的是用來存放自定義key邏輯的協(xié)議,通過重寫cacheKeyForURL自定義key,如果沒有傳SDWebImageContextCacheKeyFilter進(jìn)來則使用url的string值。
b、然后通過context里面的SDWebImageContextImageThumbnailPixelSize、SDWebImageContextImagePreserveAspectRatio和SDWebImageContextImageTransformer這3個(gè)里面是否有值,如果有值就加上上面的key進(jìn)行拼接,沒值就直接用上面的key。
查到緩存后就是回調(diào)了,回調(diào)看代碼注釋,問題應(yīng)該不大,要注意的是它也走了callDownloadProcessForOperation這個(gè)方法,因?yàn)?code>options為SDWebImageRefreshCached的情況下,也是要走下載的,所以索性將找到的緩存,放到callDownloadProcessForOperation處理,而不是直接回調(diào)。
接下來看一下SDImageCache模塊,看看SDWebImage是如何查詢緩存的。