前言
之前在寫項目時,經常用到SDWebImage這個第三方庫來加載圖片,并且了解到了這個第三方庫在處理圖片時自帶異步下載和緩存功能,以及對cell復用的處理。這篇文章來系統學習一下SDWebImage第三方庫的知識以及底層原理
簡介
SDWebImage
為UIImageView
、UIButton
提供了下載分類,可以很簡單地實現圖片異步下載與緩存功能。SDWebImage
的第三方庫具有以下特性:
異步下載圖片
異步緩存(內存+磁盤),自動管理緩存有效性
同一個URL不會重復下載
自動識別無效URL,不會反復重試
不阻塞主線程
使用GCD與ARC
用法
1.在UITableView中使用UIImageView+WebCache
UITabelViewCell
中的 UIImageView
控件直接調用 sd_setImageWithURL: placeholderImage:
方法即可
2.使用回調Blocks
[listTableViewCell.sightsImageView sd_setImageWithURL:(nullable NSURL *) completed:^(UIImage * _Nullable image, NSError * _Nullable error, SDImageCacheType cacheType, NSURL * _Nullable imageURL) {NSLog(@"回調");}];
3.使用SDWebImageManager單例類
SDWebImage是一個單例類,也是SDWebImage庫中的核心類,負責下載與緩存的處理
+ (nonnull instancetype)sharedManager {static dispatch_once_t once;static id instance;dispatch_once(&once, ^{instance = [self new];});return instance;
}
?
- (nonnull instancetype)init {id<SDImageCache> cache = [[self class] defaultImageCache];if (!cache) {cache = [SDImageCache sharedImageCache];}id<SDImageLoader> loader = [[self class] defaultImageLoader];if (!loader) {loader = [SDWebImageDownloader sharedDownloader];}return [self initWithCache:cache loader:loader];
}
可以看到SDWebImageManager將圖片下載和圖片緩存組合起來了,用法如下:
SDWebImageManager *manager = [SDWebImageManager sharedManager];[manager loadImageWithURL:(nullable NSURL *) options:(SDWebImageOptions) progress:^(NSInteger receivedSize, NSInteger expectedSize, NSURL * _Nullable targetURL) {} completed:^(UIImage * _Nullable image, NSData * _Nullable data, NSError * _Nullable error, SDImageCacheType cacheType, BOOL finished, NSURL * _Nullable imageURL) {}];
4.單獨使用SDWebImageDownloader異步下載圖片
使用SDWebImageDownloader可以異步下載圖片,但是圖片不會緩存到磁盤或內存
SDWebImageDownloader *downloader = [SDWebImageDownloader sharedDownloader];[downloader downloadImageWithURL:(nullable NSURL *) options:(SDWebImageDownloaderOptions) progress:^(NSInteger receivedSize, NSInteger expectedSize, NSURL * _Nullable targetURL) {} completed:^(UIImage * _Nullable image, NSData * _Nullable data, NSError * _Nullable error, BOOL finished) {}];
5.單獨使用SDImageCache異步緩存圖片
SDImageCache可以和SDWebImageDownloader一樣使用單例來緩存數據,支持內存緩存和異步的磁盤緩存
添加緩存:
[[SDImageCache sharedImageCache] storeImage:(nullable UIImage *) forKey:(nullable NSString *) completion:^{}];
默認情況下,圖片數據會同時緩存到內存和磁盤中,如果只想要內存緩存的話,可以使用下面的方法:
[[SDImageCache sharedImageCache] storeImage:image forKey:(nullable NSString *) toDisk:NO completion:^{}];
或者:
[[SDImageCache sharedImageCache] storeImageToMemory:(nullable UIImage *) forKey:(nullable NSString *)];
讀取緩存可以使用以下方法:
[[SDImageCache sharedImageCache] queryCacheOperationForKey:(nullable NSString *) done:^(UIImage * _Nullable image, NSData * _Nullable data, SDImageCacheType cacheType) {}];
實現原理
sd_setImageWithURL
我們查看sd_setImageWithURL方法是如何實現的可以發現,這個方法在UIImageView+WebCache文件中,并且這個文件中所有與這個方法類似的方法最后都會調用下面這個方法:
因此為Cell的UIImageView加載圖片的原理就藏在這個方法中,來看這個方法是怎么實現的:
- (nullable id<SDWebImageOperation>)sd_internalSetImageWithURL:(nullable NSURL *)urlplaceholderImage:(nullable UIImage *)placeholderoptions:(SDWebImageOptions)optionscontext:(nullable SDWebImageContext *)contextsetImageBlock:(nullable SDSetImageBlock)setImageBlockprogress:(nullable SDImageLoaderProgressBlock)progressBlockcompleted:(nullable SDInternalCompletionBlock)completedBlock {// 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 is NSString and shouldUseWeakMemoryCache is true, [cacheKeyForURL:context] will crash. just for a global protect.if ([url isKindOfClass:NSString.class]) {url = [NSURL URLWithString:(NSString *)url];//SDWeb允許傳入NSString類型}// Prevents app crashing on argument type error like sending NSNull instead of NSURLif (![url isKindOfClass:NSURL.class]) {url = nil;//防止不是URL類型導致崩潰}if (context) {// copy to avoid mutable objectcontext = [context copy];//創建副本以避免直接修改可變對象} else {context = [NSDictionary dictionary];//如果沒有提供上下文則創建一個空的字典作為上下文}NSString *validOperationKey = context[SDWebImageContextSetImageOperationKey];//嘗試從上下文中獲取鍵值if (!validOperationKey) {// pass through the operation key to downstream, which can used for tracing operation or image view classvalidOperationKey = NSStringFromClass([self class]);SDWebImageMutableContext *mutableContext = [context mutableCopy];mutableContext[SDWebImageContextSetImageOperationKey] = validOperationKey;context = [mutableContext copy];}//valid無效就以當前類名作為操作鍵創建一個self.sd_latestOperationKey = validOperationKey;//更新最新操作鍵if (!(SD_OPTIONS_CONTAINS(options, SDWebImageAvoidAutoCancelImage))) {// cancel previous loading for the same set-image operation key by default[self sd_cancelImageLoadOperationWithKey:validOperationKey];}//默認情況下,如果沒有設置SDWebImageAvoidAutoCancelImage選項,則取消與當前設置圖片操作鍵相關聯的所有先前的下載操作。 可以避免復用導致的問題SDWebImageLoadState *loadState = [self sd_imageLoadStateForKey:validOperationKey];//獲取或創建與當前操作鍵關聯的圖片加載狀態對象if (!loadState) {loadState = [SDWebImageLoadState new];}// 設置加載對象的url為當前的urlloadState.url = url;//將更新后的加載狀態對象與當前操作鍵關聯[self sd_setImageLoadState:loadState forKey:validOperationKey];// 從上下文中獲取圖片管理器,沒有就創建一個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];}BOOL shouldUseWeakCache = NO;if ([manager.imageCache isKindOfClass:SDImageCache.class]) {shouldUseWeakCache = ((SDImageCache *)manager.imageCache).config.shouldUseWeakMemoryCache;}if (!(options & SDWebImageDelayPlaceholder)) {//判斷是否顯示占位圖if (shouldUseWeakCache) {NSString *key = [manager cacheKeyForURL:url context:context];// call memory cache to trigger weak cache sync logic, ignore the return value and go on normal query// this unfortunately will cause twice memory cache query, but it's fast enough// in the future the weak cache feature may be re-design or removed[((SDImageCache *)manager.imageCache) imageFromMemoryCacheForKey:key];}dispatch_main_async_safe(^{//立即顯示占位圖[self sd_setImage:placeholder imageData:nil basedOnClassOrViaCustomSetImageBlock:setImageBlock cacheType:SDImageCacheTypeNone imageURL:url];});}id <SDWebImageOperation> operation = nil;if (url) {// reset the progress //重制進度追蹤NSProgress *imageProgress = loadState.progress;if (imageProgress) {imageProgress.totalUnitCount = 0;imageProgress.completedUnitCount = 0;}#if SD_UIKIT || SD_MAC// check and start image indicator[self sd_startImageIndicator];id<SDWebImageIndicator> imageIndicator = self.sd_imageIndicator;//啟動圖片加載小菊花
#endif//設置block回調,用于更新UI以及通知調用者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.0dispatch_async(dispatch_get_main_queue(), ^{[imageIndicator updateIndicatorProgress:progress];});}
#endif ? ? ?//調用外部進度回調if (progressBlock) {progressBlock(receivedSize, expectedSize, targetURL);}};//弱引用避免循環引用@weakify(self);//開始加載圖片operation = [manager loadImageWithURL:url options:options context:context progress:combinedProgressBlock completed:^(UIImage *image, NSData *data, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {@strongify(self);if (!self) { return; }// if the progress not been updated, mark it to complete stateif (imageProgress && finished && !error && imageProgress.totalUnitCount == 0 && imageProgress.completedUnitCount == 0) {imageProgress.totalUnitCount = SDWebImageProgressUnitCountUnknown;imageProgress.completedUnitCount = SDWebImageProgressUnitCountUnknown;} //將進度標記為完成狀態#if SD_UIKIT || SD_MAC// check and stop image indicator//讓小菊花停止if (finished) {[self sd_stopImageIndicator];}
#endif//決定是否調用完成回調BOOL shouldCallCompletedBlock = finished || (options & SDWebImageAvoidAutoSetImage);//決定是否設置圖片BOOL shouldNotSetImage = ((image && (options & SDWebImageAvoidAutoSetImage)) ||(!image && !(options & SDWebImageDelayPlaceholder)));SDWebImageNoParamsBlock callCompletedBlockClosure = ^{if (!self) { return; }if (!shouldNotSetImage) {[self sd_setNeedsLayout]; //設置圖片}if (completedBlock && shouldCallCompletedBlock) {completedBlock(image, data, error, cacheType, finished, url);}};// case 1a: we got an image, but the SDWebImageAvoidAutoSetImage flag is set// OR// case 1b: we got no image and the SDWebImageDelayPlaceholder is not set//Case 1a:下載成功,但設置了 不自動設置圖片//Case 1b:下載失敗,但設置了 不延遲占位圖(即立即顯示占位圖)//不自動設置 image,而是只調用 completedBlock。if (shouldNotSetImage) {dispatch_main_async_safe(callCompletedBlockClosure);return;}//下載成功,自動設置圖片或下載失敗,延遲顯示占位圖//使用下載圖或使用占位圖UIImage *targetImage = nil;NSData *targetData = nil;if (image) {// case 2a: we got an image and the SDWebImageAvoidAutoSetImage is not settargetImage = image;targetData = data;} else if (options & SDWebImageDelayPlaceholder) {// case 2b: we got no image and the SDWebImageDelayPlaceholder flag is settargetImage = placeholder;targetData = nil;}#if SD_UIKIT || SD_MAC// check whether we should use the image transition// 檢查是否應該使用圖片過渡效果。SDWebImageTransition *transition = nil;BOOL shouldUseTransition = NO;if (options & SDWebImageForceTransition) {// AlwaysshouldUseTransition = YES;} else if (cacheType == SDImageCacheTypeNone) {// From networkshouldUseTransition = YES;} else {// From disk (and, user don't use sync query)if (cacheType == SDImageCacheTypeMemory) {shouldUseTransition = NO;} else if (cacheType == SDImageCacheTypeDisk) {if (options & SDWebImageQueryMemoryDataSync || options & SDWebImageQueryDiskDataSync) {shouldUseTransition = NO;} else {shouldUseTransition = YES;}} else {// Not valid cache type, fallbackshouldUseTransition = NO;}}if (finished && shouldUseTransition) {transition = self.sd_imageTransition;}
#endifdispatch_main_async_safe(^{
#if SD_UIKIT || SD_MAC[self sd_setImage:targetImage imageData:targetData options:options basedOnClassOrViaCustomSetImageBlock:setImageBlock transition:transition cacheType:cacheType imageURL:imageURL callback:callCompletedBlockClosure];
#else[self sd_setImage:targetImage imageData:targetData basedOnClassOrViaCustomSetImageBlock:setImageBlock cacheType:cacheType imageURL:imageURL];callCompletedBlockClosure();
#endif});}];[self sd_setImageLoadOperation:operation forKey:validOperationKey];} else {// 如果url無效則立即停止小菊花
#if SD_UIKIT || SD_MAC[self sd_stopImageIndicator];
#endifif (completedBlock) {dispatch_main_async_safe(^{ // 設置回調返回錯誤NSError *error = [NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorInvalidURL userInfo:@{NSLocalizedDescriptionKey : @"Image url is nil"}];completedBlock(nil, nil, error, SDImageCacheTypeNone, YES, url);});}}return operation;
}
實現的流程我已經通過注釋進行了解釋,用自然語言將整個過程描述一遍的話就是:
先對URL預處理,以免類型錯誤,如果是NSString會自動轉換
準備上下文
context
,context
是一個配置字典,可以指定緩存策略、解碼器、下載器等。取消前一個請求,取消舊的下載任務
加載狀態綁定
從context中獲取圖片管理器SDWebImageManager
顯示占位圖
圖片加載開始,重制進度對象,啟動小菊花
啟動圖片下載,設置block更新小菊花,調用progresssBlock
下載完后根據不同的情況處理圖片
如果URL為空:停止小菊花,調用完成block并返回URL無效錯誤
loadImageWithURL
然后我們來看看loadImageWithURL是怎么實現的
- (SDWebImageCombinedOperation *)loadImageWithURL:(nullable NSURL *)urloptions:(SDWebImageOptions)optionscontext:(nullable SDWebImageContext *)contextprogress:(nullable SDImageLoaderProgressBlock)progressBlockcompleted:(nonnull SDInternalCompletionBlock)completedBlock {// Invoking this method without a completedBlock is pointlessNSAssert(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.//先檢查URL的類型if ([url isKindOfClass:NSString.class]) {url = [NSURL URLWithString:(NSString *)url];}
?// Prevents app crashing on argument type error like sending NSNull instead of NSURLif (![url isKindOfClass:NSURL.class]) {url = nil;}//創建一個新的操作用于管理這次加載SDWebImageCombinedOperation *operation = [SDWebImageCombinedOperation new];operation.manager = self;
?BOOL isFailedUrl = NO;if (url) {//當URL存在時,先檢查它是否在訪問失敗的url列表里SD_LOCK(_failedURLsLock);//加鎖防止多個線程訪問同一個資源isFailedUrl = [self.failedURLs containsObject:url];SD_UNLOCK(_failedURLsLock);}// Preprocess the options and context arg to decide the final the result for manager//預處理選項和上下文參數確定最終的結果SDWebImageOptionsResult *result = [self processedResultForURL:url options:options context:context];
?//如果url無效或是失敗的url沒有設置重試選項,立即調用完成回調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}] queue:result.context[SDWebImageContextCallbackQueue] url:url];return operation;//返回操作實例}
?//將當前操作添加到正在運行的操作列表中并進行加鎖保證線程安全SD_LOCK(_runningOperationsLock);[self.runningOperations addObject:operation];SD_UNLOCK(_runningOperationsLock);// Start the entry to load image from cache, the longest steps are below 啟動從緩存中加載圖片最長的流程如下// Steps without transformer: 沒有變換器的流程, 變換器指的是對圖像進行加工的工具// 1. query image from cache, miss 從緩存中查詢圖像, 如果緩存中沒有圖像// 2. download data and image 下載數據以及圖像// 3. store image to cache 并將其存儲到緩存中// Steps with transformer:// 1. query transformed image from cache, miss 從緩存中查詢已變換的圖像,如果沒有// 2. query original image from cache, miss 在緩存中查詢原始圖像, 如果沒有// 3. download data and image 下載數據與圖像// 4. do transform in CPU 在CPU中完成轉換操作// 5. store original image to cache 將原始圖像存儲到緩存中// 6. store transformed image to cache 將變換后的圖像存儲到緩存中[self callCacheProcessForOperation:operation url:url options:result.options context:result.context progress:progressBlock completed:completedBlock];
?return operation;
}
我們同樣使用自然語言描述一下整個流程:
URL類型檢驗與轉換
初始化加載操作對象,得到
CombinedOperation
,用來標識和管理這次加載任務判斷是否是“失敗 URL”,避免每次都去請求已經確定失敗的地址
生成處理結果對象,統一處理
options
和context
,確保后續所有調用用的是標準格式如果 URL 是空字符串,或者是黑名單 URL 且沒有設置重試選項,會直接調用
completedBlock
并返回錯誤加入運行中操作集合
調用方法callCacheProcessForOperation: url: options: context: progress: completed: 來決定緩存和下載策略并執行
callCacheProcessForOperation
接著我們看看callCacheProcessForOperation這個方法是如何實現的
- (void)callCacheProcessForOperation:(nonnull SDWebImageCombinedOperation *)operationurl:(nonnull NSURL *)urloptions:(SDWebImageOptions)optionscontext:(nullable SDWebImageContext *)contextprogress:(nullable SDImageLoaderProgressBlock)progressBlockcompleted:(nullable SDInternalCompletionBlock)completedBlock {// Grab the image cache to use//獲取需要查詢的緩存圖像,如果上下文中有則優先從上下文中獲取,否則就從當前類中獲取id<SDImageCache> imageCache = context[SDWebImageContextImageCache];if (!imageCache) {imageCache = self.imageCache;}// Get the query cache type//獲取緩存查詢類型,默認查詢所有類型的緩存(內存和磁盤)SDImageCacheType queryCacheType = SDImageCacheTypeAll;if (context[SDWebImageContextQueryCacheType]) {queryCacheType = [context[SDWebImageContextQueryCacheType] integerValue];}// Check whether we should query cache//檢查是否應該查詢緩存BOOL shouldQueryCache = !SD_OPTIONS_CONTAINS(options, SDWebImageFromLoaderOnly);if (shouldQueryCache) {// transformed cache key// 根據url與上下文生成緩存鍵NSString *key = [self cacheKeyForURL:url context:context];// to avoid the SDImageCache's sync logic use the mismatched cache key// we should strip the `thumbnail` related context//為了避免SDImageCache的同步邏輯使用不匹配的緩存鍵,我們需要移除與縮略圖相關的上下文SDWebImageMutableContext *mutableContext = [context mutableCopy];mutableContext[SDWebImageContextImageThumbnailPixelSize] = nil;mutableContext[SDWebImageContextImagePreserveAspectRatio] = nil;@weakify(operation);//查詢緩存的操作operation.cacheOperation = [imageCache queryImageForKey:key options:options context:mutableContext cacheType:queryCacheType completion:^(UIImage * _Nullable cachedImage, NSData * _Nullable cachedData, SDImageCacheType cacheType) {@strongify(operation);if (!operation || operation.isCancelled) {// 如果操作被取消或是不存在則返回錯誤// Image combined operation cancelled by user[self callCompletionBlockForOperation:operation completion:completedBlock error:[NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorCancelled userInfo:@{NSLocalizedDescriptionKey : @"Operation cancelled by user during querying the cache"}] queue:context[SDWebImageContextCallbackQueue] url:url];// 安全從運行操作列表中移除操作[self safelyRemoveOperationFromRunning:operation];return;} else if (!cachedImage) { //如果緩存中圖片不存在,再去查詢原始緩存NSString *originKey = [self originalCacheKeyForURL:url context:context];BOOL mayInOriginalCache = ![key isEqualToString:originKey];// Have a chance to query original cache instead of downloading, then applying transform// Thumbnail decoding is done inside SDImageCache's decoding part, which does not need post processing for transformif (mayInOriginalCache) {// 可能存在在原始緩存中,就用原始緩存查詢流程[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];}
}
同樣我們使用自然語言描述,可以對照源碼將步驟一一對應:
獲取要使用的緩存對象
確定要查詢的緩存類型
判斷是否需要查詢緩存,如果設置了
SDWebImageFromLoaderOnly
選項,就不查詢緩存,直接跳到下載流程構造緩存 key:會移除縮略圖尺寸等相關信息,避免 key 不一致導致查詢失敗
執行緩存查詢:
如果找到緩存圖像,就繼續進入下載或處理流程;
如果未找到:
會嘗試用原始緩存 key(未做圖像變換前的 key)再查一次(這是給例如縮略圖、變換圖保留原始圖緩存的情況);
如果原始 key 也沒找到,再進入下載流程。
queryImageForKey
在剛剛函數的實現中,有一行通過queryImageForKey來查詢緩存操作:
operation.cacheOperation = [imageCache queryImageForKey:key options:options context:mutableContext cacheType:queryCacheType completion:^(UIImage * _Nullable cachedImage, NSData * _Nullable cachedData, SDImageCacheType cacheType) {}];
queryImageForKey最后會調用queryCacheOperationForKey,我們來看看它是如何實現的:
- (nullable SDImageCacheToken *)queryCacheOperationForKey:(nullable NSString *)key options:(SDImageCacheOptions)options context:(nullable SDWebImageContext *)context cacheType:(SDImageCacheType)queryCacheType done:(nullable SDImageCacheQueryCompletionBlock)doneBlock {if (!key) {//如果緩存鍵為空,則立即完成回調if (doneBlock) {doneBlock(nil, nil, SDImageCacheTypeNone);}return nil;}// Invalid cache type//如果緩存類型為無也立即完成回調if (queryCacheType == SDImageCacheTypeNone) {if (doneBlock) {doneBlock(nil, nil, SDImageCacheTypeNone);}return nil;}// First check the in-memory cache...//首先檢查內存緩存UIImage *image;//如果查詢類型沒有要查詢磁盤, 則直接只查詢內存if (queryCacheType != SDImageCacheTypeDisk) {image = [self imageFromMemoryCacheForKey:key];}//如果找到了圖像if (image) {//只解碼第一幀保證圖片是靜態的if (options & SDImageCacheDecodeFirstFrameOnly) {// Ensure static imageif (image.sd_imageFrameCount > 1) {
#if SD_MACimage = [[NSImage alloc] initWithCGImage:image.CGImage scale:image.scale orientation:kCGImagePropertyOrientationUp];
#elseimage = [[UIImage alloc] initWithCGImage:image.CGImage scale:image.scale orientation:image.imageOrientation];
#endif}} else if (options & SDImageCacheMatchAnimatedImageClass) {// Check image class matchingClass animatedImageClass = image.class;Class desiredImageClass = context[SDWebImageContextAnimatedImageClass];if (desiredImageClass && ![animatedImageClass isSubclassOfClass:desiredImageClass]) {image = nil;}}}
?//檢查是否只需要查詢內存,只查詢內存的話之后立即回調,不再查詢磁盤BOOL shouldQueryMemoryOnly = (queryCacheType == SDImageCacheTypeMemory) || (image && !(options & SDImageCacheQueryMemoryData));if (shouldQueryMemoryOnly) {if (doneBlock) {doneBlock(image, nil, SDImageCacheTypeMemory);}return nil;}//接下來查詢磁盤緩存// Second check the disk cache...SDCallbackQueue *queue = context[SDWebImageContextCallbackQueue];SDImageCacheToken *operation = [[SDImageCacheToken alloc] initWithDoneBlock:doneBlock];operation.key = key;//用于查詢對象operation.callbackQueue = queue;//設置操作隊列// Check whether we need to synchronously query disk// 1. in-memory cache hit & memoryDataSync// 2. in-memory cache miss & diskDataSync//根據是否需要同步處理,選擇同步或異步查詢磁盤BOOL shouldQueryDiskSync = ((image && options & SDImageCacheQueryMemoryDataSync) ||(!image && options & SDImageCacheQueryDiskDataSync));//定義從磁盤查詢數據的BlockNSData* (^queryDiskDataBlock)(void) = ^NSData* {//定義Block,對取消操作進行加鎖@synchronized (operation) {if (operation.isCancelled) {return nil;}}//如果操作沒有被取消,從所有可能路徑中搜索數據return [self diskImageDataBySearchingAllPathsForKey:key];};//定義從磁盤創建圖像的BlockUIImage* (^queryDiskImageBlock)(NSData*) = ^UIImage*(NSData* diskData) {@synchronized (operation) {if (operation.isCancelled) {return nil;}}UIImage *diskImage;if (image) {// the image is from in-memory cache, but need image data//如果已經在內存中找到圖像,但是需要圖像數據diskImage = image;} else if (diskData) {BOOL shouldCacheToMemory = YES;if (context[SDWebImageContextStoreCacheType]) {//檢查是否應該將圖像緩存到內存中SDImageCacheType cacheType = [context[SDWebImageContextStoreCacheType] integerValue];shouldCacheToMemory = (cacheType == SDImageCacheTypeAll || cacheType == SDImageCacheTypeMemory);}//// 特殊情況:如果用戶查詢同一URL的圖像以避免多次解碼和寫入相同的圖像對象到磁盤緩存中,我們在這里再次查詢和檢查內存緩存// Special case: If user query image in list for the same URL, to avoid decode and write **same** image object into disk cache multiple times, we query and check memory cache here again.if (shouldCacheToMemory && self.config.shouldCacheImagesInMemory) {diskImage = [self.memoryCache objectForKey:key];}// decode image data only if in-memory cache missed//如果內存緩存未命中,解碼磁盤數據if (!diskImage) {diskImage = [self diskImageForKey:key data:diskData options:options context:context];// check if we need sync logicif (shouldCacheToMemory) {[self _syncDiskToMemoryWithImage:diskImage forKey:key];}}}return diskImage;};// Query in ioQueue to keep IO-safe// 用IO隊列保證IO操作安全// 同步執行磁盤查詢if (shouldQueryDiskSync) {__block NSData* diskData;__block UIImage* diskImage;dispatch_sync(self.ioQueue, ^{diskData = queryDiskDataBlock();diskImage = queryDiskImageBlock(diskData);});if (doneBlock) {doneBlock(diskImage, diskData, SDImageCacheTypeDisk);}} else {//異步執行查詢操作dispatch_async(self.ioQueue, ^{NSData* diskData = queryDiskDataBlock();UIImage* diskImage = queryDiskImageBlock(diskData);@synchronized (operation) {if (operation.isCancelled) {return;}}if (doneBlock) {[(queue ?: SDCallbackQueue.mainQueue) async:^{// Dispatch from IO queue to main queue need time, user may call cancel during the dispatch timing// This check is here to avoid double callback (one is from `SDImageCacheToken` in sync)@synchronized (operation) {if (operation.isCancelled) {return;}}doneBlock(diskImage, diskData, SDImageCacheTypeDisk);}];}});}return operation;
}
使用自然語言描述:
校驗key和緩存類型
嘗試查詢內存緩存:如果設置的
cacheType
不是.Disk
,就嘗試從內存中獲取圖片。如果找到了圖片:如果設置了只解碼第一幀(針對動圖),會將動圖轉成靜態圖。
如果設置了需要匹配特定圖片類(如動圖類),但類型不匹配,則丟棄這個圖片。
判斷是否只需要查詢內存
準備磁盤查詢操作
同步或異步執行磁盤查詢:如果設置了同步查詢磁盤的選項,則在 IO 隊列中同步讀取磁盤并執行回調;否則異步查詢:
異步從磁盤讀取數據并解碼成圖片。
最后將結果切回主線程或指定的回調隊列進行回調。
在讀取數據、解碼圖片和回調之前,都會判斷是否調用了取消操作(通過
operation.isCancelled
)來提前退出,避免多余工作。
可見這個方法的作用是:
根據指定的 key(緩存鍵),從內存或磁盤中查詢圖片緩存,并通過回調返回結果(UIImage 和 image data)。支持多種查詢選項,比如只查詢內存、是否解碼第一幀、是否匹配特定圖片類等。返回一個 SDImageCacheToken
,用于后續可能的取消操作。
callDownloadProcessForOperation
這個方法負責在緩存查找完成后,決定是否從網絡下載圖片并執行相關回調
- (void)callDownloadProcessForOperation:(nonnull SDWebImageCombinedOperation *)operationurl:(nonnull NSURL *)urloptions:(SDWebImageOptions)optionscontext:(SDWebImageContext *)contextcachedImage:(nullable UIImage *)cachedImagecachedData:(nullable NSData *)cachedDatacacheType:(SDImageCacheType)cacheTypeprogress:(nullable SDImageLoaderProgressBlock)progressBlockcompleted:(nullable SDInternalCompletionBlock)completedBlock {// Mark the cache operation end//標記緩存操作結束@synchronized (operation) {operation.cacheOperation = nil;}// Grab the image loader to use//獲取默認加載器id<SDImageLoader> imageLoader = context[SDWebImageContextImageLoader];if (!imageLoader) {imageLoader = self.imageLoader;}// Check whether we should download image from network//檢查是否需要從網上下載圖片BOOL shouldDownload = !SD_OPTIONS_CONTAINS(options, SDWebImageFromCacheOnly);shouldDownload &= (!cachedImage || options & SDWebImageRefreshCached);//如果需要刷新緩存或者緩存中沒有圖像shouldDownload &= (![self.delegate respondsToSelector:@selector(imageManager:shouldDownloadImageForURL:)] || [self.delegate imageManager:self shouldDownloadImageForURL:url]);//委托是否允許下載if ([imageLoader respondsToSelector:@selector(canRequestImageForURL:options:context:)]) {shouldDownload &= [imageLoader canRequestImageForURL:url options:options context:context];} else {shouldDownload &= [imageLoader canRequestImageForURL:url];}if (shouldDownload) {if (cachedImage && options & SDWebImageRefreshCached) {//找到圖像但是通知刷新緩存// If image was found in the cache but SDWebImageRefreshCached is provided, notify about the cached image// AND try to re-download it in order to let a chance to NSURLCache to refresh it from server.[self callCompletionBlockForOperation:operation completion:completedBlock image:cachedImage data:cachedData error:nil cacheType:cacheType finished:YES queue:context[SDWebImageContextCallbackQueue] url:url];// Pass the cached image to the image loader. The image loader should check whether the remote image is equal to the cached image.SDWebImageMutableContext *mutableContext;if (context) {mutableContext = [context mutableCopy];} else {mutableContext = [NSMutableDictionary dictionary];}mutableContext[SDWebImageContextLoaderCachedImage] = cachedImage;context = [mutableContext copy];}@weakify(operation);//發起圖像下載請求operation.loaderOperation = [imageLoader requestImageWithURL:url options:options context:context progress:progressBlock completed:^(UIImage *downloadedImage, NSData *downloadedData, NSError *error, BOOL finished) {@strongify(operation);if (!operation || operation.isCancelled) {//如果操作被取消返回錯誤信息// Image combined operation cancelled by user[self callCompletionBlockForOperation:operation completion:completedBlock error:[NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorCancelled userInfo:@{NSLocalizedDescriptionKey : @"Operation cancelled by user during sending the request"}] queue:context[SDWebImageContextCallbackQueue] url:url];} else if (cachedImage && options & SDWebImageRefreshCached && [error.domain isEqualToString:SDWebImageErrorDomain] && error.code == SDWebImageErrorCacheNotModified) {// Image refresh hit the NSURLCache cache, do not call the completion block} else if ([error.domain isEqualToString:SDWebImageErrorDomain] && error.code == SDWebImageErrorCancelled) {// Download operation cancelled by user before sending the request, don't block failed URL[self callCompletionBlockForOperation:operation completion:completedBlock error:error queue:context[SDWebImageContextCallbackQueue] url:url];} else if (error) {[self callCompletionBlockForOperation:operation completion:completedBlock error:error queue:context[SDWebImageContextCallbackQueue] url:url];BOOL shouldBlockFailedURL = [self shouldBlockFailedURLWithURL:url error:error options:options context:context];//向錯誤集合中添加當前錯誤if (shouldBlockFailedURL) {SD_LOCK(self->_failedURLsLock);[self.failedURLs addObject:url];SD_UNLOCK(self->_failedURLsLock);}} else {if ((options & SDWebImageRetryFailed)) {SD_LOCK(self->_failedURLsLock);[self.failedURLs removeObject:url];SD_UNLOCK(self->_failedURLsLock);}// Continue transform process//繼續圖像轉換流程,同時保存圖像到緩存中[self callTransformProcessForOperation:operation url:url options:options context:context originalImage:downloadedImage originalData:downloadedData cacheType:SDImageCacheTypeNone finished:finished completed:completedBlock];}if (finished) {//完成后在當前操作列表中移除當前操作[self safelyRemoveOperationFromRunning:operation];}}];} else if (cachedImage) {//如果不下載且緩存中有圖像,則使用緩存的圖像[self callCompletionBlockForOperation:operation completion:completedBlock image:cachedImage data:cachedData error:nil cacheType:cacheType finished:YES queue:context[SDWebImageContextCallbackQueue] url:url];[self safelyRemoveOperationFromRunning:operation];} else {//圖像不在緩存中,也不允許下載// Image not in cache and download disallowed by delegate[self callCompletionBlockForOperation:operation completion:completedBlock image:nil data:nil error:nil cacheType:SDImageCacheTypeNone finished:YES queue:context[SDWebImageContextCallbackQueue] url:url];[self safelyRemoveOperationFromRunning:operation];}
}
使用自然語言描述如下:
標記緩存操作已結束
獲取圖片加載器(imageLoader)
判斷是否需要下載圖片
如果需要下載,進入下載流程:
如果緩存中已有圖片且設置了刷新緩存(
RefreshCached
):先立即回調一次緩存圖像,讓用戶界面立即顯示;
同時繼續向服務器發起請求,用于刷新或確認是否真的有新內容;
把緩存圖像傳給加載器,用于與遠程圖像比較,避免重復下載。
發起下載請求
通過
imageLoader
執行網絡請求,傳入 URL、選項、上下文等;請求完成后會回調到一個
completed:
block。
處理下載完成回調:
如果下載被禁止,但已經命中緩存,則直接使用緩存圖像回調并移除任務
如果不下載也沒有緩存圖像,直接回調空圖像,表示整個請求失敗或被禁止,任務結束
storeImage
在執行完下載后,會繼續執行轉換與緩存處理,這里我們不關注轉換操作,將目光聚集到保存操作,保存操作的核心是storeImage,搜索storeImage可以看到它的實現:
將圖像存儲到內存緩存
這里判斷是否繼續存儲到磁盤,如果不需要存儲到磁盤,就調用完成回調并返回
這一段將數據存儲到磁盤中
setImage
下載成功后,經過重重回調,要回調的數據沿著SDWebImageDownloaderOperation->SDWebImageDownloader->SDWebImageManager->UIView+WebCache
一路流動,其中流動到SDWebImageManager
中時對圖片進行了緩存,最后在UIView+WebCache
中為UIImageView
設置了處理好的圖片。
可以在sd_internalSetImageWithURL
方法中看到,在更新一系列外部配置像圖片過度效果等后,會在主線程調用sd_setImage
更新UI
sd_setImage:
可以看到這里通過判斷類是button還是imageView來設置不同的設置方法
總結
由此總結一下SDWebImage的調用流程:
首先我們會進入setImagewithURL:方法中,然后進入sd-InternalmageWithURL方法中,在這個方法中我們首先通過validOperationKey取消正在運行的任務,任務是通過sd_cancelImageLoadOperationWithKey方法取消的,這一步是為了避免同一資源被重復下載,接著我們初始化SDWebManager(這里因為SDWebManager是單例,所以只初始化一次),接著進行一系列配置后調用loadImageWithURL方法,首先檢查URL是否在錯誤的集合中,如果沒有就調用queryImageForKey去查找緩存,查找緩存的步驟是首先查找內存緩存,內存緩存找不到再去查找磁盤緩存,都找不到則去查詢原始數據。如果都找不到我們就去執行下載操作,下載操作完成后通過storeImage方法將圖像存儲到緩存中,最后回到SDWebImageManager單例類中通過setImage方法將Image設置在對應的視圖上