[iOS开发]SDWebImage源码学习

语言: CN / TW / HK

SDWebImage是一个开源的第三方库,它提供了UIImageView的一个分类,以支持从远程服务器下载并缓存图片的功能。它具有以下功能: 1. 一个异步的图片加载器。 2. 一个异步的内存+磁盘图片缓存 3. 支持GIF、WebP图片 4. 后台图片解压缩处理 5. 确保同一个URL的图片不被多次下载 6. 确保非法的URL不会被反复加载 7. 确保下载及缓存时,主线程不被阻塞。

大致流程如下: 在这里插入图片描述

调用

接下来一点点看,我们一般都会使用方法加载图片:

objectivec //UIImage+WebCache.h - (void)sd_setImageWithURL:(nullable NSURL *)url placeholderImage:(nullable UIImage *)placeholder completed:(nullable SDExternalCompletionBlock)completedBlock NS_REFINED_FOR_SWIFT;UIImage+WebCache.h文件里,我们还可以找到常见的:

objectivec - (void)sd_setImageWithURL:(nullable NSURL *)url NS_REFINED_FOR_SWIFT; - (void)sd_setImageWithURL:(nullable NSURL *)url placeholderImage:(nullable UIImage *)placeholder NS_REFINED_FOR_SWIFT; - (void)sd_setImageWithURL:(nullable NSURL *)url placeholderImage:(nullable UIImage *)placeholder options:(SDWebImageOptions)options NS_REFINED_FOR_SWIFT; - (void)sd_setImageWithURL:(nullable NSURL *)url completed:(nullable SDExternalCompletionBlock)completedBlock; 等常用方法。 对于SDExternalCompletionBlock,我们可以在SDWebImageManager.h中找到定义:

objectivec typedef void(^SDExternalCompletionBlock)(UIImage * _Nullable image, NSError * _Nullable error, SDImageCacheType cacheType, NSURL * _Nullable imageURL); 查看UIImage+WebCache.m文件,可以看到这些方法的实现代码:

```objectivec - (void)sd_setImageWithURL:(nullable NSURL *)url { [self sd_setImageWithURL:url placeholderImage:nil options:0 progress:nil completed:nil]; }

  • (void)sd_setImageWithURL:(nullable NSURL )url placeholderImage:(nullable UIImage )placeholder { [self sd_setImageWithURL:url placeholderImage:placeholder options:0 progress:nil completed:nil]; }

  • (void)sd_setImageWithURL:(nullable NSURL )url placeholderImage:(nullable UIImage )placeholder options:(SDWebImageOptions)options { [self sd_setImageWithURL:url placeholderImage:placeholder options:options progress:nil completed:nil]; }

  • (void)sd_setImageWithURL:(nullable NSURL )url placeholderImage:(nullable UIImage )placeholder options:(SDWebImageOptions)options context:(nullable SDWebImageContext *)context { [self sd_setImageWithURL:url placeholderImage:placeholder options:options context:context progress:nil completed:nil]; }

  • (void)sd_setImageWithURL:(nullable NSURL *)url completed:(nullable SDExternalCompletionBlock)completedBlock { [self sd_setImageWithURL:url placeholderImage:nil options:0 progress:nil completed:completedBlock]; }

  • (void)sd_setImageWithURL:(nullable NSURL )url placeholderImage:(nullable UIImage )placeholder completed:(nullable SDExternalCompletionBlock)completedBlock { [self sd_setImageWithURL:url placeholderImage:placeholder options:0 progress:nil completed:completedBlock]; }

  • (void)sd_setImageWithURL:(nullable NSURL )url placeholderImage:(nullable UIImage )placeholder options:(SDWebImageOptions)options completed:(nullable SDExternalCompletionBlock)completedBlock { [self sd_setImageWithURL:url placeholderImage:placeholder options:options progress:nil completed:completedBlock]; }

  • (void)sd_setImageWithURL:(nullable NSURL )url placeholderImage:(nullable UIImage )placeholder options:(SDWebImageOptions)options progress:(nullable SDImageLoaderProgressBlock)progressBlock completed:(nullable SDExternalCompletionBlock)completedBlock { [self sd_setImageWithURL:url placeholderImage:placeholder options:options context:nil progress:progressBlock completed:completedBlock]; } ``` 它们最终都调用了

objectivec - (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; 方法。查看它的代码:

objectivec - (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 { [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); } }]; } 发现它调用了

objectivec - (void)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; 方法。其简要流程为: 1. 将 SDWebImageContext 复制并转换为 immutable,获取其中的 validOperationKey 值作为校验 id,默认值为当前 view 的类名; 2. 执行 sd_cancelImageLoadOperationWithKey 取消上一次任务,保证没有当前正在进行的异步下载操作, 不会与即将进行的操作发生冲突; 3. 设置占位图; 4. 初始化 SDWebImageManagerSDImageLoaderProgressBlock , 重置 NSProgressSDWebImageIndicator; 5. 开启下载loadImageWithURL: 并将返回的 SDWebImageOperation 存入 sd_operationDictionarykeyvalidOperationKey; 6. 取到图片后,调用 sd_setImage: 同时为新的 image 添加 Transition 过渡动画; 7. 动画结束后停止 indicator

UIView+Cache.h文件中可以找到它的实现:

```objectivec - (void)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 { 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]; } self.sd_latestOperationKey = validOperationKey; //1.取消先前的下载任务 [self sd_cancelImageLoadOperationWithKey:validOperationKey]; self.sd_imageURL = url;

if (!(options & SDWebImageDelayPlaceholder)) {
    dispatch_main_async_safe(^{
        //设置占位图
        // 作为图片下载完成之前的替代图片。
        [self sd_setImage:placeholder imageData:nil basedOnClassOrViaCustomSetImageBlock:setImageBlock cacheType:SDImageCacheTypeNone imageURL:url];
    });
}

if (url) {
    // reset the progress
    // 重置进度
    NSProgress *imageProgress = objc_getAssociatedObject(self, @selector(sd_imageProgress));
    // 获取图像加载进度
    if (imageProgress) {
        imageProgress.totalUnitCount = 0;
        imageProgress.completedUnitCount = 0;
    }

if SD_UIKIT || SD_MAC

    // check and start image indicator
    // 是否显示进度条
    [self sd_startImageIndicator];
    id<SDWebImageIndicator> imageIndicator = self.sd_imageIndicator;

endif

    // 获取图像管理者对象
    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];
    }

    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);
    id <SDWebImageOperation> 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 callCompletedBlockClojure = ^{
            //self是否被释放
            if (!self) { return; }
            if (!shouldNotSetImage) {
                [self sd_setNeedsLayout];
            }
            //不要自动设置图片,调用Block传入UIImage对象
            if (completedBlock && shouldCallCompletedBlock) {
                completedBlock(image, data, error, cacheType, finished, url);
            }
        };

        // case 1a: we got an image, but the SDWebImageAvoidAutoSetImage flag is set
        // 我们得到了一个图像,但SDWebImageAvoidAutoSetImage标志被设置了
        // OR 或
        // case 1b: we got no image and the SDWebImageDelayPlaceholder is not set
        // 我们没有图像,也没有设置SDWebImageDelayPlaceholder
        if (shouldNotSetImage) {
            dispatch_main_async_safe(callCompletedBlockClojure);
            return;
        }

        UIImage *targetImage = nil;
        NSData *targetData = nil;
        if (image) {
            // case 2a: we got an image and the SDWebImageAvoidAutoSetImage is not set
            // 我们得到了一个图像,但SDWebImageAvoidAutoSetImage没有设置
            targetImage = image;
            targetData = data;
        } else if (options & SDWebImageDelayPlaceholder) {
            // case 2b: we got no image and the SDWebImageDelayPlaceholder flag is set
            // 我们没有图像,并且设置了SDWebImageDelayPlaceholder标志
            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) {
                // SDWebImageQueryMemoryDataSync:
                // 默认情况下,当您仅指定“SDWebImageQueryMemoryData”时,我们会异步查询内存映像数据。
                // 将此掩码也组合在一起,以同步查询内存图像数据
                // 不建议同步查询数据,除非您要确保在同一 runloop 中加载映像以避免在单元重用期间闪烁。
                // SDWebImageQueryDiskDataSync:
                // 默认情况下,当内存缓存未命中时,我们会异步查询磁盘缓存。此掩码可以强制同步查询磁盘缓存(当内存缓存未命中时)。
                // 这两个选项打开则NO。
                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 basedOnClassOrViaCustomSetImageBlock:setImageBlock transition:transition cacheType:cacheType imageURL:imageURL];

else

            [self sd_setImage:targetImage imageData:targetData basedOnClassOrViaCustomSetImageBlock:setImageBlock cacheType:cacheType imageURL:imageURL];

endif

            callCompletedBlockClojure();
        });
    }];
    // 将生成的加载操作赋给UIView的自定义属性,并赋值
    [self sd_setImageLoadOperation:operation forKey:validOperationKey];
} else {

if SD_UIKIT || SD_MAC

    [self sd_stopImageIndicator];

endif

    dispatch_main_async_safe(^{
        if (completedBlock) {
            NSError *error = [NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorInvalidURL userInfo:@{NSLocalizedDescriptionKey : @"Image url is nil"}];
            completedBlock(nil, nil, error, SDImageCacheTypeNone, YES, url);
        }
    });
}

} `` 参数列表中,有一项SDWebImageOptions,在SDWebImageDefine.h里面,定义了SDWebImageOptions`,其是一种暴露在外的可供使用者使用的选择方法。

objectivec typedef NS_OPTIONS(NSUInteger, SDWebImageOptions) { // 默认情况下,当URL下载失败时,URL会被列入黑名单,导致库不会再去重试,该标记用于禁用黑名单 SDWebImageRetryFailed = 1 << 0, // 默认情况下,图片下载开始于UI交互,该标记禁用这一特性,这样下载延迟到UIScrollView减速时 SDWebImageLowPriority = 1 << 1, // 该标记启用渐进式下载,图片在下载过程中是渐渐显示的,如同浏览器一下。 // 默认情况下,图像在下载完成后一次性显示 SDWebImageProgressiveLoad = 1 << 2, // 即使缓存了映像,也要遵循 HTTP 响应缓存控件,并在需要时从远程位置刷新映像。 // 磁盘缓存将由 NSURLCache 而不是 SDWebImage 处理,从而导致性能略有下降。 // 此选项有助于处理同一请求URL后面的图像变化,例如Facebook图形API个人资料图片。 // 如果刷新缓存的图像,则对缓存的图像调用一次完成块,然后对最终图像调用一次完成块。 // 仅当无法使用嵌入式缓存破坏参数使 URL 保持静态时,才使用此标志。 SDWebImageRefreshCached = 1 << 3, // 在iOS 4+系统中,当程序进入后台后继续下载图片。这将要求系统给予额外的时间让请求完成 // 如果后台任务超时,则操作被取消 SDWebImageContinueInBackground = 1 << 4, // 通过设置NSMutableURLRequest.HTTPShouldHandleCookies = YES;来处理存储在NSHTTPCookieStore中的cookie SDWebImageHandleCookies = 1 << 5, // 允许不受信任的SSL认证 // 用于测试目的。在生产中谨慎使用。 SDWebImageAllowInvalidSSLCertificates = 1 << 6, // 默认情况下,图片下载按入队的顺序来执行。该标记将其移到队列的前面, // 以便图片能立即下载而不是等到当前队列被加载 SDWebImageHighPriority = 1 << 7, // 默认情况下,占位图片在加载图片的同时被加载。该标记延迟占位图片的加载直到图片已以被加载完成 SDWebImageDelayPlaceholder = 1 << 8, // 通常我们不调用动画图片的transformDownloadedImage代理方法,因为大多数转换代码可以管理它。 // 使用这个票房则不任何情况下都进行转换。 SDWebImageTransformAnimatedImage = 1 << 9, // 默认情况下,图像在下载后添加到图像视图中。 // 但在某些情况下,我们希望在设置图像之前做一些事(have the hand)(例如,应用滤镜或添加交叉淡入淡出动画) // 如果要在成功时手动设置图像完成,请使用此标志 SDWebImageAvoidAutoSetImage = 1 << 10, // 默认情况下,将按照其原始大小对图像进行解码。 // 此标志会将图像缩小到与设备受限内存兼容的大小。 // 要控制限制内存字节数,请选中“SDImageCoderHelper.defaultScaleDownLimitBytes”(在iOS上默认为60MB) // 这实际上将转换为使用v5.5.0中的上下文选项“.imageThumbnailPixelSize”(在iOS上默认为(3966,3966)。以前没有。 // 此标志也会影响 v5.5.0 中的逐行图像和动画图像。以前没有。 //@note 如果您需要细节控件,最好改用上下文选项“imageThumbnailPixelSize”和“imagePreserveAspectRatio”。 SDWebImageScaleDownLargeImages = 1 << 11, // 默认情况下,当图像已缓存在内存中时,我们不会查询图像数据。此掩码可以强制同时查询图像数据。但是,此查询是异步的,除非您指定“SDWebImageQueryMemoryDataSync” SDWebImageQueryMemoryData = 1 << 12, // 默认情况下,当您仅指定“SDWebImageQueryMemoryData”时,我们会异步查询内存映像数据。将此掩码也组合在一起,以同步查询内存图像数据。 //@note 不建议同步查询数据,除非您要确保在同一 runloop 中加载映像以避免在单元重用期间闪烁。 SDWebImageQueryMemoryDataSync = 1 << 13, // 默认情况下,当内存缓存未命中时,我们会异步查询磁盘缓存。此掩码可以强制同步查询磁盘缓存(当内存缓存未命中时)。 //@note 这 3 个查询选项可以组合在一起。有关这些掩码组合的完整列表,请参阅 wiki 页面。 //@note 不建议同步查询数据,除非您希望确保在同一 runloop 中加载映像以避免在单元重用期间闪烁。 SDWebImageQueryDiskDataSync = 1 << 14, // 默认情况下,当缓存丢失时,将从加载程序加载映像。此标志可以防止仅从缓存加载。 SDWebImageFromCacheOnly = 1 << 15, // 默认情况下,我们在从加载程序加载图像之前查询缓存。此标志可以防止仅从加载程序加载。 SDWebImageFromLoaderOnly = 1 << 16, // 默认情况下,当您使用“SDWebImageTransition”在映像加载完成后执行某些视图转换时,仅当管理器的回调是异步的(来自网络或磁盘缓存查询)时,此转换才应用于映像。 // 此掩码可以强制对任何情况(如内存缓存查询或同步磁盘缓存查询)应用视图转换。 SDWebImageForceTransition = 1 << 17, // 默认情况下,我们将在缓存查询期间解码后台的图像,并从网络下载。这有助于提高性能,因为在屏幕上呈现图像时,需要首先对其进行解码。但这发生在Core Animation的主队列上。 // 但是,此过程也可能增加内存使用量。如果由于内存消耗过多而遇到问题,此标志可能会阻止解码图像。 SDWebImageAvoidDecodeImage = 1 << 18, // 默认情况下,我们对动画图像进行解码。此标志可以仅强制解码第一帧并生成静态图像。 SDWebImageDecodeFirstFrameOnly = 1 << 19, // 默认情况下,对于“SDAnimatedImage”,我们在渲染期间解码动画图像帧以减少内存使用量。但是,您可以指定将所有帧预加载到内存中,以便在动画图像由大量 imageView 共享时降低 CPU 使用率。 // 这实际上会在后台队列中触发“preloadAllAnimatedImageFrames”(仅限磁盘缓存和下载)。 SDWebImagePreloadAllFrames = 1 << 20, // 默认情况下,当您使用“SDWebImageContextAnimatedImageClass”上下文选项时(例如使用旨在使用“SDAnimatedImage”的“SDAnimatedImageView”),当内存缓存命中时,我们可能仍使用“UIImage”,或者图像解码器无法生成与您的自定义类完全匹配的解码器作为回退解决方案。 // 使用此选项,可以确保我们始终回调映像与您提供的类。如果无法生成一个,则将使用代码为“SDWebImageErrorBadImageData”的错误。 // 请注意,此选项与“SDWebImageDecodeFirstFrameOnly”不兼容,后者始终生成 UIImage/NSImage。 SDWebImageMatchAnimatedImageClass = 1 << 21, // 默认情况下,当我们从网络加载图像时,图像将被写入缓存(内存和磁盘,由您的“storeCacheType”上下文选项控制) // 这可能是一个异步操作,最终的“SDInternalCompletionBlock”回调并不能保证写入的磁盘缓存已完成,并可能导致逻辑错误。(例如,您刚刚在完成块中修改磁盘数据,但是,磁盘缓存尚未准备就绪) // 如果需要使用完成块中的磁盘缓存进行处理,则应使用此选项来确保在回调时已写入磁盘缓存。 // 请注意,如果在使用自定义缓存序列化程序或使用转换器时使用它,我们也会等到写入的输出图像数据完成。 SDWebImageWaitStoreCache = 1 << 22, // 我们通常不会对矢量图像应用变换,因为矢量图像支持动态变化到任何大小,光栅化到固定大小会丢失细节。要修改矢量图像,可以在运行时处理矢量数据(例如修改PDF标签/ SVG元素)。 // 无论如何,使用此标志来转换它们。 SDWebImageTransformVectorImage = 1 << 23 }; 慢慢看这个函数的实现,看到:

objectivec NSString *validOperationKey = context[SDWebImageContextSetImageOperationKey]; //从context获取对应的validOperationKey,然后根据参数validOperationKey进行后面的操作, //如果operationKey为nil key取NSStringFromClass([self class]) 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]; } //设置sd_latestOperationKey,最新的operationKey self.sd_latestOperationKey = validOperationKey;

看最后一行:self.sd_latestOperationKey = validOperationKey;,可以看到,点语法实际在调用settergetter方法,使用objc_getAssociatedObjectobjc_setAssociatedObject函数。

objectivec - (nullable NSString *)sd_latestOperationKey { return objc_getAssociatedObject(self, @selector(sd_latestOperationKey)); } - (void)setSd_latestOperationKey:(NSString * _Nullable)sd_latestOperationKey { objc_setAssociatedObject(self, @selector(sd_latestOperationKey), sd_latestOperationKey, OBJC_ASSOCIATION_COPY_NONATOMIC); }

有一个函数叫:sd_cancelCurrentImageLoad

objectivec - (void)sd_cancelCurrentImageLoad { [self sd_cancelImageLoadOperationWithKey:self.sd_latestOperationKey]; self.sd_latestOperationKey = nil; } 这里面调用sd_cancelImageLoadOperationWithKey:方法时使用了sd_latestOperationKey。再接着前面的内容向下看,这里直接调用了sd_cancelImageLoadOperationWithKey:方法:

objectivec //取消先前的下载任务 [self sd_cancelImageLoadOperationWithKey:validOperationKey]; 我们从中不难发现,这句效果其实和在这里直接使用[self sd_cancelCurrentImageLoad];效果一样。

这句话通过键值取消当前图像的加载,键值是UIImageViewImageLoad,我们继续点开下一层看到sd_cancelImageLoadOperationWithKey这个方法的实现 ```objectivec - (void)sd_cancelImageLoadOperationWithKey:(nullable NSString )key { if (key) { // Cancel in progress downloader from queue // 从队列中取消正在进行的下载程序 SDOperationsDictionary operationDictionary = [self sd_operationDictionary]; id operation;

    @synchronized (self) {
        operation = [operationDictionary objectForKey:key];
    }
    if (operation) {
        if ([operation conformsToProtocol:@protocol(SDWebImageOperation)]) {
            [operation cancel];
        }
        @synchronized (self) {
            [operationDictionary removeObjectForKey:key];
        }
    }
}

} // 先获得方法一的返回字典数据,传入 key 在返回的字典中查找是否已经存在,如果存在则取消所有操作 // conformsToProtocol 方法如果符合这个协议(协议中声明了取消方法),调用协议中的取消方法。 ``` 接下来了解一下其是怎样实现取消队列中所有操作的下载。

objectivec NSMutableDictionary *operationDictionary = [self operationDictionary];

我们点开 sd_operationDictionary的源码:

objectivec - (SDOperationsDictionary *)sd_operationDictionary { @synchronized(self) { SDOperationsDictionary *operations = objc_getAssociatedObject(self, &loadOperationKey); if (operations) { return operations; } operations = [[NSMapTable alloc] initWithKeyOptions:NSPointerFunctionsStrongMemory valueOptions:NSPointerFunctionsWeakMemory capacity:0]; objc_setAssociatedObject(self, &loadOperationKey, operations, OBJC_ASSOCIATION_RETAIN_NONATOMIC); return operations; } }

这两个方法用的是 OC 中 runtime 方法,原理是两个文件关联方法,和上层的存储方法差不多,传入 valuekey 对应,取出也是根据 key 取出 valueobject 传入 self 即可。

objectivec // 取出方法 // 传入 object 和 key 返回 value id objc_getAssociatedObject(id object, const void *key)

objectivec // 设置关联方法,为现有对象添加新属性,在本处是为self添加 // 传入 object、key、value,policy // 该函数中第一位参数表示目标对象, // 第二个参数,key。因为一个对象可以关联多个新的对像,我们需要一个标志来区分他们。 // 所以这个key就起这样的作用。这里的需要的key的地址,不关心它指向谁。 // 第三个参数表示要添加的属性, // 第四个参数设置objc_AssociationPolicy // policy 即存储方式,和声明使用几种属性大致相同,有 copy,retain,copy,retain_nonatomic,assign 五种) void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy); 通过键值来得到绑定的的关联对象(分类,可以通过它来扩展方法;关联对象,可以通过它来扩展属性)的值赋值给可变字典类型的operations,如果operations不为空,那么返回该关联对象的值。暂时就这样理解。如果为空,就通过键值与关联策略(传递nil以清除现有关联)为给定对象设定关联值。这就是operationDictionary的实现。 再说回来我们来看一下取消队列中下载的实现,其先为每个每一个UIView+WebCache 创建operationDictionary。假设我们第一次进行这个操作,所以其不会进入第一个if中进行判断,因为一开始默认的loadOperationKey为静态变量,没有进行赋值操作。自然也没有key值,所以后面的if也不会走。 同样我们也看一下如果前面已经有相同的key值其是怎么进行取消与判断的,后面的代码。 如果有两个键值相同的内容进入队列,那么通过前面字典的创建得到的关联对象的值也是一样的,传入key在返回的字典中查找是否已经存在,如果存在则取消所有操作,conformsToProtocol方法如果符合这个协议(协议中声明了取消方法),也调用协议中的取消方法。 实际上,所有的操作都是由一个实际上,所有的操作都是由一个operationDictionary字典维护的,执行新的操作之前,cancel所有的operation这样做好处多多: - 针对一个UIImageView,取消前一个操作,省时、省流量 - 也就是避免对一张图片进行重复下载。加载图片完成后, 回调时会先检查任务的Operation还在不在, 不在,则不回调显示, 反之回调显示并移除Operation。 - 当程序中断导致链接失效时,当前的下载还在操作队列中,但是按道理应该是失效状态,我们可以通过先取消当前正在进行的下载来保证操作队列中的链接不存在这种情况。

取消操作后,直接是一句:self.sd_imageURL = url;。这句话使用点语法,暗藏玄机:

objectivec - (nullable NSURL *)sd_imageURL { return objc_getAssociatedObject(self, @selector(sd_imageURL)); } - (void)setSd_imageURL:(NSURL * _Nullable)sd_imageURL { objc_setAssociatedObject(self, @selector(sd_imageURL), sd_imageURL, OBJC_ASSOCIATION_RETAIN_NONATOMIC); } 其实也调用了objc_setAssociatedObject函数。以每个需要下载的图像的url为加载的键值进行绑定。这样每个都有自己所对应的键值。 接下来向下看:

objectivec if (!(options & SDWebImageDelayPlaceholder)) { dispatch_main_async_safe(^{ [self sd_setImage:placeholder imageData:nil basedOnClassOrViaCustomSetImageBlock:setImageBlock cacheType:SDImageCacheTypeNone imageURL:url]; }); } 前面说到,SDWebImageDelayPlaceholder是一个option选项,其为枚举类型:

objectivec // 默认情况下,占位图片在加载图片的同时被加载。该标记延迟占位图片的加载直到图片已以被加载完成 SDWebImageDelayPlaceholder = 1 << 8, dispatch_main_async_safe是个宏定义,保证在主线程安全执行。 ```objectivec

ifndef dispatch_main_async_safe

#define dispatch_main_async_safe(block)\ if (dispatch_queue_get_label(DISPATCH_CURRENT_QUEUE_LABEL) == dispatch_queue_get_label(dispatch_get_main_queue())) {\ block();\ } else {\ dispatch_async(dispatch_get_main_queue(), block);\ }

endif

```

主线程调用了:

objectivec - (void)sd_setImage:(UIImage *)image imageData:(NSData *)imageData basedOnClassOrViaCustomSetImageBlock:(SDSetImageBlock)setImageBlock cacheType:(SDImageCacheType)cacheType imageURL:(NSURL *)imageURL; 查看代码:

```objectivec - (void)sd_setImage:(UIImage )image imageData:(NSData )imageData basedOnClassOrViaCustomSetImageBlock:(SDSetImageBlock)setImageBlock cacheType:(SDImageCacheType)cacheType imageURL:(NSURL *)imageURL {

if SD_UIKIT || SD_MAC

[self sd_setImage:image imageData:imageData basedOnClassOrViaCustomSetImageBlock:setImageBlock transition:nil cacheType:cacheType imageURL:imageURL];

else

// watchOS does not support view transition. Simplify the logic
if (setImageBlock) {
    setImageBlock(image, imageData, cacheType, imageURL);
} else if ([self isKindOfClass:[UIImageView class]]) {
    UIImageView *imageView = (UIImageView *)self;
    [imageView setImage:image];
}

endif

} ```

方法先是进行开发环境检查,SDWebImage 支持当前的 MAC / iOS / TV / WATCH 平台,这种适配各个平台的兼容,对框架开发意义重大。下面简要介绍一下各个标签的含义:

```objectivec

if !TARGET_OS_IPHONE && !TARGET_OS_IOS && !TARGET_OS_TV && !TARGET_OS_WATCH

#define SD_MAC 1

else

#define SD_MAC 0

endif

// 判断当前平台是不是MAC。 // TARGET_OS_MAC 定义在所有的平台中,比如MAC、iPhone、Watch、TV等,因此单纯的使用TARGET_OS_MAC 判断当前是不是MAC 平台是不可行的。 // 但按照上面的判断方式,也存在一个缺点:当Apple出现新的平台时,判断条件要修改。

if TARGET_OS_IOS || TARGET_OS_TV

#define SD_UIKIT 1

else

#define SD_UIKIT 0

endif

// iOS 和 tvOS 是非常相似的,UIKit在这两个平台中都存在,但是watchOS在使用UIKit时,是受限的。 // 因此定义SD_UIKIT为真的条件是iOS 和 tvOS这两个平台。

if TARGET_OS_IOS

#define SD_IOS 1

else

#define SD_IOS 0

endif

if TARGET_OS_TV

#define SD_TV 1

else

#define SD_TV 0

endif

if TARGET_OS_WATCH

#define SD_WATCH 1

else

#define SD_WATCH 0

endif

// 这三个宏定义用于区分iOS、TV、WATCH三个平台。 ``` 对于这些,我们就知道是为了不同环境适配就好了。不是WATCH的情况下调用了:

objectivec - (void)sd_setImage:(UIImage *)image imageData:(NSData *)imageData basedOnClassOrViaCustomSetImageBlock:(SDSetImageBlock)setImageBlock transition:(SDWebImageTransition *)transition cacheType:(SDImageCacheType)cacheType imageURL:(NSURL *)imageURL; 在这个方法里,对self类进行检查,

objectivec ... } else if ([view isKindOfClass:[UIImageView class]]) { UIImageView *imageView = (UIImageView *)view; finalSetImageBlock = ^(UIImage *setImage, NSData *setImageData, SDImageCacheType setCacheType, NSURL *setImageURL) { }; } ... 几个if情况看下来,我们不难看出,==这就是为不同的类的==self==设置图片==。后面还有个if (transition)对是否需要过渡。分析比较冗长,到目前为止就是:==动态绑定validOperationKey->先取消之前的下载->动态绑定imageURL->判断并根据要求设置占位图片==。 接下来,我们看到if (url),检测传入的URL是否为空。接下来是重置进度数据。

objectivec NSProgress *imageProgress = objc_getAssociatedObject(self, @selector(sd_imageProgress)); if (imageProgress) { imageProgress.totalUnitCount = 0; imageProgress.completedUnitCount = 0; } 接下来是[self sd_startImageIndicator];,查看sd_startImageIndicator代码:

objectivec - (void)sd_startImageIndicator { id<SDWebImageIndicator> imageIndicator = self.sd_imageIndicator; if (!imageIndicator) { return; } dispatch_main_async_safe(^{ [imageIndicator startAnimatingIndicator]; }); } 再查看startAnimatingIndicator代码:

```objectivec - (void)startAnimatingIndicator {

if SD_UIKIT

[self.indicatorView startAnimating];

else

[self.indicatorView startAnimation:nil];

endif

self.indicatorView.hidden = NO;

} ``` 查看定义:

```objectivec

if SD_UIKIT

@property (nonatomic, strong, readonly, nonnull) UIActivityIndicatorView *indicatorView;

else

@property (nonatomic, strong, readonly, nonnull) NSProgressIndicator *indicatorView;

endif

`` 我们知道了indicatorView是加载小菊花,这句代码的意思就是让它==根据情况选择加载小菊花==。如果没见过小菊花,那应该是加载的太快了,导致小菊花还没有加载出来就结束了,并且白底确实看不清。再向下看,先获取图像管理者对象,如果自定义了图像管理者对象就用自定义的,否则就用单例对象。接下来,==创建代码块用来处理进度数据==,然后回调block。 再接下来,==创建图像加载操作==,图像管理者对象manager`调用

objectivec - (SDWebImageCombinedOperation *)loadImageWithURL:(nullable NSURL *)url options:(SDWebImageOptions)options context:(nullable SDWebImageContext *)context progress:(nullable SDImageLoaderProgressBlock)progressBlock completed:(nonnull SDInternalCompletionBlock)completedBlock; 里面拆分成下面6个方法: - callCacheProcessForOperation - callDownloadProcessForOperation - callStoreCacheProcessForOperation - callTransformProcessForOperation - callCompletionBlockForOperation - safelyRemoveOperationFromRunning

分别是:==缓存查询、下载、存储、转换、执行回调、清理回调==。 查看该方法代码:

```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 // 回调block是必传的 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.
// 虽然参数要求传NSURL类型对象但是如果传NSStriing类型对象并不会有警告,所以再做一下处理
if ([url isKindOfClass:NSString.class]) {
    url = [NSURL URLWithString:(NSString *)url];
}

// Prevents app crashing on argument type error like sending NSNull instead of NSURL
// 防止参数url的类型错误导致崩溃,例如url的是NSNull类型的
if (![url isKindOfClass:NSURL.class]) {
    url = nil;
}
// 生成一个图像加载操作的封装对象
SDWebImageCombinedOperation *operation = [SDWebImageCombinedOperation new];
operation.manager = self;
// 定义变量保存要加载的url是否失败过
BOOL isFailedUrl = NO;
if (url) {
    // 加锁
    SD_LOCK(self.failedURLsLock);
    // 判断是否失败过
    isFailedUrl = [self.failedURLs containsObject:url];
    SD_UNLOCK(self.failedURLsLock);
}
// 如果链接地址不正确,
// 或者之前加载失败过并且没有设置失败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}] url:url];
    return operation;
}
// 加锁
SD_LOCK(self.runningOperationsLock);
// 保存当前操作封装对象到属性中
[self.runningOperations addObject:operation];
SD_UNLOCK(self.runningOperationsLock);

// Preprocess the options and context arg to decide the final the result for manager
// 预处理选项和上下文参数,以确定管理器的最终结果
SDWebImageOptionsResult *result = [self processedResultForURL:url options:options context:context];

// Start the entry to load image from cache
// 启动条目以从缓存加载图像
[self callCacheProcessForOperation:operation url:url options:result.options context:result.context progress:progressBlock completed:completedBlock];

return operation;

} `` 这里,我们看到了manage的==第一层==的调用,这层调用可以直观的看出,返回了一个SDWebImageOperation图像加载操作,如果出错了(即如果链接地址不正确,或者之前加载失败过并且没有设置失败url`重试选项)就直接回调错误并返回。这一层下来,我们仍然没有看到它从内存获取或者从网路下载的实际过程,接下来,我们看它里面的第二层调用:

objectivec // 启动条目以从缓存加载图像 [self callCacheProcessForOperation:operation url:url options:result.options context:result.context progress:progressBlock completed:completedBlock]; 调用了

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; 我们看它的实现:

```objectivec // 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 // 获取要使用的图像缓存 id imageCache; if ([context[SDWebImageContextImageCache] conformsToProtocol:@protocol(SDImageCache)]) { imageCache = context[SDWebImageContextImageCache]; } else { 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) {// 查询缓存
    // 获取url对应的key
    NSString *key = [self cacheKeyForURL:url context:context];
    // 通过缓存对象查询url对应的缓存
    @weakify(operation);
    operation.cacheOperation = [imageCache queryImageForKey:key options:options context:context 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"}] url:url];
            [self safelyRemoveOperationFromRunning:operation];
            return;
        } else if (context[SDWebImageContextImageTransformer] && !cachedImage) {
            // Have a chance to query original cache instead of downloading
            // 有机会查询原始缓存而不是下载
            // 在该方法内部仍有机会触发下载方法
            [self callOriginalCacheProcessForOperation:operation url:url options:options context:context progress:progressBlock completed:completedBlock];
            return;
        }

        // Continue download process
        // 继续下载过程
        [self callDownloadProcessForOperation:operation url:url options:options context:context cachedImage:cachedImage cachedData:cachedData cacheType:cacheType progress:progressBlock completed:completedBlock];
    }];
} else { // 不查询缓存
    // Continue download process
    // 继续下载过程
    [self callDownloadProcessForOperation:operation url:url options:options context:context cachedImage:nil cachedData:nil cacheType:SDImageCacheTypeNone progress:progressBlock completed:completedBlock];
}

} ``` 我们可以看到,这一方法内部进行了更细的划分,将逻辑打散,一个方法接口,最终目的是回调一个完成块(completedBlock)。先分成两个主要逻辑部分: 第一个是应该查询缓存: 调用了

objectivec - (id<SDWebImageOperation>)queryImageForKey:(NSString *)key options:(SDWebImageOptions)options context:(SDWebImageContext *)context cacheType:(SDImageCacheType)cacheType completion:(SDImageCacheQueryCompletionBlock)completionBlock { if (!key) { return nil; } NSArray<id<SDImageCache>> *caches = self.caches; NSUInteger count = caches.count; if (count == 0) { return nil; } else if (count == 1) { return [caches.firstObject queryImageForKey:key options:options context:context cacheType:cacheType completion:completionBlock]; } switch (self.queryOperationPolicy) { case SDImageCachesManagerOperationPolicyHighestOnly: { id<SDImageCache> cache = caches.lastObject; return [cache queryImageForKey:key options:options context:context cacheType:cacheType completion:completionBlock]; } break; case SDImageCachesManagerOperationPolicyLowestOnly: { id<SDImageCache> cache = caches.firstObject; return [cache queryImageForKey:key options:options context:context cacheType:cacheType completion:completionBlock]; } break; case SDImageCachesManagerOperationPolicyConcurrent: { SDImageCachesManagerOperation *operation = [SDImageCachesManagerOperation new]; [operation beginWithTotalCount:caches.count]; [self concurrentQueryImageForKey:key options:options context:context cacheType:cacheType completion:completionBlock enumerator:caches.reverseObjectEnumerator operation:operation]; return operation; } break; case SDImageCachesManagerOperationPolicySerial: { SDImageCachesManagerOperation *operation = [SDImageCachesManagerOperation new]; [operation beginWithTotalCount:caches.count]; [self serialQueryImageForKey:key options:options context:context cacheType:cacheType completion:completionBlock enumerator:caches.reverseObjectEnumerator operation:operation]; return operation; } break; default: return nil; break; } } 通过一系列逻辑判断与调用,对图像数据进行了查询,最后一层的查询结束后,结束块返回了三个参数:UIImage * _Nullable cachedImage, NSData * _Nullable cachedData, SDImageCacheType cacheType。经过判断,如果可以从缓存获取图片,则返回,如无法从缓存获取图片,则进行下载。 第二个是不应该查询缓存: 不查询缓存则直接调用下载方法。 下面看看==下载的流程==:

```objectivec // SDWebImageManager.h // 下载流程 - (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 { // Grab the image loader to use // 抓取要使用的图像加载器 id imageLoader; if ([context[SDWebImageContextImageLoader] conformsToProtocol:@protocol(SDImageLoader)]) { imageLoader = context[SDWebImageContextImageLoader]; } else { imageLoader = self.imageLoader; }

// Check whether we should download image from network
// 检查我们是否应该从网络下载图像
BOOL shouldDownload = !SD_OPTIONS_CONTAINS(options, SDWebImageFromCacheOnly);
// SDWebImageFromCacheOnly:默认情况下,当缓存丢失时,将从加载程序加载映像。此标志可以防止仅从缓存加载。
shouldDownload &= (!cachedImage || options & SDWebImageRefreshCached);
shouldDownload &= (![self.delegate respondsToSelector:@selector(imageManager:shouldDownloadImageForURL:)] || [self.delegate imageManager:self shouldDownloadImageForURL:url]);
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.
        // 如果在缓存中找到图像,但提供了SDWebImageRefreshCached,请通知缓存的图像并尝试重新下载它,
        // 以便有机会通过NSURLCache从服务器刷新它。
        [self callCompletionBlockForOperation:operation completion:completedBlock image:cachedImage data:cachedData error:nil cacheType:cacheType finished:YES 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"}] url:url];
        } else if (cachedImage && options & SDWebImageRefreshCached && [error.domain isEqualToString:SDWebImageErrorDomain] && error.code == SDWebImageErrorCacheNotModified) {
            // SDWebImageErrorCacheNotModified : The remote location specify that the cached image is not modified, such as the HTTP response 304 code. It's useful for `SDWebImageRefreshCached`
            // 远程位置指定不修改缓存的图像,如 HTTP 响应 304 代码。它对于“SDWebImageRefreshCached”很有用
            // Image refresh hit the NSURLCache cache, do not call the completion block
            // 图像刷新命中 NSURL缓存,不调用完成块
        } else if ([error.domain isEqualToString:SDWebImageErrorDomain] && error.code == SDWebImageErrorCancelled) {
            // Download operation cancelled by user before sending the request, don't block failed URL
            // 用户在发送请求前取消下载操作,请勿阻止失败的URL
            [self callCompletionBlockForOperation:operation completion:completedBlock error:error url:url];
        } else if (error) {
            [self callCompletionBlockForOperation:operation completion:completedBlock error:error 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 store cache process
            // 继续存储缓存过程
            [self callStoreCacheProcessForOperation:operation url:url options:options context:context downloadedImage:downloadedImage downloadedData:downloadedData finished:finished progress:progressBlock 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 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 url:url];
    [self safelyRemoveOperationFromRunning:operation];
}

} ``` 我们重点关注其中开启下载任务的部分,调用了

objectivec // SDWebImageLoadersManager.m - (id<SDWebImageOperation>)requestImageWithURL:(NSURL *)url options:(SDWebImageOptions)options context:(SDWebImageContext *)context progress:(SDImageLoaderProgressBlock)progressBlock completed:(SDImageLoaderCompletedBlock)completedBlock { // url不存在 if (!url) { return nil; } NSArray<id<SDImageLoader>> *loaders = self.loaders; for (id<SDImageLoader> loader in loaders.reverseObjectEnumerator) { if ([loader canRequestImageForURL:url]) { return [loader requestImageWithURL:url options:options context:context progress:progressBlock completed:completedBlock]; } } return nil; } 如果url正常,则继续调用

objectivec // SDWebImageDownloader.m - (id<SDWebImageOperation>)requestImageWithURL:(NSURL *)url options:(SDWebImageOptions)options context:(SDWebImageContext *)context progress:(SDImageLoaderProgressBlock)progressBlock completed:(SDImageLoaderCompletedBlock)completedBlock { UIImage *cachedImage = context[SDWebImageContextLoaderCachedImage]; // 创建变量保存图像下载的选项 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 // 忽视从NSURLCache中获取缓存 downloaderOptions |= SDWebImageDownloaderIgnoreCachedResponse; } // 开启下载任务并获取下载token return [self downloadImageWithURL:url options:downloaderOptions context:context progress:progressBlock completed:completedBlock]; } 接下来查看最后调用的方法:

objectivec // SDWebImageDownloader.m - (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 将用作回调字典的键,因此它不能为 nil。如果为 nil,请立即调用没有图像或数据的已完成块。 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; } // 加锁 SD_LOCK(self.operationsLock); id downloadOperationCancelToken; // 从缓存中获取url对应的操作对象 // 尝试复用之前生成的 operation 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`. // 有一种情况是,该操作可能被标记为已完成或已取消,但未从“self”中删除。URLOperations'. if (!operation || operation.isFinished || operation.isCancelled) { // 创建出新的 operation 并存储在 URLOperations 中 。 // 同时会配置 completionBlock,使得任务完成后可以及时清理 URLOperations。 // 保存 progressBlock 和 completedBlock; // 提交 operation 到 downloadQueue。 operation = [self createDownloaderOperationWithUrl:url options:options context:context]; if (!operation) { SD_UNLOCK(self.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 = ^{ // 操作完成时,从缓存中移除该url对应的操作对象 @strongify(self); if (!self) { return; } SD_LOCK(self.operationsLock); [self.URLOperations removeObjectForKey:url]; SD_UNLOCK(self.operationsLock); }; // 缓存url和其对应的操作对象 self.URLOperations[url] = operation; // Add the handlers before submitting to operation queue, avoid the race condition that operation finished before setting handlers. // 提交到操作队列前添加处理程序,避免设置处理程序前操作完成的争用条件。 downloadOperationCancelToken = [operation addHandlersForProgress:progressBlock completed:completedBlock]; // Add operation to operation queue only after all configuration done according to Apple's doc. // 只有在根据Apple的文档完成所有配置后,才能将操作添加到操作队列中。 // `addOperation:` does not synchronously execute the `operation.completionBlock` so this will not cause deadlock. // “addOperation:”不会同步执行 “operation.completeBlock”,因此这不会导致死锁。 // 将操作对象添加到操作队列中 [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. // 因此,我们在这里锁定操作,在“SDWebImageDownloaderOperation”中,我们使用“@synchonzied(self)”来确保这两个类之间的线程安全。 @synchronized (operation) { downloadOperationCancelToken = [operation addHandlersForProgress:progressBlock completed:completedBlock]; // 这里 addHandlersForProgress: 会将 progressBlock 与 completedBlock 一起存入 // NSMutableDictionary<NSString *, id> SDCallbacksDictionary 然后返回保存在 // downloadOperationCancelToken 中。 // 另外,Operation 在 addHandlersForProgress: // 时并不会清除之前存储的 callbacks 是增量保存的, // 也就是说多次调用的 callBack 在完成后都会被依次执行。 } if (!operation.isExecuting) { if (options & SDWebImageDownloaderHighPriority) { operation.queuePriority = NSOperationQueuePriorityHigh; } else if (options & SDWebImageDownloaderLowPriority) { operation.queuePriority = NSOperationQueuePriorityLow; } else { operation.queuePriority = NSOperationQueuePriorityNormal; } } } // 解锁 SD_UNLOCK(self.operationsLock); // 生成下载token SDWebImageDownloadToken *token = [[SDWebImageDownloadToken alloc] initWithDownloadOperation:operation]; token.url = url; token.request = operation.request; token.downloadOperationCancelToken = downloadOperationCancelToken; // 最终 operation、url、request、downloadOperationCancelToken // 一起被打包进 SDWebImageDownloadToken, 下载方法结束。 // 返回token return token; } 看一眼下面else里一大堆注释那的- (nullable id)addHandlersForProgress:(nullable SDWebImageDownloaderProgressBlock)progressBlock completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock方法的实现: objectivec // SDWebImageDownloaderOperation.m static NSString *const kProgressCallbackKey = @"progress"; static NSString *const kCompletedCallbackKey = @"completed"; typedef NSMutableDictionary<NSString *, id> SDCallbacksDictionary; @property (strong, nonatomic, nonnull) NSMutableArray<SDCallbacksDictionary *> *callbackBlocks; // 下载进度 完成 callback 存储到 数组中(callbackBlocks)可能会空 // 实现高吞吐量的数据读写保证线程安全 - (nullable id)addHandlersForProgress:(nullable SDWebImageDownloaderProgressBlock)progressBlock completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock { SDCallbacksDictionary *callbacks = [NSMutableDictionary new]; if (progressBlock) callbacks[kProgressCallbackKey] = [progressBlock copy]; if (completedBlock) callbacks[kCompletedCallbackKey] = [completedBlock copy]; @synchronized (self) { [self.callbackBlocks addObject:callbacks]; } return callbacks; }

看回去,之前的方法中调用的- (nullable NSOperation<SDWebImageDownloaderOperation> *)createDownloaderOperationWithUrl:(nonnull NSURL *)url options:(SDWebImageDownloaderOptions)options context:(nullable SDWebImageContext *)context方法:

```objectivec // SDWebImageDownloader.m - (nullable NSOperation )createDownloaderOperationWithUrl:(nonnull NSURL )url options:(SDWebImageDownloaderOptions)options context:(nullable SDWebImageContext *)context { // 获取下载请求超时时间,默认15秒 NSTimeInterval timeoutInterval = self.config.downloadTimeout; if (timeoutInterval == 0.0) { timeoutInterval = 15.0; }

// In order to prevent from potential duplicate caching (NSURLCache + SDImageCache) we disable the cache for image requests if told otherwise
// 为了防止潜在的重复缓存(NSURLCache + SDImageCache),如果另有说明,我们将禁用图像请求的缓存
// 根据设置的选项不同,设置不同的请求缓存策略
NSURLRequestCachePolicy cachePolicy = options & SDWebImageDownloaderUseNSURLCache ? NSURLRequestUseProtocolCachePolicy : NSURLRequestReloadIgnoringLocalCacheData;
// 创建请求对象
NSMutableURLRequest *mutableRequest = [[NSMutableURLRequest alloc] initWithURL:url cachePolicy:cachePolicy timeoutInterval:timeoutInterval];
// Cookies处理方式
mutableRequest.HTTPShouldHandleCookies = SD_OPTIONS_CONTAINS(options, SDWebImageDownloaderHandleCookies);
// 默认使用管线化
mutableRequest.HTTPShouldUsePipelining = YES;
// 设置HTTP请求头字段
SD_LOCK(self.HTTPHeadersLock);
mutableRequest.allHTTPHeaderFields = self.HTTPHeaders;
SD_UNLOCK(self.HTTPHeadersLock);

// Context Option
// 上下文选项
SDWebImageMutableContext *mutableContext;
if (context) {
    mutableContext = [context mutableCopy];
} else {
    mutableContext = [NSMutableDictionary dictionary];
}

// Request Modifier
// 从 imageContext 中取出 id<SDWebImageDownloaderRequestModifier> requestModifier 对 request 进行改造。
// 值得注意的是 requestModifier 的获取是有优先级的,
// 通过 imageContext 得到的优先级高于 downloader 所拥有的。
// 通过这种方既满足了接口调用方可控,又能支持全局配置
// 同理,id<SDWebImageDownloaderResponseModifier> responseModifier 、
//      id<SDWebImageDownloaderDecryptor> decryptor 也是如此。
id<SDWebImageDownloaderRequestModifier> requestModifier;
if ([context valueForKey:SDWebImageContextDownloadRequestModifier]) {
    requestModifier = [context valueForKey:SDWebImageContextDownloadRequestModifier];
} else {
    requestModifier = self.requestModifier;
}

NSURLRequest *request;
if (requestModifier) {
    NSURLRequest *modifiedRequest = [requestModifier modifiedRequestWithRequest:[mutableRequest copy]];
    // If modified request is nil, early return
    if (!modifiedRequest) {
        return nil;
    } else {
        request = [modifiedRequest copy];
    }
} else {
    request = [mutableRequest copy];
}
// Response Modifier
// 响应修饰符
id<SDWebImageDownloaderResponseModifier> responseModifier;
if ([context valueForKey:SDWebImageContextDownloadResponseModifier]) {
    responseModifier = [context valueForKey:SDWebImageContextDownloadResponseModifier];
} else {
    responseModifier = self.responseModifier;
}
if (responseModifier) {
    mutableContext[SDWebImageContextDownloadResponseModifier] = responseModifier;
}
// Decryptor
// 解密器
id<SDWebImageDownloaderDecryptor> decryptor;
if ([context valueForKey:SDWebImageContextDownloadDecryptor]) {
    decryptor = [context valueForKey:SDWebImageContextDownloadDecryptor];
} else {
    decryptor = self.decryptor;
}
if (decryptor) {
    mutableContext[SDWebImageContextDownloadDecryptor] = decryptor;
}

context = [mutableContext copy];

// Operation Class
// 最后,从 downloaderConfig 中取出 operationClass 创建 operation
Class operationClass = self.config.operationClass;
if (operationClass && [operationClass isSubclassOfClass:[NSOperation class]] && [operationClass conformsToProtocol:@protocol(SDWebImageDownloaderOperation)]) {
    // Custom operation class
    // 自定义操作类
} else {
    operationClass = [SDWebImageDownloaderOperation class];
}
NSOperation<SDWebImageDownloaderOperation> *operation = [[operationClass alloc] initWithRequest:request inSession:self.session options:options context:context];

if ([operation respondsToSelector:@selector(setCredential:)]) {
    if (self.config.urlCredential) {
        operation.credential = self.config.urlCredential;
    } else if (self.config.username && self.config.password) {
        operation.credential = [NSURLCredential credentialWithUser:self.config.username password:self.config.password persistence:NSURLCredentialPersistenceForSession];
    }
}

if ([operation respondsToSelector:@selector(setMinimumProgressInterval:)]) {
    operation.minimumProgressInterval = MIN(MAX(self.config.minimumProgressInterval, 0), 1);
}

if (options & SDWebImageDownloaderHighPriority) {
    operation.queuePriority = NSOperationQueuePriorityHigh;
} else if (options & SDWebImageDownloaderLowPriority) {
    operation.queuePriority = NSOperationQueuePriorityLow;
}

if (self.config.executionOrder == SDWebImageDownloaderLIFOExecutionOrder) {
    // Emulate LIFO execution order by systematically, each previous adding operation can dependency the new operation
    // 通过系统地模拟后进先出的执行顺序,每个先前添加的操作都可以依赖于新操作
    // This can gurantee the new operation to be execulated firstly, even if when some operations finished, meanwhile you appending new operations
    // 这可以保证首先执行新操作,即使某些操作完成,同时附加新操作
    // Just make last added operation dependents new operation can not solve this problem. See test case #test15DownloaderLIFOExecutionOrder
    // 只是让最后添加的操作依赖新操作不能解决这个问题。查看测试用例#test15DownloaderLIFOExecutionOrder
    for (NSOperation *pendingOperation in self.downloadQueue.operations) {
        [pendingOperation addDependency:operation];
    }
    //默认情况下,每个任务是按照 FIFO 顺序添加到 downloadQueue 中,
    //如果用户设置的是 LIFO 时,添加进队列前会修改队列中现有任务的优先级来达到效果
    //通过遍历队列,将新任务修改为当前队列中所有任务的依赖以反转优先级
}

return operation;

} `` 总体看下来,SDWebImageDownloader这个类从字面上理解是一个下载器类,实际看代码,发现这个类并没有做实际的下载,而是维护着一个NSURLSession对象和一个NSOperationQueue对象,管理着一些SDWebImageDownloaderOperation对象。实际的下载应该是在SDWebImageDownloaderOperation类里完成的。这个是后话了。SDWebImageDownloader通过传入的参数生成一个SDWebImageDownloaderOperation对象,并添加到操作队列中。具体的网络下载操作则由SDWebImageDownloaderOperation`对象自己处理。 接下来回到前面的

objectivec - (void)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; 方法里面。在loadImageWithURL:options:context:progress:completed:的回调block中,接收到了如下参数:UIImage *image, NSData *data, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL。如完成,则停止小菊花。 接下来进行判断:

objectivec // 如果完成,或者没完成但是设置了手动设置图像的选项,就需要调用完成block BOOL shouldCallCompletedBlock = finished || (options & SDWebImageAvoidAutoSetImage); // 如果有图并且设置了手动设置图像的选项, // 或者没有图并且没设置延迟加载占位图的选项, // 就不需要设置图像 BOOL shouldNotSetImage = ((image && (options & SDWebImageAvoidAutoSetImage)) || (!image && !(options & SDWebImageDelayPlaceholder))); 接下来判断是否需要图像以及需要的图像是什么:

objectivec // 如果不需要设置图像就直接回调完成情况 if (shouldNotSetImage) { dispatch_main_async_safe(callCompletedBlockClojure); 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; }

再对加载动画进行操作,最后在主线程刷新:

```objectivec 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

callCompletedBlockClojure();

}); `` 以上就是实现的主要流程。 在SDWebImageManager核心方法中涉及到两个重要的类SDWebImageDownloaderSDImageCache,下面将进行讲解: <font size=4 color="orange" >**SDWebImageDownloader**</font> 这个类的方法我们前面已经看过一些了,也说了具体的网络下载操作是由SDWebImageDownloaderOperation对象处理。图片的下载是放在一个NSOperationQueue`操作队列中来完成的,其声明如下:

objectivec @property (strong, nonatomic, nonnull) NSOperationQueue *downloadQueue; 默认情况下,队列最大并发数是6。如果需要的话,可以通过SDWebImageDownloader类的maxConcurrentDownloads属性来修改。图片的下载回调通过block实现。

objectivec // SDImageDownLoader.h typedef void(^SDImageLoaderProgressBlock)(NSInteger receivedSize, NSInteger expectedSize, NSURL * _Nullable targetURL); typedef void(^SDImageLoaderCompletedBlock)(UIImage * _Nullable image, NSData * _Nullable data, NSError * _Nullable error, BOOL finished); // SDWebImageDownloader.h typedef SDImageLoaderProgressBlock SDWebImageDownloaderProgressBlock; typedef SDImageLoaderCompletedBlock SDWebImageDownloaderCompletedBlock; 我们看一下- (nullable NSOperation<SDWebImageDownloaderOperation> *)createDownloaderOperationWithUrl:(nonnull NSURL *)url options:(SDWebImageDownloaderOptions)options context:(nullable SDWebImageContext *)context方法的代码,里面有这么一行:

objectivec // 创建下载操作对象 NSOperation<SDWebImageDownloaderOperation> *operation = [[operationClass alloc] initWithRequest:request inSession:self.session options:options context:context]; // 初始化没有什么特别的,需要注意的是这里传入的 nullable session 是以 unownedSessin 保存, // 区别于内部默认生成的 ownedSession。如果初始化时 session 为空,会在 start 时创建 ownedSession。 这个operation其实就是我们下载操作的对象。这里使用了NSOperation对象来进行多线程工作。这里的operation对象是属于NSOperation的子类SDWebImageDownloaderOperation。我们简单回顾一下NSOperation的一些要点: - NSOperation有两个方法:main()start()。如果想使用同步,那么最简单方法的就是把逻辑写在main()中,使用异步,需要把逻辑写到start()中,然后加入到队列之中。 - NSOperation什么时候执行呢?按照正常想法,是手动调用main()start(),当然这样也可以。当调用start()的时候,默认的是在当前线程执行同步操作,如果是在主线程调用了,那么必然会导致程序死锁。另外一种方法就是加入到operationQueue中,operationQueue会尽快执行NSOperation,如果operationQueue是同步的,那么它会等到NSOperationisFinished等于YES后,再执行下一个任务,如果是异步的,通过设置maxConcurrentOperationCount来控制同时执行的最大操作,某个操作完成后,继续其他的操作。当然如果NSOperationQueuemaxConcurrentOperationCount如果设置为1的话,进相当于FIFO了。 - 队列是怎么调用我们的执行的操作的呢?如果你只是想弄一个同步的方法,那很简单,你只要重写main()这个函数,在里面添加你要的操作。如果想定义异步的方法的话就重写start()方法。在你添加进operationQueue中的时候系统将自动调用你这个start()方法,这时将不再调用main里面的方法。 - 并不是调用了cancel就一定取消了,如果NSOperation没有执行,那么就会取消,如果执行了,只会将isCancelled设置为YES。所以,在我们的操作中,我们应该在每个操作开始前,或者在每个有意义的实际操作完成后,先检查下这个属性是不是已经设置为YES。如果是YES,则后面操作都可以不用再执行了。

通过观察- (nullable SDWebImageDownloadToken *)downloadImageWithURL:(nullable NSURL *)url options:(SDWebImageDownloaderOptions)options context:(nullable SDWebImageContext *)context progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock的代码,我们可以看出来,SDWebImageDownloaderoperation加到队列中执行:

objectivec // 将操作对象添加到操作队列中 [self.downloadQueue addOperation:operation]; 上面两行代码就是SDWebImageDownloader开启下载图片任务的==关键==: 1. 生成下载任务的Operation操作; 2. 将Operation操作加入到队列中,实现异步任务下载;

SDWebImageDownloaderOperation 我们接下来看一看SDWebImageDownloaderOperation重写的start()方法:

```objectivec // 当SDWebImageDownloaderOperation添加到并发队列,就会调用start方法。 - (void)start { @synchronized (self) { // 确认当前下载状态 if (self.isCancelled) { self.finished = YES; // 如果当前下载状态是完成 则设置下载完成状态为YES 并且结束下载任务 // Operation cancelled by user before sending the request [self callCompletionBlocksWithError:[NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorCancelled userInfo:@{NSLocalizedDescriptionKey : @"Operation cancelled by user before sending the request"}]]; [self reset]; return; }

if SD_UIKIT

    // 注册后台任务
    Class UIApplicationClass = NSClassFromString(@"UIApplication");
    BOOL hasApplication = UIApplicationClass && [UIApplicationClass respondsToSelector:@selector(sharedApplication)];
    if (hasApplication && [self shouldContinueWhenAppEntersBackground]) {
        __weak typeof(self) wself = self;
        UIApplication * app = [UIApplicationClass performSelector:@selector(sharedApplication)];
        self.backgroundTaskId = [app beginBackgroundTaskWithExpirationHandler:^{
            [wself cancel];
        }];
    }

endif

    NSURLSession *session = self.unownedSession;
    if (!session) {
        NSURLSessionConfiguration *sessionConfig = [NSURLSessionConfiguration defaultSessionConfiguration];
        sessionConfig.timeoutIntervalForRequest = 15;

        /**
         *  Create the session for this task
         *  We send nil as delegate queue so that the session creates a serial operation queue for performing all delegate
         *  method calls and completion handler calls.
         */
        session = [NSURLSession sessionWithConfiguration:sessionConfig
                                                delegate:self
                                           delegateQueue:nil];
        self.ownedSession = session;
    }

    if (self.options & SDWebImageDownloaderIgnoreCachedResponse) {
        // Grab the cached data for later check
        NSURLCache *URLCache = session.configuration.URLCache;
        if (!URLCache) {
            URLCache = [NSURLCache sharedURLCache];
        }
        NSCachedURLResponse *cachedResponse;
        // NSURLCache's `cachedResponseForRequest:` is not thread-safe, see https://developer.apple.com/documentation/foundation/nsurlcache#2317483
        @synchronized (URLCache) {
            cachedResponse = [URLCache cachedResponseForRequest:self.request];
        }
        if (cachedResponse) {
            self.cachedData = cachedResponse.data;
        }
    }

    self.dataTask = [session dataTaskWithRequest:self.request];
    self.executing = YES;
}

if (self.dataTask) {
    if (self.options & SDWebImageDownloaderHighPriority) {
        self.dataTask.priority = NSURLSessionTaskPriorityHigh;
        self.coderQueue.qualityOfService = NSQualityOfServiceUserInteractive;
    } else if (self.options & SDWebImageDownloaderLowPriority) {
        self.dataTask.priority = NSURLSessionTaskPriorityLow;
        self.coderQueue.qualityOfService = NSQualityOfServiceBackground;
    } else {
        self.dataTask.priority = NSURLSessionTaskPriorityDefault;
        self.coderQueue.qualityOfService = NSQualityOfServiceDefault;
    }
    // 开始下载任务
    [self.dataTask resume];
    for (SDWebImageDownloaderProgressBlock progressBlock in [self callbacksForKey:kProgressCallbackKey]) {
        progressBlock(0, NSURLResponseUnknownLength, self.request.URL);
    }
    __block typeof(self) strongSelf = self;
    // 下载开始通知
    dispatch_async(dispatch_get_main_queue(), ^{
        [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStartNotification object:strongSelf];
    });
} else {
    // 如果下载任务实例化失败,则以错误的状态调用completedBlock回调
    [self callCompletionBlocksWithError:[NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorInvalidDownloadOperation userInfo:@{NSLocalizedDescriptionKey : @"Task can't be initialized"}]];
    [self done];
}

} `` 本类遵守了NSURLSessionTaskDelegateNSURLSessionDataDelegate两个协议,并实现了代理方法。NSURLSessionDataDelegate`是 task 级别的协议,主要用来处理 data 和 upload,如接收到响应,接收到数据,是否缓存数据。

objectivec // 每次返回新的数据时会调用该方法,开发者需要在该方法中合理地处理先前的数据, // 否则会被新数据覆盖。如果没有提供该方法的实现,那么session将会继续任务,也就是说会覆盖之前的数据。 - (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveResponse:(NSURLResponse *)response completionHandler:(void (^)(NSURLSessionResponseDisposition disposition))completionHandler;didReceiveResponse 时,会保存 response.expectedContentLength 作为 expectedSize。然后调用 modifiedResponseWithResponse: 保存编辑后的 reponseobjectivec // 客户端已收到服务器返回的部分数据 // data 自上次调用以来收到的数据 // 该方法可能被多次调用,并且每次调用只提供自上次调用以来收到的数据; // 因此 NSData 通常是由许多不同的data拼凑在一起的,所以尽量使用 [NSData enumerateByteRangesUsingBlock:] 方法迭代数据, // 而非 [NSData getBytes] - (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data;

每次 didReceiveData 会将 data 追加到 imageData[self.imageData appendData:data] ,更新 receivedSizeself.receivedSize = self.imageData.length 。最终,当 receivedSize > expectedSize 判定下载完成,执行后续处理。如果你支持了 SDWebImageDownloaderProgressiveLoad,每当收到数据时,将会进入 coderQueue 进行边下载边解码:

```objectivec NSData *imageData = [self.imageData copy];

// keep maximum one progressive decode process during download if (self.coderQueue.operationCount == 0) { // NSOperation have autoreleasepool, don't need to create extra one [self.coderQueue addOperationWithBlock:^{ UIImage image = SDImageLoaderDecodeProgressiveImageData(imageData, self.request.URL, finished, self, [[self class] imageOptionsFromDownloaderOptions:self.options], self.context); if (image) { // We do not keep the progressive decoding image even when finished=YES. Because they are for view rendering but not take full function from downloader options. And some coders implementation may not keep consistent between progressive decoding and normal decoding. [self callCompletionBlocksWithImage:image imageData:nil error:nil finished:NO]; } }]; } `` 否则,在- (void)URLSession:(NSURLSession )session task:(NSURLSessionTask )task didCompleteWithError:(NSError )error;`(后面有)方法里解码:

objectivec if (imageData && self.decryptor) { imageData = [self.decryptor decryptedDataWithData:imageData response:self.response]; }

objectivec // 当 dataTask 接收完所有数据后,session会调用该方法,主要是防止缓存指定的URL或修改与 NSCacheURLResponse 相关联的字典userInfo // 如果没有实现该方法,那么使用 configuration 决定缓存策略 // proposedResponse 默认的缓存行为;根据当前缓存策略和响应头的某些字段,如 Pragma 和 Cache-Control 确定 // completionHandler 缓存数据;传递 nil 不做缓存 // 不应该依赖该方法来接收数据,只有 NSURLRequest.cachePolicy 决定缓存 response 时候调用该方法: // 只有当以下所有条件都成立时,才会缓存 responses: // 是HTTP或HTTPS请求,或者自定义的支持缓存的网络协议; // 确保请求成功,响应头的状态码在200-299范围内 // response 是来自服务器的,而非缓存中本身就有的 // NSURLRequest.cachePolicy 允许缓存 // NSURLSessionConfiguration.requestCachePolicy 允许缓存 // 响应头的某些字段 如 Pragma 和 Cache-Control 允许缓存 // response 不能比提供的缓存空间大太多,如不能比提供的磁盘缓存空间还要大5% - (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask willCacheResponse:(NSCachedURLResponse *)proposedResponse completionHandler:(void (^)(NSCachedURLResponse * _Nullable cachedResponse))completionHandler; NSURLSessionTaskDelegateNSURLSessionTask 级别的委托。

objectivec // 已完成传输数据的任务,调用该方法 // error 客户端错误,例如无法解析主机名或连接到主机; // 服务器错误不会在此处显示; // 为 nil 表示没有发生错误,此任务已成功完成 - (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error; 在这个代理方法里,无error,则进行一系列判断,也会对接受的imageData使用SDImageLoader代理的SDImageLoaderDecodeImageData()方法解码。

我们就明白了,实际的下载过程是在这里完成的,只是经过一层层的调用,隐藏得太好了。主要过程总结为: 1. 下载的图片会通过didReceiveData函数不断返回图片data数据,将其拼接到imageData中; 2. 若设置了图片按SDWebImageDownloaderProgressiveDownload渐进式显示,则会将当前图片解压并显示到屏幕上; 3. 这个图片下载完成时,调用didCompleteWithError函数,此时将生成的图片解压缩并设置到屏幕上;

在继续进行之前,我们来浅看一下SDImageLoaderDecodeImageData()方法,它定义在SDImageLoader文件里,经过一系列判断后,他调用了SDImageIOCoder.m里的- (UIImage *)decodedImageWithData:(NSData *)data options:(nullable SDImageCoderOptions *)options;。观察一下SDImageIOCoder类,发现这个是编解码器类,提供了对PNG、JPEG、TIFF类型图片的编解码,还支持逐行解码。重要的是里面有对SDWebImageProgressiveCoder协议方法的实现。主要有解码、编码、解压和缩小的功能,下面我们就梳理一下各个功能的逻辑:

解码 这个功能实现比较简单,主要是利用了UIImage类的initWithData:和initWithCGImage: scale: orientation:这两个对象方法: - 首先判断是否有图像数据; - 接着利用图像数据生成图片对象; - 然后判断图片对象是否生成成功; - 再获取图像方向,如果方向不是向上,就根据方向生成图片对象; - 最后返回生成的图片对象

编码 这个功能涉及到了对ImageIO库中CGImageDestinationRef的使用,主要是三个步骤: 1. 利用CGImageDestinationCreateWithData()函数创建图像目的地 2. 使用CGImageDestinationAddImage()函数将图片对象添加到图像目的地中 3. 调用CGImageDestinationFinalize()函数将图片对象写入到数据对象中

具体的逻辑是: - 首先判断是否有图片对象; - 接着判断图片对象的格式; - 然后创建图像目的地对象; - 接着配置编码的属性,即图像的方向; - 然后将图片对象和属性添加到图像目的地对象中编码; - 最后返回编码后的数据;

解压 主要步骤是: 1. 使用CGBitmapContextCreate()函数创建位图图像上下文; 2. 利用CGContextDrawImage()函数将图片绘制到位图图像上下文中; 3. 调用CGBitmapContextCreateImage()函数从位图图像上下文中获取位图图像;

具体逻辑是: - 首先判断是否应该解码,如果图片对象不存在、是动图、有透明度就不解码; - 接下来创建一个自动释放池,以便在内存不足时清除缓存; - 然后创建一个位图图像上下文; - 接着就把图像绘制到位图图像上下文中; - 再从位图图像上下文中获取绘制好的位图对象; - 最后利用位图对象生成图片对象并返回;

解压并缩小 具体逻辑: - 首先判断是否应该解码,如果图片对象不存在、是动图、有透明度就不解码; - 然后判断是否应该缩小,如果图像的像素数量不超过指定数量就只解压不缩小; - 接下来创建一个自动释放池,以便在内存不足时清除缓存; - 接着创建一个位图图像上下文; - 然后创建一个循环,依次从原图像中获取一片宽度和原图相等,高度是指定数值,横坐标为0,纵坐标递增的图像,绘制到位图图像上下文的指定位置和大小,直至获取完原图。 - 再从位图图像上下文中获取绘制好的位图对象; - 最后利用位图对象生成图片对象并返回;

其中缩小的功能实现的思想是,将原图分片缩小。每次获取原图中的一片进行缩小,直至全部缩小完成。 参考博客:源码阅读:SDWebImage(七)——SDWebImageImageIOCoder

接下来再看看SDImageCacheSDImageCache 这个类是SDWebImage中负责缓存相关功能的类。功能包括保存、查询、删除等相关操作,逻辑很清晰。查看注释:SDImageCache 维护内存缓存和磁盘缓存。磁盘缓存写入操作是异步执行的,因此不会给 UI 增加不必要的延迟。这个类还有个私有类SDMemoryCache,它继承自自NSCache,负责管理图像的内存缓存,其内部就是将 NSCache 扩展为了 SDMemoryCache 协议,并为iOS/tvOS平台添加了@property (nonatomic, strong, nonnull) NSMapTable<KeyType, ObjectType> *weakCache; // strong-weak cache,并为其添加了信号量锁来保证线程安全。 weakCache 的作用在于恢复缓存,它通过 CacheConfigshouldUseWeakMemoryCache 开关以控制。先看看其如何实现的:

objectivec - (id)objectForKey:(id)key { id obj = [super objectForKey:key]; if (!self.config.shouldUseWeakMemoryCache) { return obj; } if (key && !obj) { // Check weak cache SD_LOCK(self.weakCacheLock); obj = [self.weakCache objectForKey:key]; SD_UNLOCK(self.weakCacheLock); if (obj) { // Sync cache NSUInteger cost = 0; if ([obj isKindOfClass:[UIImage class]]) { cost = [(UIImage *)obj sd_memoryCost]; } [super setObject:obj forKey:key cost:cost]; } } return obj; } 由于 NSCache 遵循 NSDiscardableContent 策略来存储临时对象的,当内存紧张时,缓存对象有可能被系统清理掉。此时,如果应用访问 MemoryCache 时,缓存一旦未命中,则会转入 diskCache 的查询操作,可能导致 image 闪烁现象。而当开启 shouldUseWeakMemoryCache 时,因为 weakCache 保存着对象的弱引用 (在对象 被 NSCache 被清理且没有被释放的情况下),我们可通过 weakCache 取到缓存,将其放回 NSCache 中。从而减少磁盘 I/O。 SDImageCache实现了:

objectivec - (void)storeImage:(nullable UIImage *)image imageData:(nullable NSData *)imageData forKey:(nullable NSString *)key toMemory:(BOOL)toMemory toDisk:(BOOL)toDisk completion:(nullable SDWebImageNoParamsBlock)completionBlock; 方法。该方法将图像异步缓存到指定密钥下的内存中,可以选择是否缓存到磁盘中,并且可以直接将图像的数据保存到磁盘中,就不必再通过将原图像编码后获取图像的数据再保存到磁盘中,以节省硬件资源。在SDImageDefine.h文件中定义了缓存类型:

objectivec // Image Cache Type typedef NS_ENUM(NSInteger, SDImageCacheType) { /** * For query and contains op in response, means the image isn't available in the image cache * For op in request, this type is not available and take no effect. */ /* 对于查询并在响应中包含 op,表示图像在图像缓存中不可用。 对于请求中的操作,此类型不可用且无效。 */ SDImageCacheTypeNone, /** * For query and contains op in response, means the image was obtained from the disk cache. * For op in request, means process only disk cache. */ /* 对于查询和包含 op 的响应,表示映像是从磁盘缓存中获取的。 对于请求中的操作,表示仅处理磁盘缓存。 */ SDImageCacheTypeDisk, /** * For query and contains op in response, means the image was obtained from the memory cache. * For op in request, means process only memory cache. */ /* 对于查询和包含 op 的响应,表示图像是从内存缓存中获取的。 对于请求中的 op,表示仅处理内存缓存。 */ SDImageCacheTypeMemory, /** * For query and contains op in response, this type is not available and take no effect. * For op in request, means process both memory cache and disk cache. */ /* 对于查询和包含 op 的响应,此类型不可用且不起作用。 对于请求中的 op,表示处理内存缓存和磁盘缓存。 */ SDImageCacheTypeAll }; 进行 memoryCache 写入:

objectivec // 如果需要缓存到内存 if (toMemory && self.config.shouldCacheImagesInMemory) { NSUInteger cost = image.sd_memoryCost; [self.memoryCache setObject:image forKey:key cost:cost]; } 进行 diskCache 写入,操作逻辑放入 ioQueueautoreleasepool 中:

objectivec dispatch_async(self.ioQueue, ^{ @autoreleasepool { NSData *data = ... // 根据 SDImageFormat 对 image 进行编码获取 if (!data && image) { //Check image's associated image format, may return .undefined //... data = [[SDImageCodersManager sharedManager] encodedDataWithImage:image format:format options:nil]; } [self _storeImageDataToDisk:data forKey:key]; if (image) { // Check extended data id extendedObject = image.sd_extendedObject; // ... get extended data [self.diskCache setExtendedData:extendedData forKey:key]; } } // call completionBlock in main queue }); 另一个重要的方法就是 image query,定义在 SDImageCache 协议中:

```objectivec - (id)queryImageForKey:(NSString )key options:(SDWebImageOptions)options context:(nullable SDWebImageContext )context cacheType:(SDImageCacheType)cacheType completion:(nullable SDImageCacheQueryCompletionBlock)completionBlock { 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];

} `` 它只做了一件事情,将SDWebImageOptions转换为SDImageCacheOptions,然后调用queryCacheOperationForKey:SDImageCacheDefine.m`文件中实现了图片的解码方法:

objectivec UIImage * _Nullable SDImageCacheDecodeImageData(NSData * _Nonnull imageData, NSString * _Nonnull cacheKey, SDWebImageOptions options, SDWebImageContext * _Nullable context);

内存缓存: SDMemoryCache继承自NSCache。内存缓存的处理是使用 NSCache 对象来实现的。NSCache 是一个类似于集合的容器。它存储 key-value 对,这一点类似于 NSDictionary 类。 我们通常用使用缓存来临时存储短时间使用但创建昂贵的对象。 重用这些对象可以优化性能,因为它们的值不需要重新计算。另外一方面,这些对象对于程序来说不是紧要的,在内存紧张时会被丢弃。 磁盘缓存: 查看了SDDiskCache文件,它是磁盘缓存类,继承自NSObject,它通过@property (nonatomic, strong, nonnull) NSFileManager *fileManager;一个NSFileManager对象来实现缓存操作。SDImageCache 提供了大量方法来缓存、获取、移除及清空图片。而对于每个图片,为了方便地在内存或磁盘中对它进行这些操作,我们需要一个 key 值来索引它。 在内存中,我们将其作为 NSCachekey 值,而在磁盘中,我们用这个 key 作为图片的文件名。 对于一个远程服务器下载的图片,其 url 是作为这个 key 的最佳选择了。浅看一下过期缓存的清理: 1. 根据 SDImageCacheConfigExpireType 排序得到 NSDirectoryEnumerator *fileEnumerator ,开始过滤; 2. 以 cacheConfig.maxDiskAage 对比判断是否过期,将过期 URL 存入 urlsToDelete; 3. 调用 [self.fileManager removeItemAtURL:fileURL error:nil]; 4. 根据 cacheConfig.maxDiskSize 来删除磁盘缓存的数据,清理到 maxDiskSize 的 1/2 为止。

磁盘查询:

objectivec - (nullable NSData *)diskImageDataBySearchingAllPathsForKey:(nullable NSString *)key { if (!key) { return nil; } NSData *data = [self.diskCache dataForKey:key]; if (data) { return data; } // Addtional cache path for custom pre-load cache if (self.additionalCachePathBlock) { NSString *filePath = self.additionalCachePathBlock(key); if (filePath) { data = [NSData dataWithContentsOfFile:filePath options:self.config.diskCacheReadingOptions error:nil]; } } return data; }

  • 按图片的名称key拼接上定义在存储路径下的全路径,按照全路径在磁盘中搜索是否存在图片;
  • 除了生成存储的路径外,我们还可以自定义自己的存放路径到磁盘中,按照当前图片的名称key拼接上自定义路径到磁盘中搜索;
  • 若查找到的话,是一个NSData类型的图片数据,需转为UIImage对象之后,需将其解压到屏幕上显示。

小结:

SDWebImage通过了一系列复杂的封装,把一个繁琐的过程封装成简单的接口来使用,实现了并发的图片下载及解压缩操作以及内存与磁盘存储等复杂功能。从网上看到下面这张图片,觉得特别好,非常清晰: 在这里插入图片描述