【iOS】SDWebImage第三方库源码学习笔记

前言

之前在写项目时,经常用到SDWebImage这个第三方库来加载图片,并且了解到了这个第三方库在处理图片时自带异步下载和缓存功能,以及对cell复用的处理。这篇文章来系统学习一下SDWebImage第三方库的知识以及底层原理

简介

SDWebImageUIImageViewUIButton提供了下载分类,可以很简单地实现图片异步下载与缓存功能。SDWebImage的第三方库具有以下特性

  1. 异步下载图片

  2. 异步缓存(内存+磁盘),自动管理缓存有效性

  3. 同一个URL不会重复下载

  4. 自动识别无效URL,不会反复重试

  5. 不阻塞主线程

  6. 使用GCD与ARC

用法

1.在UITableView中使用UIImageView+WebCache

UITabelViewCell 中的 UIImageView 控件直接调用 sd_setImageWithURL: placeholderImage:方法即可

2.使用回调Blocks

objectivec 复制代码
[listTableViewCell.sightsImageView sd_setImageWithURL:(nullable NSURL *) completed:^(UIImage * _Nullable image, NSError * _Nullable error, SDImageCacheType cacheType, NSURL * _Nullable imageURL) {
            NSLog(@"回调");
        }];

3.使用SDWebImageManager单例类

SDWebImage是一个单例类,也是SDWebImage库中的核心类,负责下载与缓存的处理

objectivec 复制代码
+ (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将图片下载和图片缓存组合起来了,用法如下:

objectivec 复制代码
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可以异步下载图片,但是图片不会缓存到磁盘或内存

objectivec 复制代码
 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一样使用单例来缓存数据,支持内存缓存和异步的磁盘缓存

添加缓存:

objectivec 复制代码
[[SDImageCache sharedImageCache] storeImage:(nullable UIImage *) forKey:(nullable NSString *) completion:^{
        
    }];

默认情况下,图片数据会同时缓存到内存和磁盘中,如果只想要内存缓存的话,可以使用下面的方法:

objectivec 复制代码
[[SDImageCache sharedImageCache] storeImage:image forKey:(nullable NSString *) toDisk:NO completion:^{
        
    }];

或者:

objectivec 复制代码
[[SDImageCache sharedImageCache] storeImageToMemory:(nullable UIImage *) forKey:(nullable NSString *)];

读取缓存可以使用以下方法:

objectivec 复制代码
[[SDImageCache sharedImageCache] queryCacheOperationForKey:(nullable NSString *) done:^(UIImage * _Nullable image, NSData * _Nullable data, SDImageCacheType cacheType) {
        
    }];

实现原理

sd_setImageWithURL

我们查看sd_setImageWithURL方法是如何实现的可以发现,这个方法在UIImageView+WebCache文件中,并且这个文件中所有与这个方法类似的方法最后都会调用下面这个方法:

因此为Cell的UIImageView加载图片的原理就藏在这个方法中,来看这个方法是怎么实现的:

objectivec 复制代码
- (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 {
    
    // 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 NSURL
    if (![url isKindOfClass:NSURL.class]) {
        url = nil;//防止不是URL类型导致崩溃
    }
    
    if (context) {
        // copy to avoid mutable object
        context = [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 class
        validOperationKey = 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为当前的url
    loadState.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.0
                dispatch_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 state
            if (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 set
                targetImage = image;
                targetData = data;
            } else if (options & SDWebImageDelayPlaceholder) {
                // case 2b: we got no image and the SDWebImageDelayPlaceholder flag is set
                targetImage = 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) {
                // Always
                shouldUseTransition = YES;
            } else if (cacheType == SDImageCacheTypeNone) {
                // From network
                shouldUseTransition = 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, fallback
                    shouldUseTransition = NO;
                }
            }
            if (finished && shouldUseTransition) {
                transition = self.sd_imageTransition;
            }
#endif
            dispatch_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];
#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;
}

实现的流程我已经通过注释进行了解释,用自然语言将整个过程描述一遍的话就是:

  1. 先对URL预处理,以免类型错误,如果是NSString会自动转换

  2. 准备上下文contextcontext 是一个配置字典,可以指定缓存策略、解码器、下载器等。

  3. 取消前一个请求,取消旧的下载任务

  4. 加载状态绑定

  5. 从context中获取图片管理器SDWebImageManager

  6. 显示占位图

  7. 图片加载开始,重制进度对象,启动小菊花

  8. 启动图片下载,设置block更新小菊花,调用progresssBlock

  9. 下载完后根据不同的情况处理图片

如果URL为空:停止小菊花,调用完成block并返回URL无效错误

loadImageWithURL

然后我们来看看loadImageWithURL是怎么实现的

objectivec 复制代码
- (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.
    //先检查URL的类型
    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;
    }
    //创建一个新的操作用于管理这次加载
    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;
}

我们同样使用自然语言描述一下整个流程:

  1. URL类型检验与转换

  2. 初始化加载操作对象,得到CombinedOperation,用来标识和管理这次加载任务

  3. 判断是否是"失败 URL",避免每次都去请求已经确定失败的地址

  4. 生成处理结果对象,统一处理 optionscontext,确保后续所有调用用的是标准格式

  5. 如果 URL 是空字符串,或者是黑名单 URL 且没有设置重试选项,会直接调用 completedBlock 并返回错误

  6. 加入运行中操作集合

  7. 调用方法callCacheProcessForOperation: url: options: context: progress: completed: 来决定缓存和下载策略并执行

callCacheProcessForOperation

接着我们看看callCacheProcessForOperation这个方法是如何实现的

objectivec 复制代码
- (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
    //获取需要查询的缓存图像,如果上下文中有则优先从上下文中获取,否则就从当前类中获取
    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 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];
    }
}

同样我们使用自然语言描述,可以对照源码将步骤一一对应:

  1. 获取要使用的缓存对象

  2. 确定要查询的缓存类型

  3. 判断是否需要查询缓存,如果设置了 SDWebImageFromLoaderOnly 选项,就不查询缓存,直接跳到下载流程

  4. 构造缓存 key:会移除缩略图尺寸等相关信息,避免 key 不一致导致查询失败

  5. 执行缓存查询:

    1. 如果找到缓存图像,就继续进入下载或处理流程;

    2. 如果未找到:

      1. 会尝试用原始缓存 key(未做图像变换前的 key)再查一次(这是给例如缩略图、变换图保留原始图缓存的情况);

      2. 如果原始 key 也没找到,再进入下载流程。

queryImageForKey

在刚刚函数的实现中,有一行通过queryImageForKey来查询缓存操作:

objectivec 复制代码
operation.cacheOperation = [imageCache queryImageForKey:key options:options context:mutableContext cacheType:queryCacheType completion:^(UIImage * _Nullable cachedImage, NSData * _Nullable cachedData, SDImageCacheType cacheType) {}];

queryImageForKey最后会调用queryCacheOperationForKey,我们来看看它是如何实现的:

objectivec 复制代码
- (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 image
            if (image.sd_imageFrameCount > 1) {
#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
            Class 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));
    //定义从磁盘查询数据的Block
    NSData* (^queryDiskDataBlock)(void) = ^NSData* {//定义Block,对取消操作进行加锁
        @synchronized (operation) {
            if (operation.isCancelled) {
                return nil;
            }
        }
        //如果操作没有被取消,从所有可能路径中搜索数据
        return [self diskImageDataBySearchingAllPathsForKey:key];
    };
    //定义从磁盘创建图像的Block
    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
            //如果已经在内存中找到图像,但是需要图像数据
            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 logic
                if (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;
}

使用自然语言描述:

  1. 校验key和缓存类型

  2. 尝试查询内存缓存:如果设置的 cacheType 不是 .Disk,就尝试从内存中获取图片。如果找到了图片:

    • 如果设置了只解码第一帧(针对动图),会将动图转成静态图。

    • 如果设置了需要匹配特定图片类(如动图类),但类型不匹配,则丢弃这个图片。

  3. 判断是否只需要查询内存

  4. 准备磁盘查询操作

  5. 同步或异步执行磁盘查询:如果设置了同步查询磁盘的选项,则在 IO 队列中同步读取磁盘并执行回调;否则异步查询:

    • 异步从磁盘读取数据并解码成图片。

    • 最后将结果切回主线程或指定的回调队列进行回调。

  6. 在读取数据、解码图片和回调之前,都会判断是否调用了取消操作(通过 operation.isCancelled)来提前退出,避免多余工作。

可见这个方法的作用是:

根据指定的 key(缓存键) ,从内存或磁盘中查询图片缓存,并通过回调返回结果(UIImage 和 image data)。支持多种查询选项,比如只查询内存、是否解码第一帧、是否匹配特定图片类等。返回一个 SDImageCacheToken,用于后续可能的取消操作。

callDownloadProcessForOperation

这个方法负责在缓存查找完成后,决定是否从网络下载图片并执行相关回调

objectivec 复制代码
- (void)callDownloadProcessForOperation:(nonnull SDWebImageCombinedOperation *)operation
                                    url:(nonnull NSURL *)url
                                options:(SDWebImageOptions)options
                                context:(SDWebImageContext *)context
                            cachedImage:(nullable UIImage *)cachedImage
                             cachedData:(nullable NSData *)cachedData
                              cacheType:(SDImageCacheType)cacheType
                               progress:(nullable SDImageLoaderProgressBlock)progressBlock
                              completed:(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];
    }
}

使用自然语言描述如下:

  1. 标记缓存操作已结束

  2. 获取图片加载器(imageLoader)

  3. 判断是否需要下载图片

  4. 如果需要下载,进入下载流程:

    1. 如果缓存中已有图片且设置了刷新缓存(RefreshCached):

      1. 先立即回调一次缓存图像,让用户界面立即显示;

      2. 同时继续向服务器发起请求,用于刷新或确认是否真的有新内容;

      3. 把缓存图像传给加载器,用于与远程图像比较,避免重复下载。

    2. 发起下载请求

      1. 通过 imageLoader 执行网络请求,传入 URL、选项、上下文等;

      2. 请求完成后会回调到一个 completed: block。

  5. 处理下载完成回调:

  6. 如果下载被禁止,但已经命中缓存,则直接使用缓存图像回调并移除任务

  7. 如果不下载也没有缓存图像,直接回调空图像,表示整个请求失败或被禁止,任务结束

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设置在对应的视图上

相关推荐
暗流者13 分钟前
AAA 服务器与 RADIUS 协议笔记
运维·服务器·笔记
项目題供诗25 分钟前
React学习(十二)
javascript·学习·react.js
艾莉丝努力练剑1 小时前
【C语言16天强化训练】从基础入门到进阶:Day 7
java·c语言·学习·算法
Ro Jace1 小时前
科研笔记:博士生手册
笔记
#include>1 小时前
【Golang】有关垃圾收集器的笔记
笔记·golang
2501_915106322 小时前
iOS混淆工具实战 金融支付类 App 的安全防护与合规落地
android·ios·小程序·https·uni-app·iphone·webview
I烟雨云渊T2 小时前
iOS 数据持久化
macos·ios·cocoa
自强的小白2 小时前
学习Java24天
java·学习
Qlittleboy2 小时前
手机、电脑屏幕的显示坏点检测和成像原理
经验分享·笔记
SalvoGao4 小时前
空转学习 | cell-level 与 spot-level的区别
人工智能·深度学习·学习