SDWebImage 源碼(5.15.7)分析記錄
SDWebImage的整體的加載流程如下圖所示,源碼的分析也是基于這個流程。

【step 1】sd_setImageWithURL
由于SDWebImage 添加了UIImageView+WebCache分類,所以可以直接在UIImageView的實例上調(diào)用圖片加載方法, 分類里定義了一些sd_setImageWithURL相關(guān)的方法,最終這些方法都會調(diào)用到下面的方法
- (void)sd_setImageWithURL:(nullable NSURL *)url
placeholderImage:(nullable UIImage *)placeholder
options:(SDWebImageOptions)options
context:(nullable SDWebImageContext *)context
progress:(nullable SDImageLoaderProgressBlock)progressBlock
completed:(nullable SDExternalCompletionBlock)completedBlock {
// 這個方法是在`UIView+WebCache`分類內(nèi),這個分類里面定義了通用的一些屬性和方法,因為不只是UIImageView在使用,像UIButton也在使用
[self sd_internalSetImageWithURL:url
placeholderImage:placeholder
options:options
context:context
setImageBlock:nil
progress:progressBlock
completed:^(UIImage * _Nullable image, NSData * _Nullable data, NSError * _Nullable error, SDImageCacheType cacheType, BOOL finished, NSURL * _Nullable imageURL) {
if (completedBlock) {
completedBlock(image, error, cacheType, imageURL);
}
}];
}
注意點:
SDWebImageContext:這個類型是在5.0之后出現(xiàn)的,本質(zhì)就是一個字典,如果外部未提供的話,內(nèi)部會自動生成一個
-
SDExternalCompletionBlock: 外部完成回調(diào),相應的SDWebImage內(nèi)部使用的是SDInternalCompletionBlock,兩種回調(diào)的定義如下
typedef void(^SDExternalCompletionBlock)(UIImage * _Nullable image, NSError * _Nullable error, SDImageCacheType cacheType, NSURL * _Nullable imageURL); typedef void(^SDInternalCompletionBlock)(UIImage * _Nullable image, NSData * _Nullable data, NSError * _Nullable error, SDImageCacheType cacheType, BOOL finished, NSURL * _Nullable imageURL);
【step 2】sd_internalSetImageWithURL
該方法代碼存在于UIView+WebCache分類內(nèi),源碼及注釋如下所示
/// 給imageView設(shè)置圖片
/// @param url 圖片鏈接
/// @param placeholder 占位圖
/// @param options 圖片加載選項
/// @param context 配置項,NSDictionary
/// @param setImageBlock 設(shè)置圖片 Block(基于 UIImageView 方式調(diào)用的該參數(shù)一般為 nil)
/// @param progressBlock 圖片進度回調(diào)
/// @param completedBlock 圖片完成回調(diào)
- (nullable id<SDWebImageOperation>)sd_internalSetImageWithURL:(nullable NSURL *)url
placeholderImage:(nullable UIImage *)placeholder
options:(SDWebImageOptions)options
context:(nullable SDWebImageContext *)context
setImageBlock:(nullable SDSetImageBlock)setImageBlock
progress:(nullable SDImageLoaderProgressBlock)progressBlock
completed:(nullable SDInternalCompletionBlock)completedBlock
{
// 如果外部傳入context,則對context做copy操作,防止改變,否則內(nèi)部自動創(chuàng)建一個不可變的context
if (context) {
context = [context copy];
}
else {
context = [NSDictionary dictionary];
}
// 獲取OperationKey,在外部未傳入context時,第一次取出就是nil
NSString *validOperationKey = context[SDWebImageContextSetImageOperationKey];
// OperationKey = nil 進入下面的操作
if (!validOperationKey) {
// 對于UIImageView實例來說,這里的self就是UIImageView
validOperationKey = NSStringFromClass([self class]);
// mutableCopy -> 存儲 -> copy, 將OperationKey存入到context
SDWebImageMutableContext *mutableContext = [context mutableCopy];
mutableContext[SDWebImageContextSetImageOperationKey] = validOperationKey;
context = [mutableContext copy];
}
// 將key保存到`UIImageView+WebCache`中的sd_latestOperationKey關(guān)聯(lián)屬性中
self.sd_latestOperationKey = validOperationKey;
// 取消上一次OperationKey對應的下載
[self sd_cancelImageLoadOperationWithKey:validOperationKey];
// 將url保存到`UIImageView+WebCache`中的sd_imageURL關(guān)聯(lián)屬性中
self.sd_imageURL = url;
// 和validOperationKey類似,在外部未傳入context時,第一次取出時就是nil
SDWebImageManager *manager = context[SDWebImageContextCustomManager];
if (!manager) {
manager = [SDWebImageManager sharedManager];
}
else {
// 如果外部傳入manager,context里要刪除,避免內(nèi)存泄漏
// remove this manager to avoid retain cycle (manger -> loader -> operation -> context -> manager)
SDWebImageMutableContext *mutableContext = [context mutableCopy];
mutableContext[SDWebImageContextCustomManager] = nil;
context = [mutableContext copy];
}
// 是否使用內(nèi)存弱引用,5.12.0 之前默認是YES,之后是NO
BOOL shouldUseWeakCache = NO;
if ([manager.imageCache isKindOfClass:SDImageCache.class]) {
shouldUseWeakCache = ((SDImageCache *)manager.imageCache).config.shouldUseWeakMemoryCache;
}
// 默認是要顯示占位圖,如果設(shè)置了SDWebImageDelayPlaceholder,則不顯示占位圖
// ?? SDWebImageDelayPlaceholder可以當作圖片下載失敗的占位圖,而不是圖片下載過程中的占位圖,下載過程中不顯示,下載失敗時會顯示
if (!(options & SDWebImageDelayPlaceholder)) {
// ?? 觸發(fā)弱引用存儲,如下注釋所示此結(jié)構(gòu)可能要被刪除
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;
// url非空,進入正常邏輯處理,為空停止旋轉(zhuǎn)菊花,并觸發(fā)空圖片錯誤回調(diào)
if (url) {
// 獲取`UIImageView+WebCache`中的sd_imageProgress關(guān)聯(lián)屬性,并將進度重置
NSProgress *imageProgress = objc_getAssociatedObject(self, @selector(sd_imageProgress));
if (imageProgress) {
imageProgress.totalUnitCount = 0;
imageProgress.completedUnitCount = 0;
}
#if SD_UIKIT || SD_MAC
// 如果設(shè)置了菊花(指示器),開始旋轉(zhuǎn)菊花(默認是無菊花,未調(diào)用setSd_imageIndicator)
[self sd_startImageIndicator];
id<SDWebImageIndicator> imageIndicator = self.sd_imageIndicator;
#endif
// 設(shè)置圖片加載進度回調(diào)
// ?? receivedSize:已獲取的圖片大小,expectedSize:期望的圖片總大小
SDImageLoaderProgressBlock combinedProgressBlock = ^(NSInteger receivedSize, NSInteger expectedSize, NSURL *_Nullable targetURL) {
// imageProgress關(guān)聯(lián)屬性非空,設(shè)置相應的參數(shù)
if (imageProgress) {
imageProgress.totalUnitCount = expectedSize;
imageProgress.completedUnitCount = receivedSize;
}
#if SD_UIKIT || SD_MAC
// 如果菊花實現(xiàn)了`SDWebImageIndicator`協(xié)議中的updateIndicatorProgress方法,就更新進度
if ([imageIndicator respondsToSelector:@selector(updateIndicatorProgress:)]) {
// 獲取圖片完成的進度
double progress = 0;
if (expectedSize != 0) {
progress = (double)receivedSize / expectedSize;
}
// 進度是在0-1之前,
// ?? 異常情況下<0,取0,>1,取1
progress = MAX(MIN(progress, 1), 0); // 0.0 - 1.0
// 主線程更新菊花
dispatch_async(dispatch_get_main_queue(), ^{
[imageIndicator updateIndicatorProgress:progress];
});
}
#endif
// 圖片進度回調(diào)
if (progressBlock) {
progressBlock(receivedSize, expectedSize, targetURL);
}
};
//【step 3】: 獲取圖片加載operation
@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;
}
// 異常狀態(tài)下,在完成回調(diào)中將進度設(shè)置為100%
// if the progress not been updated, mark it to complete state
if (imageProgress && finished && !error && imageProgress.totalUnitCount == 0 && imageProgress.completedUnitCount == 0) {
imageProgress.totalUnitCount = SDWebImageProgressUnitCountUnknown;
imageProgress.completedUnitCount = SDWebImageProgressUnitCountUnknown;
}
#if SD_UIKIT || SD_MAC
// 回調(diào)完成,停止菊花
// check and stop image indicator
if (finished) {
[self sd_stopImageIndicator];
}
#endif
// finished = true,或 SDWebImageAvoidAutoSetImage(禁止自動設(shè)置圖片)
BOOL shouldCallCompletedBlock = finished || (options & SDWebImageAvoidAutoSetImage);
// 圖片存在且SDWebImageAvoidAutoSetImage,或者圖片不存在且未設(shè)置SDWebImageDelayPlaceholder
BOOL shouldNotSetImage = ((image && (options & SDWebImageAvoidAutoSetImage)) ||
(!image && !(options & SDWebImageDelayPlaceholder)));
SDWebImageNoParamsBlock callCompletedBlockClosure = ^{
if (!self) {
return;
}
if (!shouldNotSetImage) {
[self sd_setNeedsLayout];
}
// 已完成且回調(diào)存在
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
if (shouldNotSetImage) {
dispatch_main_async_safe(callCompletedBlockClosure);
return;
}
UIImage *targetImage = nil;
NSData *targetData = nil;
if (image) {
// 獲取了圖片且未設(shè)置`SDWebImageAvoidAutoSetImage`
// case 2a: we got an image and the SDWebImageAvoidAutoSetImage is not set
targetImage = image;
targetData = data;
}
else if (options & SDWebImageDelayPlaceholder) {
// 未獲取到圖片且設(shè)置了`SDWebImageDelayPlaceholder`,用占位圖當返回結(jié)果
// ?? 這里是`SDWebImageDelayPlaceholder`定義所描述的情形
// case 2b: we got no image and the SDWebImageDelayPlaceholder flag is set
targetImage = placeholder;
targetData = nil;
}
#if SD_UIKIT || SD_MAC
// 判斷是否需要轉(zhuǎn)場動畫
// check whether we should use the image transition
SDWebImageTransition *transition = nil;
BOOL shouldUseTransition = NO;
if (options & SDWebImageForceTransition) {
// 設(shè)置強制轉(zhuǎn)場SDWebImageForceTransition
// Always
shouldUseTransition = YES;
}
else if (cacheType == SDImageCacheTypeNone) {
// 從網(wǎng)絡(luò)中獲取也需要轉(zhuǎn)場
// From network
shouldUseTransition = YES;
}
else {
// 從內(nèi)存獲取不需要轉(zhuǎn)場,
// 如果從磁盤中獲取且設(shè)置SDWebImageQueryMemoryDataSync或SDWebImageQueryDiskDataSync 不需要轉(zhuǎn)場
// 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, fallback
shouldUseTransition = NO;
}
}
// 處理轉(zhuǎn)場回調(diào)
if (finished && shouldUseTransition) {
transition = self.sd_imageTransition;
}
#endif
// ?? 回到主線程并將圖片設(shè)置相應的視圖上
dispatch_main_async_safe(^{
#if SD_UIKIT || SD_MAC
[self sd_setImage:targetImage
imageData:targetData
basedOnClassOrViaCustomSetImageBlock:setImageBlock
transition:transition
cacheType:cacheType
imageURL:imageURL];
#else
[self sd_setImage:targetImage
imageData:targetData
basedOnClassOrViaCustomSetImageBlock:setImageBlock
cacheType:cacheType
imageURL:imageURL];
#endif
callCompletedBlockClosure();
});
}];
// ?? 將operation 保存到sd_operationDictionary內(nèi)
[self sd_setImageLoadOperation:operation forKey:validOperationKey];
}
else {
#if SD_UIKIT || SD_MAC
[self sd_stopImageIndicator];
#endif
if (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;
}
關(guān)于sd_cancelImageLoadOperationWithKey
該方法主要用于取消指定key對應的operation任務,相關(guān)代碼發(fā)如下:
- (void)sd_cancelImageLoadOperationWithKey:(nullable NSString *)key
{
if (key) {
// 獲取關(guān)聯(lián)對象sd_operationDictionary
// Cancel in progress downloader from queue
SDOperationsDictionary *operationDictionary = [self sd_operationDictionary];
id<SDWebImageOperation> operation;
// 同步獲取operation
@synchronized(self)
{
operation = [operationDictionary objectForKey:key];
}
if (operation) {
// 如果實現(xiàn)了`SDWebImageOperation`協(xié)議中的cancle,把operation任務取消掉
// ?? NSOperation 本身有cancel方法,sd又在分類 NSOperation (SDWebImageOperation)中遵守了SDWebImageOperation協(xié)議,分類中的cancel有效
if ([operation respondsToSelector:@selector(cancel)]) {
[operation cancel];
}
// 同步移除operation
@synchronized(self)
{
[operationDictionary removeObjectForKey:key];
}
}
}
}
關(guān)于sd_operationDictionary
它是WebCacheOperation分類中的一個關(guān)聯(lián)屬性,用NSMapTable來實現(xiàn),代碼如下
- (SDOperationsDictionary *)sd_operationDictionary
{
@synchronized(self)
{
SDOperationsDictionary *operations = objc_getAssociatedObject(self, @selector(sd_operationDictionary));
if (operations) {
return operations;
}
// 初始為空時會強制設(shè)置一個關(guān)聯(lián)對象
operations = [[NSMapTable alloc] initWithKeyOptions:NSPointerFunctionsStrongMemory valueOptions:NSPointerFunctionsWeakMemory capacity:0];
objc_setAssociatedObject(self, @selector(sd_operationDictionary), operations, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
return operations;
}
}
NSMapTable 是 NSDictionary 的通用版本。和 NSDictionary / NSMutableDictionary 不同的是,NSMapTable 具有下面這些特性:
- NSDictionary / NSMutableDictionary 對鍵進行拷貝,對值持有強引用。
- NSMapTable 是可變的,沒有不可變的對應版本。
- NSMapTable 可以持有鍵和值的弱引用,當鍵或者值當中的一個被釋放時,整個這一項就會被移除掉。
- NSMapTable 可以在加入成員時進行 copy 操作。
- NSMapTable 可以存儲任意的指針,通過指針來進行相等性和散列檢查。
注意: NSMapTable 專注于強引用和弱引用,意味著 Swift 中流行的值類型是不適用的,只能用于引用類型。
關(guān)于sd_setImageLoadOperation
- (void)sd_setImageLoadOperation:(nullable id<SDWebImageOperation>)operation forKey:(nullable NSString *)key
{
if (key) {
// ??刪除舊operation
[self sd_cancelImageLoadOperationWithKey:key];
if (operation) {
// 獲取關(guān)聯(lián)對象sd_operationDictionary,并將同步保存新operation
SDOperationsDictionary *operationDictionary = [self sd_operationDictionary];
@synchronized(self)
{
[operationDictionary setObject:operation forKey:key];
}
}
}
}
【step 3】loadimage(url, options, context, progressBiock, completedBlock)
該方法位于SDWebImageManager內(nèi),主要是做一些驗證的操作,其主要加載圖片的邏輯由callCacheProcessForOperation承擔
- (SDWebImageCombinedOperation *)loadImageWithURL:(nullable NSURL *)url
options:(SDWebImageOptions)options
context:(nullable SDWebImageContext *)context
progress:(nullable SDImageLoaderProgressBlock)progressBlock
completed:(nonnull SDInternalCompletionBlock)completedBlock
{
// 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.
// 如果外部傳入NSString,這里嘗試轉(zhuǎn)換成NSURL
if ([url isKindOfClass:NSString.class]) {
url = [NSURL URLWithString:(NSString *)url];
}
// Prevents app crashing on argument type error like sending NSNull instead of NSURL
// 判斷url是否合法
if (![url isKindOfClass:NSURL.class]) {
url = nil;
}
SDWebImageCombinedOperation *operation = [SDWebImageCombinedOperation new];
operation.manager = self;
BOOL isFailedUrl = NO;
if (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為長度為空或失敗過且未設(shè)置重試(SDWebImageRetryFailed)
if (url.absoluteString.length == 0 || (!(options & SDWebImageRetryFailed) && isFailedUrl)) {
NSString *description = isFailedUrl ? @"Image url is blacklisted" : @"Image url is nil";
NSInteger code = isFailedUrl ? SDWebImageErrorBlackListed : SDWebImageErrorInvalidURL;
// 觸發(fā)錯誤完成回調(diào),內(nèi)包含錯誤信息,這里的錯誤信息要么是圖片加載失敗要么是url為空
[self callCompletionBlockForOperation:operation completion:completedBlock error:[NSError errorWithDomain:SDWebImageErrorDomain code:code userInfo:@{NSLocalizedDescriptionKey : description}] queue:result.context[SDWebImageContextCallbackQueue] url:url];
return operation;
}
// 如果之前未加載過,將operation加到runningOperations中
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
// 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;
}
callCacheProcessForOperation真正的加載圖片的入口,具體代碼如下:
// Query normal cache process
- (void)callCacheProcessForOperation:(nonnull SDWebImageCombinedOperation *)operation
url:(nonnull NSURL *)url
options:(SDWebImageOptions)options
context:(nullable SDWebImageContext *)context
progress:(nullable SDImageLoaderProgressBlock)progressBlock
completed:(nullable SDInternalCompletionBlock)completedBlock
{
// Grab the image cache to use
// 如果context外部未傳入相應的SDImageCache,默認用imageCache (SDImageCache單例)
id<SDImageCache> imageCache = context[SDWebImageContextImageCache];
if (!imageCache) {
imageCache = self.imageCache;
}
// Get the query cache type
// 默認是緩存類型是SDImageCacheTypeAll,如果通過context傳入,則用外部的
SDImageCacheType queryCacheType = SDImageCacheTypeAll;
if (context[SDWebImageContextQueryCacheType]) {
queryCacheType = [context[SDWebImageContextQueryCacheType] integerValue];
}
// Check whether we should query cache
// 如果未設(shè)置SDWebImageFromLoaderOnly(直接網(wǎng)絡(luò)加載)那么就先查找緩存,否則直接網(wǎng)絡(luò)加載
BOOL shouldQueryCache = !SD_OPTIONS_CONTAINS(options, SDWebImageFromLoaderOnly);
if (shouldQueryCache) {
// transformed cache key
// 根據(jù)url生成相應的key
NSString *key = [self cacheKeyForURL:url context:context];
@weakify(operation);
// 【step 4】查找圖片
operation.cacheOperation = [imageCache queryImageForKey:key
options:options
context:context
cacheType:queryCacheType
completion:^(UIImage *_Nullable cachedImage, NSData *_Nullable cachedData, SDImageCacheType cacheType) {
@strongify(operation);
// operation不存在或取消,觸發(fā)錯誤完成回調(diào)
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) {
// 如果緩存圖片不存在,將當前傳入的key和根據(jù)urla獲取的key比較,如果相符,那么直接查找原始緩存好的圖片,否則重新下載圖片
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 transform
if (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];
}
}
【step 4】queryImage(key, options, completionBlock)
查詢圖片的方法位于SDImageCache文件內(nèi),是一個分類,主要用于查詢圖片之前的配置操作,具體的查詢操作由queryCacheOperationForKey來完成。具體代碼如下:
- (id<SDWebImageOperation>)queryImageForKey:(NSString *)key options:(SDWebImageOptions)options context:(nullable SDWebImageContext *)context cacheType:(SDImageCacheType)cacheType completion:(nullable SDImageCacheQueryCompletionBlock)completionBlock
{
// 獲取緩存相關(guān)的配置
SDImageCacheOptions cacheOptions = 0;
if (options & SDWebImageQueryMemoryData)
cacheOptions |= SDImageCacheQueryMemoryData;
if (options & SDWebImageQueryMemoryDataSync)
cacheOptions |= SDImageCacheQueryMemoryDataSync;
if (options & SDWebImageQueryDiskDataSync)
cacheOptions |= SDImageCacheQueryDiskDataSync;
if (options & SDWebImageScaleDownLargeImages)
cacheOptions |= SDImageCacheScaleDownLargeImages;
if (options & SDWebImageAvoidDecodeImage)
cacheOptions |= SDImageCacheAvoidDecodeImage;
if (options & SDWebImageDecodeFirstFrameOnly)
cacheOptions |= SDImageCacheDecodeFirstFrameOnly;
if (options & SDWebImagePreloadAllFrames)
cacheOptions |= SDImageCachePreloadAllFrames;
if (options & SDWebImageMatchAnimatedImageClass)
cacheOptions |= SDImageCacheMatchAnimatedImageClass;
return [self queryCacheOperationForKey:key options:cacheOptions context:context cacheType:cacheType done:completionBlock];
}
queryCacheOperationForKey源碼如下所示:
- (nullable SDImageCacheToken *)queryCacheOperationForKey:(nullable NSString *)key options:(SDImageCacheOptions)options context:(nullable SDWebImageContext *)context cacheType:(SDImageCacheType)queryCacheType done:(nullable SDImageCacheQueryCompletionBlock)doneBlock
{
// 處理key為nil的情況
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...
// 1、首先查找內(nèi)存緩存
UIImage *image;
if (queryCacheType != SDImageCacheTypeDisk) {
image = [self imageFromMemoryCacheForKey:key];
}
// 內(nèi)存中存在相應的圖片
if (image) {
// 是否只解碼第一幀
if (options & SDImageCacheDecodeFirstFrameOnly) {
// Ensure static image
// 如果image是動圖,那么只獲取其第一幀
if (image.sd_isAnimated) {
#if SD_MAC
image = [[NSImage alloc] initWithCGImage:image.CGImage scale:image.scale orientation:kCGImagePropertyOrientationUp];
#else
image = [[UIImage alloc] initWithCGImage:image.CGImage scale:image.scale orientation:image.imageOrientation];
#endif
}
}
else if (options & SDImageCacheMatchAnimatedImageClass) {
// Check image class matching
// 如果設(shè)置了圖片期望的類型,但是 image 的類與設(shè)置的類型不一致則將 image 置空
Class animatedImageClass = image.class;
Class desiredImageClass = context[SDWebImageContextAnimatedImageClass];
if (desiredImageClass && ![animatedImageClass isSubclassOfClass:desiredImageClass]) {
image = nil;
}
}
}
// 是否設(shè)置了只查詢內(nèi)存
// 緩存類型是SDImageCacheTypeMemory 或在圖片存在的情況下,未設(shè)置SDImageCacheQueryMemoryData(強制查詢圖片數(shù)據(jù))
BOOL shouldQueryMemoryOnly = (queryCacheType == SDImageCacheTypeMemory) || (image && !(options & SDImageCacheQueryMemoryData));
// 只查詢內(nèi)存的情況下,直接走完成回調(diào)
if (shouldQueryMemoryOnly) {
if (doneBlock) {
doneBlock(image, nil, SDImageCacheTypeMemory);
}
return nil;
}
// 2、查找磁盤緩存
// Second check the disk cache...
// queue是否經(jīng)context由外部傳入,這里可空
SDCallbackQueue *queue = context[SDWebImageContextCallbackQueue];
SDImageCacheToken *operation = [[SDImageCacheToken alloc] initWithDoneBlock:doneBlock];
operation.key = key;
operation.callbackQueue = queue;
// 判斷是否需要同步查找磁盤緩存
// 1. 內(nèi)存緩存命中且 SDImageCacheQueryMemoryDataSync
// 2. 內(nèi)存緩存未命中且 SDImageCacheQueryDiskDataSync
BOOL shouldQueryDiskSync = ((image && options & SDImageCacheQueryMemoryDataSync) ||
(!image && options & SDImageCacheQueryDiskDataSync));
// 查詢磁盤數(shù)據(jù)回調(diào),下同,只不過這里返回NSData, 下面返回UIImage
NSData * (^queryDiskDataBlock)(void) = ^NSData *
{
@synchronized(operation)
{
if (operation.isCancelled) {
return nil;
}
}
return [self diskImageDataBySearchingAllPathsForKey:key];
};
// 查詢磁盤圖片回調(diào)
UIImage * (^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
// 這里的圖片是從內(nèi)存中獲取的
diskImage = image;
}
else if (diskData) {
// 從磁盤中獲取的圖片默認要緩存到內(nèi)存中
BOOL shouldCacheToMomery = YES;
// 取出context中的緩存類型
if (context[SDWebImageContextStoreCacheType]) {
SDImageCacheType cacheType = [context[SDWebImageContextStoreCacheType] integerValue];
shouldCacheToMomery = (cacheType == SDImageCacheTypeAll || cacheType == SDImageCacheTypeMemory);
}
CGSize thumbnailSize = CGSizeZero;
// 取出context中的縮略圖大小,正常情況下是未傳入的這里可為空
NSValue *thumbnailSizeValue = context[SDWebImageContextImageThumbnailPixelSize];
if (thumbnailSizeValue != nil) {
#if SD_MAC
thumbnailSize = thumbnailSizeValue.sizeValue;
#else
thumbnailSize = thumbnailSizeValue.CGSizeValue;
#endif
}
// ??:縮略圖不應該回到內(nèi)存中,也就是不應該放在memoryCache里(這里的判斷有點不太懂)
if (thumbnailSize.width > 0 && thumbnailSize.height > 0) {
// Query full size cache key which generate a thumbnail, should not write back to full size memory cache
shouldCacheToMomery = NO;
}
// 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.
// 再次查詢內(nèi)存中是否存在
if (shouldCacheToMomery && self.config.shouldCacheImagesInMemory) {
diskImage = [self.memoryCache objectForKey:key];
}
// decode image data only if in-memory cache missed
// 如果內(nèi)存中不存在圖片,就利用diskData生成
if (!diskImage) {
diskImage = [self diskImageForKey:key data:diskData options:options context:context];
// 設(shè)置了內(nèi)存緩存 將圖片放入內(nèi)存中
if (shouldCacheToMomery && diskImage && self.config.shouldCacheImagesInMemory) {
NSUInteger cost = diskImage.sd_memoryCost;
[self.memoryCache setObject:diskImage forKey:key cost:cost];
}
}
}
return diskImage;
};
// Query in ioQueue to keep IO-safe
// 同步查詢
if (shouldQueryDiskSync) {
__block NSData *diskData;
__block UIImage *diskImage;
dispatch_sync(self.ioQueue, ^{
diskData = queryDiskDataBlock();
diskImage = queryDiskImageBlock(diskData);
});
// 獲取上步的data和image,放入doneBlock回調(diào)中
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 為外部傳入,未傳入的情況下用主隊列
[(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)
// 防止再次回調(diào)
@synchronized(operation)
{
if (operation.isCancelled) {
return;
}
}
doneBlock(diskImage, diskData, SDImageCacheTypeDisk);
}];
}
});
}
return operation;
}
【step 5】disk result
這里涉及到兩部分
- 從磁盤中查找的過程:【step 4】中的queryCacheOperationForKey方法
- 從網(wǎng)絡(luò)下載放入磁盤的過程:【step 8】中磁盤存儲過程
【step 6】requestImageWithURL(url, options, context, progressBlock, completedBlock)
下載代碼位于 SDWebImageDownloader內(nèi),requestImageWithURL具體代碼如下所示:
- (id<SDWebImageOperation>)requestImageWithURL:(NSURL *)url options:(SDWebImageOptions)options context:(SDWebImageContext *)context progress:(SDImageLoaderProgressBlock)progressBlock completed:(SDImageLoaderCompletedBlock)completedBlock
{
UIImage *cachedImage = context[SDWebImageContextLoaderCachedImage];
// 從options中獲取所有下載的配置
SDWebImageDownloaderOptions downloaderOptions = 0;
if (options & SDWebImageLowPriority)
downloaderOptions |= SDWebImageDownloaderLowPriority;
if (options & SDWebImageProgressiveLoad)
downloaderOptions |= SDWebImageDownloaderProgressiveLoad;
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 (options & SDWebImageAvoidDecodeImage)
downloaderOptions |= SDWebImageDownloaderAvoidDecodeImage;
if (options & SDWebImageDecodeFirstFrameOnly)
downloaderOptions |= SDWebImageDownloaderDecodeFirstFrameOnly;
if (options & SDWebImagePreloadAllFrames)
downloaderOptions |= SDWebImageDownloaderPreloadAllFrames;
if (options & SDWebImageMatchAnimatedImageClass)
downloaderOptions |= SDWebImageDownloaderMatchAnimatedImageClass;
if (cachedImage && options & SDWebImageRefreshCached) {
// force progressive off if image already cached but forced refreshing
downloaderOptions &= ~SDWebImageDownloaderProgressiveLoad;
// ignore image read from NSURLCache if image if cached but force refreshing
downloaderOptions |= SDWebImageDownloaderIgnoreCachedResponse;
}
// 下載圖片
return [self downloadImageWithURL:url options:downloaderOptions context:context progress:progressBlock completed:completedBlock];
}
如上代碼所示requestImageWithURL主要用來整合下載的配置,真正的下載方法由downloadImageWithURL承接
downloadImageWithURL
該方法主要用來配置下載圖片的operation并加入到下載隊列中,并返回可取消任務的token標識。
operation的類型是SDWebImageDownloaderOperation、SDWebImageDownloaderOperation也實現(xiàn)了SDWebImageDownloaderOperation協(xié)議,
SDWebImageDownloaderOperation 內(nèi)部有具體的圖片下載方法。
downloadImageWithURL具體代碼如下所示
- (nullable SDWebImageDownloadToken *)downloadImageWithURL:(nullable NSURL *)url
options:(SDWebImageDownloaderOptions)options
context:(nullable SDWebImageContext *)context
progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock
{
// The URL will be used as the key to the callbacks dictionary so it cannot be nil. If it is nil immediately call the completed block with no image or data.
// 判斷url是否為空,為空則直接完成回調(diào),并返回錯誤信息
if (url == nil) {
if (completedBlock) {
NSError *error = [NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorInvalidURL userInfo:@{ NSLocalizedDescriptionKey : @"Image url is nil" }];
completedBlock(nil, nil, error, YES);
}
return nil;
}
id downloadOperationCancelToken;
// When different thumbnail size download with same url, we need to make sure each callback called with desired size
// 如果context從外部傳入過濾器,直接使用過濾器查找cacheKey,否則使用url.absoluteString
id<SDWebImageCacheKeyFilter> cacheKeyFilter = context[SDWebImageContextCacheKeyFilter];
NSString *cacheKey;
if (cacheKeyFilter) {
cacheKey = [cacheKeyFilter cacheKeyForURL:url];
}
else {
cacheKey = url.absoluteString;
}
// 獲取解碼配置
SDImageCoderOptions *decodeOptions = SDGetDecodeOptionsFromContext(context, [self.class imageOptionsFromDownloaderOptions:options], cacheKey);
SD_LOCK(_operationsLock);
NSOperation<SDWebImageDownloaderOperation> *operation = [self.URLOperations objectForKey:url];
// There is a case that the operation may be marked as finished or cancelled, but not been removed from `self.URLOperations`.
BOOL shouldNotReuseOperation;
// operation存在的情況下,但是已經(jīng)完成或被取消,那么不應該重用,
if (operation) {
@synchronized(operation)
{
shouldNotReuseOperation = operation.isFinished || operation.isCancelled || SDWebImageDownloaderOperationGetCompleted(operation);
}
}
else {
shouldNotReuseOperation = YES;
}
//
if (shouldNotReuseOperation) {
// 根據(jù)url創(chuàng)建operation
operation = [self createDownloaderOperationWithUrl:url options:options context:context];
// 創(chuàng)建失敗直接完成回調(diào),并返回錯誤信息
if (!operation) {
SD_UNLOCK(_operationsLock);
if (completedBlock) {
NSError *error = [NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorInvalidDownloadOperation userInfo:@{ NSLocalizedDescriptionKey : @"Downloader operation is nil" }];
completedBlock(nil, nil, error, YES);
}
return nil;
}
@weakify(self);
operation.completionBlock = ^{
@strongify(self);
if (!self) {
return;
}
// 回調(diào)完成移除URLOperations內(nèi)的operation
SD_LOCK(self->_operationsLock);
[self.URLOperations removeObjectForKey:url];
SD_UNLOCK(self->_operationsLock);
};
// 將operation添加到URLOperations內(nèi)
[self.URLOperations setObject:operation forKey:url];
// Add the handlers before submitting to operation queue, avoid the race condition that operation finished before setting handlers.
// 在operation添加到downloadQueue之前 添加handler,防止在設(shè)置handler之前operation完成了
downloadOperationCancelToken = [operation addHandlersForProgress:progressBlock completed:completedBlock decodeOptions:decodeOptions];
// Add operation to operation queue only after all configuration done according to Apple's doc.
// `addOperation:` does not synchronously execute the `operation.completionBlock` so this will not cause deadlock.
// 將operation加入到下載隊列,并在合適的時機啟動任務
// ?? addOperation不會同步執(zhí)行operation.completionBlock,不會引用死鎖
[self.downloadQueue addOperation:operation];
}
else {
// When we reuse the download operation to attach more callbacks, there may be thread safe issue because the getter of callbacks may in another queue (decoding queue or delegate queue)
// So we lock the operation here, and in `SDWebImageDownloaderOperation`, we use `@synchonzied (self)`, to ensure the thread safe between these two classes.
@synchronized(operation)
{
downloadOperationCancelToken = [operation addHandlersForProgress:progressBlock completed:completedBlock decodeOptions:decodeOptions];
}
}
SD_UNLOCK(_operationsLock);
// 返回token,外部獲取之后可以cancel任務
SDWebImageDownloadToken *token = [[SDWebImageDownloadToken alloc] initWithDownloadOperation:operation];
token.url = url;
token.request = operation.request;
token.downloadOperationCancelToken = downloadOperationCancelToken;
return token;
}
【step 7】network result
圖片下載由SDWebImageDownloaderOperation中的URLSession承接,具體代碼如下所示:
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error
{
// If we already cancel the operation or anything mark the operation finished, don't callback twice
// operation 如果完成直接返回,不再回調(diào)
if (self.isFinished)
return;
NSArray<SDWebImageDownloaderOperationToken *> *tokens;
@synchronized(self)
{
tokens = [self.callbackTokens copy];
self.dataTask = nil;
__block typeof(self) strongSelf = self;
// 下載完成發(fā)送 停止和錯誤(有的話) 通知
dispatch_async(dispatch_get_main_queue(), ^{
[[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStopNotification object:strongSelf];
if (!error) {
[[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadFinishNotification object:strongSelf];
}
});
}
// make sure to call `[self done]` to mark operation as finished
if (error) {
// custom error instead of URLSession error
if (self.responseError) {
error = self.responseError;
}
// 錯誤回調(diào)
[self callCompletionBlocksWithError:error];
// 標記下載結(jié)束
[self done];
}
else {
// 如果有下載tokens不能為空
if (tokens.count > 0) {
NSData *imageData = self.imageData;
self.imageData = nil;
// data decryptor
// 解析數(shù)據(jù),這里基本上base64解密
if (imageData && self.decryptor) {
imageData = [self.decryptor decryptedDataWithData:imageData response:self.response];
}
// 數(shù)據(jù)存在進入后續(xù)處理
if (imageData) {
/** if you specified to only use cached data via `SDWebImageDownloaderIgnoreCachedResponse`,
* then we should check if the cached data is equal to image data
*/
// 如果配置了忽略緩存,首先檢查緩存數(shù)據(jù)是不是和下載數(shù)據(jù)一致,如果是的話,調(diào)用錯誤回調(diào),標記結(jié)束
if (self.options & SDWebImageDownloaderIgnoreCachedResponse && [self.cachedData isEqualToData:imageData]) {
self.responseError = [NSError errorWithDomain:SDWebImageErrorDomain
code:SDWebImageErrorCacheNotModified
userInfo:@{ NSLocalizedDescriptionKey : @"Downloaded image is not modified and ignored",
SDWebImageErrorDownloadResponseKey : self.response }];
// call completion block with not modified error
[self callCompletionBlocksWithError:self.responseError];
[self done];
}
else {
// decode the image in coder queue, cancel all previous decoding process
// 在coderQueue中解碼圖片,首先清空所有操作
[self.coderQueue cancelAllOperations];
@weakify(self);
// 存在多個token,進行遍歷,在5.8.3的版本只處理了一個token
for (SDWebImageDownloaderOperationToken *token in tokens) {
[self.coderQueue addOperationWithBlock:^{
@strongify(self);
if (!self) {
return;
}
UIImage *image;
// check if we already decode this variant of image for current callback
// 從imageMap(NSMaptable)中根據(jù)decodeOptions獲取圖片,如果第一次下載,肯定是空的
if (token.decodeOptions) {
image = [self.imageMap objectForKey:token.decodeOptions];
}
// 如果未在imageMap取到圖上,根據(jù)imageData生成圖片
if (!image) {
// check if we already use progressive decoding, use that to produce faster decoding
// 分段下載解碼器
id<SDProgressiveImageCoder> progressiveCoder = SDImageLoaderGetProgressiveCoder(self);
// 下載配置
SDWebImageOptions options = [[self class] imageOptionsFromDownloaderOptions:self.options];
SDWebImageContext *context;
if (token.decodeOptions) {
SDWebImageMutableContext *mutableContext = [NSMutableDictionary dictionaryWithDictionary:self.context];
// 將解碼配置放入到context中
SDSetDecodeOptionsToContext(mutableContext, &options, token.decodeOptions);
context = [mutableContext copy];
}
else {
context = self.context;
}
// 分段下載解碼圖片
if (progressiveCoder) {
image = SDImageLoaderDecodeProgressiveImageData(imageData, self.request.URL, YES, self, options, context);
}
// 普通下載解碼圖片
else {
image = SDImageLoaderDecodeImageData(imageData, self.request.URL, options, context);
}
// 將圖片放入imageMap保存
if (image && token.decodeOptions) {
[self.imageMap setObject:image forKey:token.decodeOptions];
}
}
CGSize imageSize = image.size;
if (imageSize.width == 0 || imageSize.height == 0) {
// 圖片寬高不正確,調(diào)用完成回調(diào),并返回錯誤信息
NSString *description = image == nil ? @"Downloaded image decode failed" : @"Downloaded image has 0 pixels";
NSError *error = [NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorBadImageData userInfo:@{NSLocalizedDescriptionKey : description}];
[self callCompletionBlockWithToken:token image:nil imageData:nil error:error finished:YES];
}
else {
// 調(diào)用完成回調(diào)
[self callCompletionBlockWithToken:token image:image imageData:imageData error:nil finished:YES];
}
}];
}
// call [self done] after all completed block was dispatched
dispatch_block_t doneBlock = ^{
@strongify(self);
if (!self) {
return;
}
[self done];
};
// 所有的token處理完成之后,再處理完成任務doneBlock
if (@available(iOS 13, tvOS 13, macOS 10.15, watchOS 6, *)) {
// seems faster than `addOperationWithBlock`
[self.coderQueue addBarrierBlock:doneBlock];
}
else {
// serial queue, this does the same effect in semantics
[self.coderQueue addOperationWithBlock:doneBlock];
}
}
}
else {
// 數(shù)據(jù)不存在,完成回調(diào),直接標記結(jié)束
[self callCompletionBlocksWithError:[NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorBadImageData userInfo:@{ NSLocalizedDescriptionKey : @"Image data is nil" }]];
[self done];
}
}
else {
// tokens為空, 表示沒有下載,直接標記結(jié)束
[self done];
}
}
}
圖片解析完成之后會調(diào)用完成回調(diào),具體如下:
callCompletionBlockWithToken
- (void)callCompletionBlockWithToken:(nonnull SDWebImageDownloaderOperationToken *)token
image:(nullable UIImage *)image
imageData:(nullable NSData *)imageData
error:(nullable NSError *)error
finished:(BOOL)finished
{
SDWebImageDownloaderCompletedBlock completedBlock = token.completedBlock;
if (completedBlock) {
SDCallbackQueue *queue = self.context[SDWebImageContextCallbackQueue];
[(queue ?: SDCallbackQueue.mainQueue) async:^{
completedBlock(image, imageData, error, finished);
}];
}
}
【step 8】store(image, imageData, key, toDisk completionBlock)
- (void)storeImage:(nullable UIImage *)image
imageData:(nullable NSData *)imageData
forKey:(nullable NSString *)key
options:(SDWebImageOptions)options
context:(nullable SDWebImageContext *)context
cacheType:(SDImageCacheType)cacheType
completion:(nullable SDWebImageNoParamsBlock)completionBlock
{
// 圖片且圖片數(shù)據(jù)不存在 或者 key不存在,直接調(diào)用完成回調(diào)
if ((!image && !imageData) || !key) {
if (completionBlock) {
completionBlock();
}
return;
}
// 緩存類型是SDImageCacheTypeMemory或者SDImageCacheTypeAll,需要存儲到內(nèi)存中
BOOL toMemory = cacheType == SDImageCacheTypeMemory || cacheType == SDImageCacheTypeAll;
// 緩存類型是SDImageCacheTypeDisk或者SDImageCacheTypeAll,需要存儲到磁盤中
BOOL toDisk = cacheType == SDImageCacheTypeDisk || cacheType == SDImageCacheTypeAll;
// if memory cache is enabled
// 存儲到內(nèi)存中
if (image && toMemory && self.config.shouldCacheImagesInMemory) {
NSUInteger cost = image.sd_memoryCost;
[self.memoryCache setObject:image forKey:key cost:cost];
}
// 如果未設(shè)置存儲到磁盤中,直接調(diào)用完成回調(diào)
if (!toDisk) {
if (completionBlock) {
completionBlock();
}
return;
}
NSData *data = imageData;
if (!data && [image respondsToSelector:@selector(animatedImageData)]) {
// If image is custom animated image class, prefer its original animated data
data = [((id<SDAnimatedImage>)image)animatedImageData];
}
SDCallbackQueue *queue = context[SDWebImageContextCallbackQueue];
// data不存在但是image存在
if (!data && image) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
// Check image's associated image format, may return .undefined
// 處理圖片格式
SDImageFormat format = image.sd_imageFormat;
if (format == SDImageFormatUndefined) {
// If image is animated, use GIF (APNG may be better, but has bugs before macOS 10.14)
// gif格式
if (image.sd_isAnimated) {
format = SDImageFormatGIF;
}
else {
// If we do not have any data to detect image format, check whether it contains alpha channel to use PNG or JPEG format
format = [SDImageCoderHelper CGImageContainsAlpha:image.CGImage] ? SDImageFormatPNG : SDImageFormatJPEG;
}
}
NSData *data = [[SDImageCodersManager sharedManager] encodedDataWithImage:image format:format options:context[SDWebImageContextImageEncodeOptions]];
dispatch_async(self.ioQueue, ^{
[self _storeImageDataToDisk:data forKey:key];
[self _archivedDataWithImage:image forKey:key];
if (completionBlock) {
[(queue ?: SDCallbackQueue.mainQueue) async:^{
completionBlock();
}];
}
});
});
}
else {
// data存在的情況下,將圖片存儲到磁盤中
dispatch_async(self.ioQueue, ^{
[self _storeImageDataToDisk:data forKey:key];
[self _archivedDataWithImage:image forKey:key];
if (completionBlock) {
[(queue ?: SDCallbackQueue.mainQueue) async:^{
completionBlock();
}];
}
});
}
}
【step 9】Image
- (void)callCompletionBlockForOperation:(nullable SDWebImageCombinedOperation *)operation
completion:(nullable SDInternalCompletionBlock)completionBlock
image:(nullable UIImage *)image
data:(nullable NSData *)data
error:(nullable NSError *)error
cacheType:(SDImageCacheType)cacheType
finished:(BOOL)finished
queue:(nullable SDCallbackQueue *)queue
url:(nullable NSURL *)url
{
if (completionBlock) {
[(queue ?: SDCallbackQueue.mainQueue) async:^{
completionBlock(image, data, error, cacheType, finished, url);
}];
}
}
【step 10】set image
設(shè)置圖片的方法
sd_setImage:mageData:basedOnClassOrViaCustomSetImageBlock:transition:cacheType:imageURL:會在【step 1】中的loadImageWithURL中的回調(diào)中調(diào)用。
該方法主要是將圖片顯示到視圖上,并處理轉(zhuǎn)場動畫,部分代碼如下:
- (void)sd_setImage:(UIImage *)image imageData:(NSData *)imageData basedOnClassOrViaCustomSetImageBlock:(SDSetImageBlock)setImageBlock transition:(SDWebImageTransition *)transition cacheType:(SDImageCacheType)cacheType imageURL:(NSURL *)imageURL
{
UIView *view = self;
SDSetImageBlock finalSetImageBlock;
// 正常情況下是不會設(shè)置setImageBlock回調(diào),所以這里不會走,如果外部設(shè)置了,會走這里
if (setImageBlock) {
finalSetImageBlock = setImageBlock;
}
else if ([view isKindOfClass:[UIImageView class]]) {
// 將圖片設(shè)置到UIImageView上
UIImageView *imageView = (UIImageView *)view;
finalSetImageBlock = ^(UIImage *setImage, NSData *setImageData, SDImageCacheType setCacheType, NSURL *setImageURL) {
imageView.image = setImage;
};
}
#if SD_UIKIT
else if ([view isKindOfClass:[UIButton class]]) {
// 將圖片設(shè)置到UIButton上
UIButton *button = (UIButton *)view;
finalSetImageBlock = ^(UIImage *setImage, NSData *setImageData, SDImageCacheType setCacheType, NSURL *setImageURL) {
[button setImage:setImage forState:UIControlStateNormal];
};
}
#endif
#if SD_MAC
else if ([view isKindOfClass:[NSButton class]]) {
NSButton *button = (NSButton *)view;
finalSetImageBlock = ^(UIImage *setImage, NSData *setImageData, SDImageCacheType setCacheType, NSURL *setImageURL) {
button.image = setImage;
};
}
#endif
#if SD_MAC
else if ([view isKindOfClass:[NSButton class]]) {
NSButton *button = (NSButton *)view;
finalSetImageBlock = ^(UIImage *setImage, NSData *setImageData, SDImageCacheType setCacheType, NSURL *setImageURL) {
button.image = setImage;
};
}
// 轉(zhuǎn)場動畫
if (transition) {
////
省略
////
}
}
相關(guān)的幾個類解析
【SDImageCacheConfig】
SDImageCacheConfig 主要是用于緩存配置的類,用于`SDDiskCache`和`SDMemoryCache`兩個類中
默認的配置中磁盤最大過期時間kDefaultCacheMaxDiskAge = 1周
默認緩存到內(nèi)存中,不使用weak內(nèi)存緩存等等,內(nèi)存緩存類使用的是SDMemoryCache,磁盤緩存類使用的是SDDiskCache
部分默認配置代碼如下
_shouldDisableiCloud = YES;
_shouldCacheImagesInMemory = YES;
_shouldUseWeakMemoryCache = NO;
_shouldRemoveExpiredDataWhenEnterBackground = YES;
_shouldRemoveExpiredDataWhenTerminate = YES;
_diskCacheReadingOptions = 0;
_diskCacheWritingOptions = NSDataWritingAtomic;
_maxDiskAge = kDefaultCacheMaxDiskAge;
_maxDiskSize = 0;
_diskCacheExpireType = SDImageCacheConfigExpireTypeModificationDate;
_fileManager = nil;
_ioQueueAttributes = DISPATCH_QUEUE_SERIAL; // NULL
_memoryCacheClass = [SDMemoryCache class];
_diskCacheClass = [SDDiskCache class];
【SDMemoryCache】
SDMemoryCache 繼承自NSCache,同時擁有了它的優(yōu)點
- 1、收到內(nèi)存警告時自動刪減緩存
- 2、線程安全
- 3、優(yōu)先刪減“最久未使用”的對象
- 4、NSCache不會拷貝鍵 ,而是強引用鍵。
SDMemoryCache 額外做的一點是加了自己的弱引用緩存,即使用NSMapTable weakCache,weakCache對值的引用是weak,而NSCache是強引用。
當使用了weakCache,在內(nèi)存吃緊時需要自己手動處理,需要監(jiān)聽 UIApplicationDidReceiveMemoryWarningNotification通知。
另外一點,使用weakCache,需要監(jiān)聽SDImageCacheConfig的 maxMemoryCost和maxMemoryCount,將匹配NSCache的屬性 totalCostLimit 和countLimit。
相關(guān)代碼如下
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey, id> *)change context:(void *)context
{
if (context == SDMemoryCacheContext) {
if ([keyPath isEqualToString:NSStringFromSelector(@selector(maxMemoryCost))]) {
self.totalCostLimit = self.config.maxMemoryCost;
}
else if ([keyPath isEqualToString:NSStringFromSelector(@selector(maxMemoryCount))]) {
self.countLimit = self.config.maxMemoryCount;
}
}
else {
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}
}
【SDDiskCache】
SDDiskCache主要作用是將圖上緩存到磁盤上,當然它也有檢查緩存大小和刪除過期緩存的功能。磁盤緩存默認是一周,也可以手動配置。緩存到磁盤上的圖片名是采用了MD5摘要算法,最終的結(jié)果是將后綴之前的部分利用MD5處理然后再拼接后綴。具體如下:
static inline NSString *_Nonnull SDDiskCacheFileNameForKey(NSString *_Nullable key)
{
const char *str = key.UTF8String;
if (str == NULL) {
str = "";
}
unsigned char r[CC_MD5_DIGEST_LENGTH];
CC_MD5(str, (CC_LONG)strlen(str), r);
NSURL *keyURL = [NSURL URLWithString:key];
NSString *ext = keyURL ? keyURL.pathExtension : key.pathExtension;
// File system has file name length limit, we need to check if ext is too long, we don't add it to the filename
if (ext.length > SD_MAX_FILE_EXTENSION_LENGTH) {
ext = nil;
}
NSString *filename = [NSString stringWithFormat:@"%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%@",
r[0], r[1], r[2], r[3], r[4], r[5], r[6], r[7], r[8], r[9], r[10],
r[11], r[12], r[13], r[14], r[15], ext.length == 0 ? @"" : [NSString stringWithFormat:@".%@", ext]];
return filename;
}
【SDWebImageDownloaderConfig】
這里存儲圖片下載的配置,默認情況下最大并發(fā)數(shù)是6,超時時間是15s,任務執(zhí)行的順序是FIFO,有效的狀態(tài)碼是200-300,內(nèi)部還有一些其它的配置,比如用戶名和密碼等
_maxConcurrentDownloads = 6;
_downloadTimeout = 15.0;
_executionOrder = SDWebImageDownloaderFIFOExecutionOrder;
_acceptableStatusCodes = [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(200, 100)];
總結(jié)
- 采用分類的形式,可以通過點語法應用到相應的控件上,比如UIImageView
- 分類內(nèi)的屬性通過關(guān)聯(lián)方法來添加對應的變量,比如:UIView (WebCache)中的
sd_imageURL屬性 - 將接口和實現(xiàn)分開,比如 定義了一個
SDWebImageOperation協(xié)議,而具體的細節(jié)由SDWebImageCombinedOperation或其它類實現(xiàn),其它還有像SDMemoryCache,SDImageLoader等