【iOS开发】—— SDWebImage源码学习(未完)

什么是SDWebImage?

SDWebImage是iOS开发中被广泛使用的第三方开源库,它提供了图片从加载、解析、缓存、清理等一系列功能。

平时我们引进这个第三方库,最主要的就是使用sd_setImageWithURL方法。下面讲讲这个方法的调用

sd_setImageWithURL调用关系

在这里插入图片描述

通过上图,可以知道SDWebImage加载的过程是首先从缓存中加载数据,缓存加载是优先从内存缓存中加载,然后才是磁盘加载。如果没有缓存,才从网络上加载。网络成功加载图片以后,存入本地缓存。

步骤一

根据自己需要选择UIImageView+WebCache类中以下任一方法:

这个类主要是实现以下方法:

- (void)sd_setImageWithURL:(nullable NSURL *)url;

- (void)sd_setImageWithURL:(nullable NSURL *)url
          placeholderImage:(nullable UIImage *)placeholder;
          
- (void)sd_setImageWithURL:(nullable NSURL *)url
          placeholderImage:(nullable UIImage *)placeholder
                   options:(SDWebImageOptions)options;
                   
- (void)sd_setImageWithURL:(nullable NSURL *)url
          placeholderImage:(nullable UIImage *)placeholder
                   options:(SDWebImageOptions)options
                   context:(nullable SDWebImageContext *)context;
                   
- (void)sd_setImageWithURL:(nullable NSURL *)url
                 completed:(nullable SDExternalCompletionBlock)completedBlock;
                 
- (void)sd_setImageWithURL:(nullable NSURL *)url
          placeholderImage:(nullable UIImage *)placeholder
                 completed:(nullable SDExternalCompletionBlock)completedBlock;
                 
- (void)sd_setImageWithURL:(nullable NSURL *)url
          placeholderImage:(nullable UIImage *)placeholder
                   options:(SDWebImageOptions)options
                 completed:(nullable SDExternalCompletionBlock)completedBlock;
                 
- (void)sd_setImageWithURL:(nullable NSURL *)url
          placeholderImage:(nullable UIImage *)placeholder
                   options:(SDWebImageOptions)options
                  progress:(nullable SDImageLoaderProgressBlock)progressBlock
                 completed:(nullable SDExternalCompletionBlock)completedBlock;                

其实以上所有的方法都是在调用下面这个方法:

- (void)sd_setImageWithURL:(nullable NSURL *)url
          placeholderImage:(nullable UIImage *)placeholder
                   options:(SDWebImageOptions)options
                   context:(nullable SDWebImageContext *)context
                  progress:(nullable SDImageLoaderProgressBlock)progressBlock
                 completed:(nullable SDExternalCompletionBlock)completedBlock;

而这个方法则是调用了UIView+WebCache类中的方法。

步骤三

上面UIImageView+WebCache调用的就是UIView+WebCache类中的

- (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;

在这个方法里,创建了一个SDWebImageManager对象,然后去调用其类的loadImageWithURL:()

所有的UIButton、UIImageView都回调用这个分类的方法来完成图片加载的处理。同时通过UIView+WebCacheOperation分类来管理请求的取消和记录工作。所有UIView及其子类的分类都是用这个类的来实现图片的加载,这个类也是主要实现了这个方法。

步骤四

上面的方法会调用SDWebImageManager类的:

- (SDWebImageCombinedOperation *)loadImageWithURL:(nullable NSURL *)url
                                          options:(SDWebImageOptions)options
                                          context:(nullable SDWebImageContext *)context
                                         progress:(nullable SDImageLoaderProgressBlock)progressBlock
                                        completed:(nonnull SDInternalCompletionBlock)completedBlock;

SDWebImageManager 对象的上述方法里,首先会查询在缓存中有没有这个图片,然后根据各种 option 判断决定是否要从网络端下载。而查询缓存中有没有是通过调用 SDImageCache 对象的实例方法来实现的。

步骤五

上面的方法会调用SDImageCache类的下面方法从图像缓存中查询给定密钥的缓存图像,如果映像缓存在内存中,则同步调用完成;否则异步调用完成。

- (nullable id<SDWebImageOperation>)queryImageForKey:(nullable NSString *)key
                                             options:(SDWebImageOptions)options
                                             context:(nullable SDWebImageContext *)context
                                           cacheType:(SDImageCacheType)cacheType
                                          completion:(nullable SDImageCacheQueryCompletionBlock)completionBlock;

SDImageCache 这个类是专门负责缓存相关的问题的,包括查询缓存和将图片进行缓存。SDImageCache 使用了一个 NSCache 对象来进行内存缓存,磁盘缓存则是把图片数据存放在应用沙盒的Caches这个文件夹下。

这里的查询缓存的步骤为:首先查询内存缓存,内存缓存查询完了以后再判断是否需要查询磁盘缓存。如果查询内存缓存已经有了结果并且没有设置一定要查询磁盘缓存,那么就不查询磁盘缓存,否则就要查询磁盘缓存。内存缓存没有查询到图片,并且磁盘缓存查询到了图片,那么就要把这个内容缓存到内存缓存中,返回缓存查询的结果;如果磁盘缓冲也没有查询到图片,就调用下一个类去下载图片。

步骤六

下载未缓冲过的图片时,使用类SDWebImageDownloader下面这个方法:

- (nullable SDWebImageDownloadToken *)downloadImageWithURL:(nullable NSURL *)url
                                                   options:(SDWebImageDownloaderOptions)options
                                                   context:(nullable SDWebImageContext *)context
                                                  progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                                                 completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock;

下载步骤

整个步骤就是下面这样:
在这里插入图片描述

UIImageView+ WebCache

这个分类在上面介绍调用关系时已经说过了,里面的方法都是在为程序员的不同需求提供对外的接口方法,最后都是调用

- (void)sd_setImageWithURL:(nullable NSURL *)url
          placeholderImage:(nullable UIImage *)placeholder
                   options:(SDWebImageOptions)options
                   context:(nullable SDWebImageContext *)context
                  progress:(nullable SDImageLoaderProgressBlock)progressBlock
                 completed:(nullable SDExternalCompletionBlock)completedBlock;

这个方法,没什么说的。

UIView+ WebCache

这个类分析一下- (void)sd_internalSetImageWithURL:()方法:

- (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;
    
     //下面这行代码是保证没有当前正在进行的异步下载操作, 使它不会与即将进行的操作发生冲突
    [self sd_cancelImageLoadOperationWithKey:validOperationKey];
    self.sd_imageURL = url;
    // 获取图像管理者对象
    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];
        });
    }
    
    // 判断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
        
        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 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
            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 basedOnClassOrViaCustomSetImageBlock:setImageBlock transition:transition cacheType:cacheType imageURL:imageURL];
#else
                [self sd_setImage:targetImage imageData:targetData basedOnClassOrViaCustomSetImageBlock:setImageBlock cacheType:cacheType imageURL:imageURL];
#endif
                callCompletedBlockClosure();
            });
        }];
        [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);
            }
        });
    }
}

一行一行分析一下:
首先是方法名:
其总共有五个参数,URL就是我们需要下载的在线图片链接,placeholder(占位符)Image其是UIImage类型,而SDWebImageOptions我们查看其源码并进行相关信息的查询
其是一种暴露在外的可供使用者使用的选择方法

typedef NS_OPTIONS(NSUInteger, SDWebImageOptions) {
    /**
     *默认情况下,当URL下载失败时,该URL会被列入黑名单,因此库不会继续尝试。
	 *此标志禁用此黑名单。
     */
    SDWebImageRetryFailed = 1 << 0,
    
    /**
     *默认情况下,图像下载是在UI交互期间启动的,此标志禁用此功能,
	 *例如,导致在UIScrollView上延迟下载。
     */
    SDWebImageLowPriority = 1 << 1,
    
    /**
    *此标志启用渐进式下载,图像在下载过程中会像浏览器一样渐进式显示。
	*默认情况下,图像仅在完全下载后显示。
     */
    SDWebImageProgressiveLoad = 1 << 2,
    
    /**
    *即使映像已缓存,也要遵守HTTP响应缓存控制,并在需要时从远程位置刷新映像。
	*磁盘缓存将由NSURLCache而不是SDWebImage处理,这会导致性能略有下降。
	*此选项有助于处理同一请求URL后面更改的图像,例如Facebook graph api配置文件图片。
	*如果刷新了缓存的图像,则使用缓存的图像调用一次完成块,然后使用最终图像调用一次完成块。
	*
	*只有当不能使用嵌入式缓存破坏参数使URL保持静态时,才使用此标志。
    */
    SDWebImageRefreshCached = 1 << 3,
    
    /**
     * In iOS 4+, continue the download of the image if the app goes to background. This is achieved by asking the system for
     * extra time in background to let the request finish. If the background task expires the operation will be cancelled.
     */
    SDWebImageContinueInBackground = 1 << 4,
    
    /**
    *通过设置
	*NSMutableURLRequest。HTTPShouldHandleCookies=是;
     */
    SDWebImageHandleCookies = 1 << 5,
    
    /**
    *启用以允许不受信任的SSL证书。
	*用于测试目的。在生产中小心使用。
     */
    SDWebImageAllowInvalidSSLCertificates = 1 << 6,
    
    /**
    *默认情况下,图像按其排队顺序加载。此标志将它们移动到
	*排在队伍前面。
     */
    SDWebImageHighPriority = 1 << 7,
    
    /**
     *默认情况下,在加载图像时加载占位符图像。此标志将延迟加载
	*直到图像加载完毕。
     */
    SDWebImageDelayPlaceholder = 1 << 8,
    
    /**
     *我们通常不会对动画图像应用变换,因为大多数变形金刚无法管理动画图像。
	 *无论如何都要使用此标志来变换它们。
     */
    SDWebImageTransformAnimatedImage = 1 << 9,
    
    /**
     *默认情况下,图像在下载后添加到imageView。但在某些情况下,我们希望
	*在设置图像之前先用手(例如,应用过滤器或添加交叉淡入淡出动画)
	*如果要在成功时手动设置完成中的图像,请使用此标志
     */
    SDWebImageAvoidAutoSetImage = 1 << 10,
    
    /**
     *默认情况下,图像将根据其原始大小进行解码。
	*此标志将图像缩小到与设备受限内存兼容的大小。
	*要控制内存字节的限制,请选中“SDImageCoderHelper”。defaultScaleDownLimitBytes`(在iOS上默认为60MB)
	*这实际上将转换为使用上下文选项“”。imageThumbnailPixelSize`来自v5。5.0(在iOS上默认为(39663966)。以前没有。
	*以及v5和动画效果中的标志。5.0. 以前没有。
	*@note如果你需要细节控件,最好使用上下文选项“imageThumbnailPixelSize”和“ImagePreserveApectRatio”。
     */
    SDWebImageScaleDownLargeImages = 1 << 11,
    
    /**
    *默认情况下,当图像已经缓存在内存中时,我们不会查询图像数据。这个掩码可以强制同时查询图像数据。但是,除非指定'SDWebImageQueryMemoryDataSync',否则此查询是异步的`
     */
    SDWebImageQueryMemoryData = 1 << 12,
    
    /**
     *默认情况下,当您只指定'SDWebImageQueryMemoryData'时,我们会异步查询内存图像数据。并结合该掩码同步查询内存图像数据。
	*@note不建议同步查询数据,除非您希望确保图像加载在同一个运行循环中,以避免在单元重用期间闪烁。
     */
    SDWebImageQueryMemoryDataSync = 1 << 13,
    
    /**
     *默认情况下,当内存缓存丢失时,我们异步查询磁盘缓存。此掩码可以强制同步查询磁盘缓存(当内存缓存未命中时)。
	*@note这3个查询选项可以组合在一起。有关这些掩码组合的完整列表,请参见wiki页面。
	*@note不建议同步查询数据,除非您希望确保图像加载在同一个运行循环中,以避免在单元重用期间闪烁。     
	*/
    SDWebImageQueryDiskDataSync = 1 << 14,
    
    /**
     *默认情况下,当缓存丢失时,将从加载程序加载图像。此标志可防止仅从缓存加载。
     */
    SDWebImageFromCacheOnly = 1 << 15,
    
    /**
     *默认情况下,我们在从加载程序加载图像之前查询缓存。此标志可防止仅从加载程序加载。
     */
    SDWebImageFromLoaderOnly = 1 << 16,
    
    /**
	*默认情况下,当您在映像加载完成后使用“SDWebImageTransition”执行某些视图转换时,此转换仅在manager的回调异步(从网络或磁盘缓存查询)时应用于映像
	*此掩码可以强制对任何情况应用视图转换,如内存缓存查询或同步磁盘缓存查询。
     */
    SDWebImageForceTransition = 1 << 17,
    
    /**
	*默认情况下,我们将在缓存查询和从网络下载期间在后台解码图像。这有助于提高性能,因为在屏幕上渲染图像时,首先需要对其进行解码。但这种情况是通过核心动画在主队列中发生的。
	*然而,这个过程也可能会增加内存使用。如果由于内存消耗过多而遇到问题,此标志可能会阻止对图像进行解码。
     */
    SDWebImageAvoidDecodeImage = 1 << 18,
    
    /**
   	*默认情况下,我们解码动画图像。此标志只能强制解码第一帧并生成静态图像。
     */
    SDWebImageDecodeFirstFrameOnly = 1 << 19,
    
    /**
     *默认情况下,对于“SDAnimatedImage”,我们在渲染期间解码动画图像帧以减少内存使用。但是,当动画图像被许多ImageView共享时,可以指定将所有帧预加载到内存中,以减少CPU使用。
	*这实际上会在后台队列中触发“Preload AllanimatedImageFrames”(仅限磁盘缓存和下载)。
     */
    SDWebImagePreloadAllFrames = 1 << 20,
    
    /**
     *默认情况下,当您使用“SDWebImageContextAnimatedImageClass”上下文选项时(比如使用“SDAnimatedImageView”,其设计目的是使用“SDAnimatedImageView”),当内存缓存命中时,或者图像解码器无法生成与自定义类完全匹配的图像作为备用解决方案时,我们仍然可以使用“UIImage”。
	*使用此选项,可以确保我们始终使用您提供的类回调图像。如果生成失败,将使用代码为“SDWebImageErrorBadImageData”的错误。
	*注意:此选项与“SDWebImageDecodeFirstFrameOnly”不兼容,后者总是生成UIImage/NSImage。
     */
    SDWebImageMatchAnimatedImageClass = 1 << 21,
    
    /**
    *默认情况下,当我们从网络加载映像时,映像将被写入缓存(内存和磁盘,由“storeCacheType”上下文选项控制)
	*这可能是一个异步操作,最终的'SDInternalCompletionBlock'回调不能保证磁盘缓存写入完成,可能会导致逻辑错误。(例如,您仅在完成块中修改磁盘数据,但磁盘缓存尚未就绪)
	*如果需要使用完成块中的磁盘缓存进行处理,则应使用此选项确保在回调时已写入磁盘缓存。
	*注意:如果在使用自定义缓存序列化程序或transformer时使用此选项,我们还将等待写入的输出图像数据完成。
     */
    SDWebImageWaitStoreCache = 1 << 22,
    
    /**
     *我们通常不会对矢量图像应用变换,因为矢量图像支持动态更改为任何大小,光栅化为固定大小会丢失细节。要修改矢量图像,可以在运行时处理矢量数据(例如修改PDF标记/SVG元素)。
	*无论如何都要使用此标志来变换它们。
     */
    SDWebImageTransformVectorImage = 1 << 23
};
第一块:
//从context获取对应的validOperationKey,然后根据参数validOperationKey进行后面的操作,
//如果operationKey为nil key取NSStringFromClass([self class])
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;
第二块:
[self sd_cancelImageLoadOperationWithKey:validOperationKey];

取消之前的下载任务。通过调用UIView+WebCacheOperation类中的方法,

在UIView+WebCacheOperation.h的API都是对下载operation的操作。绑定operation、取消operation、移除operation。在UIView+WebCacheOperation.m同样使用关联对象针对每个UIKit对象在内存中维护一个字典operationDictionary。可以对不同的key值添加对应的下载operation,也可以在下载操作没有完成的时候根据key取到operation进行取消。

以下是该方法的实现:

- (void)sd_cancelImageLoadOperationWithKey:(nullable NSString *)key {
    if (key) {
        // Cancel in progress downloader from queue
        // 从队列中取消正在进行的下载程序
        SDOperationsDictionary *operationDictionary = [self sd_operationDictionary];
        id<SDWebImageOperation> operation;
        
        @synchronized (self) {
            operation = [operationDictionary objectForKey:key];
        }
        if (operation) {
            if ([operation conformsToProtocol:@protocol(SDWebImageOperation)]) {
                [operation cancel];
            }
            @synchronized (self) {
                [operationDictionary removeObjectForKey:key];
            }
        }
    }
}

operationDictionary的key一般是类名,如此同一个UIImageView同时调用两次,第一次的下载操作会先被取消,然后将operationDictionary的中的operation对应到第二次的下载操作。

然后了解一下是如何将之前的下载任务从队列中取消:
先为UIView+WebCache 创建operationDictionary。
调用该类中的sd_operationDictionary方法:

- (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;
    }
}

通过键值来得到绑定的的关联对象的值赋值给可变字典类型的operations,如果operations不为空,那么返回该关联对象的值。

然后:
如果有两个键值相同的内容进入队列,那么通过前面字典的创建得到的关联对象的值也是一样的,传入key在返回的字典中查找是否已经存在,如果存在则取消所有操作,conformsToProtocol方法如果符合这个协议(协议中声明了取消方法),也调用协议中的取消方法。
实际上,所有的操作都是由一个operationDictionary字典维护的,执行新的操作之前,cancel所有的operation。

取消队列中的下载任务的好处:
1… 因为其是针对的一个UIImageView,取消前一个操作,省时、省流量。
避免SDWebImage的复用。 也就是避免对一张图片进行重复下载。加载图片完成后, 回调时会2. 先检查任务的Operation还在不在, 不在,则不回调显示, 反之回调显示并移除Operation。
3. 当程序中断导致链接失效时,当前的下载还在操作队列中,但是按道理应该是失效状态,我们可以通过先取消当前正在进行的下载来保证操作队列中的链接不存在这种情况。

第三块:

创建图像管理器,如果没有创建过的话,就使用单例创建;如果创建过,就删除此管理器。

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];
    }
第四块:

设置占位图

if (!(options & SDWebImageDelayPlaceholder)) {
        if (shouldUseWeakCache) {
            NSString *key = [manager cacheKeyForURL:url context:context];
            //调用内存缓存以触发弱缓存同步逻辑,忽略返回值并继续正常查询
			//不幸的是,这将导致两次内存缓存查询,但速度足够快
			//将来可能会重新设计或删除弱缓存功能
            [((SDImageCache *)manager.imageCache) imageFromMemoryCacheForKey:key];
        }
        dispatch_main_async_safe(^{
            [self sd_setImage:placeholder imageData:nil basedOnClassOrViaCustomSetImageBlock:setImageBlock cacheType:SDImageCacheTypeNone imageURL:url];
        });
    }

dispatch_main_async_safe是个宏定义,保证在主线程安全执行。

#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

在主线程调用了

第五块

判断传入的URL是否为空,如果不为空,则获取图像加载进度,然后置0。

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

加载小菊花等图像指示器。

	[self sd_startImageIndicator];
    id<SDWebImageIndicator> imageIndicator = self.sd_imageIndicator;

先看一下SDWebImageIndicator定义:

这是调用这个方法

- (void)sd_startImageIndicator {
    id<SDWebImageIndicator> imageIndicator = self.sd_imageIndicator;
    if (!imageIndicator) {
        return;
    }
    dispatch_main_async_safe(^{
        [imageIndicator startAnimatingIndicator];
    });
}

然后在主线程调用这个方法:

- (void)startAnimatingIndicator {
#if SD_UIKIT
    [self.indicatorView startAnimating];
#else
    [self.indicatorView startAnimation:nil];
#endif
    self.indicatorView.hidden = NO;
}
第七块

下面主要是调用这个方法:

- (nullable SDWebImageCombinedOperation *)
    loadImageWithURL:(nullable NSURL *)url
             options:(SDWebImageOptions)options
             context:(nullable SDWebImageContext *)context
            progress:(nullable SDImageLoaderProgressBlock)progressBlock
           completed:(nonnull SDInternalCompletionBlock)completedBlock;

获取一个加载任务。下面是它的具体实现:

- (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.
    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) {
        SD_LOCK(_failedURLsLock);
        isFailedUrl = [self.failedURLs containsObject:url];
        SD_UNLOCK(_failedURLsLock);
    }

    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;
    }
	//把加载图片的一个载体存入runningOperations。里面是所有正在做图片加载过程的operation的集合。
    SD_LOCK(_runningOperationsLock);
    [self.runningOperations addObject:operation];
    SD_UNLOCK(_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;
}


先是检查URL的合法性,然后

  • 有时候可能传入的url并不是url而是一个string,这里做一层包装,如果url类型不是NSURL那么把url置空,防止崩溃。
  • 接下来创建一个SDWebImageCombinedOperation对象,并且设置其manager为self
  • 接下来判断url是否是加载失败的url(这里用到了NSMutableSet和加锁操作)
    • 如果是失败的url或者url无效则直接回调失败
    • 如果url有效则通过url获取key

经过上面的url验证之后会调用callCacheProcessForOperation方法查找缓存,

- (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;
    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) {
        NSString *key = [self cacheKeyForURL:url context:context];
        @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];
    }
}

精简一下就是:

BOOL shouldQueryCache = (options & SDWebImageFromLoaderOnly) == 0;
if (shouldQueryCache) {
	// 缓存查找
}else {
	// 进行下载操作
}

缓冲查找的方法是:

- (nullable NSOperation *)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
            Class animatedImageClass = image.class;
            if (image.sd_isAnimated || ([animatedImageClass isSubclassOfClass:[UIImage class]] && [animatedImageClass conformsToProtocol:@protocol(SDAnimatedImage)])) {
#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...
    NSOperation *operation = [NSOperation new];
    // 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));
    NSData* (^queryDiskDataBlock)(void) = ^NSData* {
        if (operation.isCancelled) {
            return nil;
        }
        
        return [self diskImageDataBySearchingAllPathsForKey:key];
    };
    
    UIImage* (^queryDiskImageBlock)(NSData*) = ^UIImage*(NSData* diskData) {
        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 shouldCacheToMomery = YES;
            if (context[SDWebImageContextStoreCacheType]) {
                SDImageCacheType cacheType = [context[SDWebImageContextStoreCacheType] integerValue];
                shouldCacheToMomery = (cacheType == SDImageCacheTypeAll || cacheType == SDImageCacheTypeMemory);
            }
            // decode image data only if in-memory cache missed
            diskImage = [self diskImageForKey:key data:diskData options:options context:context];
            if (shouldCacheToMomery && diskImage && self.config.shouldCacheImagesInMemory) {
                NSUInteger cost = diskImage.sd_memoryCost;
                [self.memoryCache setObject:diskImage forKey:key cost:cost];
            }
        }
        return diskImage;
    };
    
    // Query in ioQueue to keep IO-safe
    if (shouldQueryDiskSync) {
        __block NSData* diskData;
        __block UIImage* diskImage;
        dispatch_sync(self.ioQueue, ^{
            diskData = queryDiskDataBlock();
            diskImage = queryDiskImageBlock(diskData);
        });
        if (doneBlock) {
            doneBlock(diskImage, diskData, SDImageCacheTypeDisk);
        }
    } else {
        dispatch_async(self.ioQueue, ^{
            NSData* diskData = queryDiskDataBlock();
            UIImage* diskImage = queryDiskImageBlock(diskData);
            if (doneBlock) {
                dispatch_async(dispatch_get_main_queue(), ^{
                    doneBlock(diskImage, diskData, SDImageCacheTypeDisk);
                });
            }
        });
    }
    
    return operation;
}
  1. 内存缓冲中查找:
- (nullable UIImage *)imageFromMemoryCacheForKey:(nullable NSString *)key {
    return [self.memCache objectForKey:key];
}

这里直接通过key去cache里面查找,如果这里找到了,并且没有强制指明查询磁盘的话直接回调

  BOOL shouldQueryMemoryOnly = (queryCacheType == SDImageCacheTypeMemory) || (image && !(options & SDImageCacheQueryMemoryData));
    if (shouldQueryMemoryOnly) {
        if (doneBlock) {
            doneBlock(image, nil, SDImageCacheTypeMemory);
        }
        return nil;
    }
  1. 磁盘缓存查找
    上一步如果没有找到的话,就会进如磁盘查找阶段,此时会新建一个NSOperation,执行queryDiskBlock代码块
    • 通过key查找磁盘获取diskData
    • 如果上一步内存找到image直接赋值,否则解码diskData,并且把磁盘的image写入内存
[self.memoryCache setObject:diskImage forKey:key cost:cost];

如果都没有找到,然后是下载图片:
在这个方法

- (nullable NSOperation *)queryCacheOperationForKey:(nullable NSString *)key options:(SDImageCacheOptions)options done:(nullable SDCacheQueryCompletedBlock)doneBlock

的回调block中,首先判断是否需要下载:

 @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];
        }];

如果需要下载,则调用SDWebImageDownloadercallDownloadProcessForOperation

- (nullable SDWebImageDownloadToken *)addProgressCallback:(SDWebImageDownloaderProgressBlock)progressBlock
                                           completedBlock:(SDWebImageDownloaderCompletedBlock)completedBlock
                                                   forURL:(nullable NSURL *)url
                                           createCallback:(SDWebImageDownloaderOperation *(^)(void))createCallback {
    // 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.
    if (url == nil) {
        if (completedBlock != nil) {
            completedBlock(nil, nil, nil, NO);
        }
        return nil;
    }
    
    LOCK(self.operationsLock);
    SDWebImageDownloaderOperation *operation = [self.URLOperations objectForKey:url];
    if (!operation) {
        operation = createCallback();
        __weak typeof(self) wself = self;
        operation.completionBlock = ^{
            __strong typeof(wself) sself = wself;
            if (!sself) {
                return;
            }
            LOCK(sself.operationsLock);
            [sself.URLOperations removeObjectForKey:url];
            UNLOCK(sself.operationsLock);
        };
        [self.URLOperations setObject:operation forKey:url];
        // Add operation to operation queue only after all configuration done according to Apple's doc.
        // `addOperation:` does not synchronously execute the `operation.completionBlock` so this will not cause deadlock.
        /*------------这里添加下载任务到downQueue----------------*/
        [self.downloadQueue addOperation:operation];
    }
    UNLOCK(self.operationsLock);

    id downloadOperationCancelToken = [operation addHandlersForProgress:progressBlock completed:completedBlock];
    
    SDWebImageDownloadToken *token = [SDWebImageDownloadToken new];
    token.downloadOperation = operation;
    token.url = url;
    token.downloadOperationCancelToken = downloadOperationCancelToken;

    return token;
}

在这里把operation添加到下载队列self.downloadQueue中,self.downloadQueue是NSOperationQueue的实例,它默认的最大并发数为6(这个值可以修改)。
然后就使用NSURLSession下载,所以在SDWebImageDownloader有着NSURLSession的代理方法。

#pragma mark NSURLSessionDataDelegate

- (void)URLSession:(NSURLSession *)session
          dataTask:(NSURLSessionDataTask *)dataTask
didReceiveResponse:(NSURLResponse *)response
 completionHandler:(void (^)(NSURLSessionResponseDisposition disposition))completionHandler {

    // Identify the operation that runs this task and pass it the delegate method
    NSOperation<SDWebImageDownloaderOperation> *dataOperation = [self operationWithTask:dataTask];
    if ([dataOperation respondsToSelector:@selector(URLSession:dataTask:didReceiveResponse:completionHandler:)]) {
        [dataOperation URLSession:session dataTask:dataTask didReceiveResponse:response completionHandler:completionHandler];
    } else {
        if (completionHandler) {
            completionHandler(NSURLSessionResponseAllow);
        }
    }
}

- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data {

    // Identify the operation that runs this task and pass it the delegate method
    NSOperation<SDWebImageDownloaderOperation> *dataOperation = [self operationWithTask:dataTask];
    if ([dataOperation respondsToSelector:@selector(URLSession:dataTask:didReceiveData:)]) {
        [dataOperation URLSession:session dataTask:dataTask didReceiveData:data];
    }
}

- (void)URLSession:(NSURLSession *)session
          dataTask:(NSURLSessionDataTask *)dataTask
 willCacheResponse:(NSCachedURLResponse *)proposedResponse
 completionHandler:(void (^)(NSCachedURLResponse *cachedResponse))completionHandler {

    // Identify the operation that runs this task and pass it the delegate method
    NSOperation<SDWebImageDownloaderOperation> *dataOperation = [self operationWithTask:dataTask];
    if ([dataOperation respondsToSelector:@selector(URLSession:dataTask:willCacheResponse:completionHandler:)]) {
        [dataOperation URLSession:session dataTask:dataTask willCacheResponse:proposedResponse completionHandler:completionHandler];
    } else {
        if (completionHandler) {
            completionHandler(proposedResponse);
        }
    }
}

#pragma mark NSURLSessionTaskDelegate

- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error {
    
    // Identify the operation that runs this task and pass it the delegate method
    NSOperation<SDWebImageDownloaderOperation> *dataOperation = [self operationWithTask:task];
    if ([dataOperation respondsToSelector:@selector(URLSession:task:didCompleteWithError:)]) {
        [dataOperation URLSession:session task:task didCompleteWithError:error];
    }
}

- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task willPerformHTTPRedirection:(NSHTTPURLResponse *)response newRequest:(NSURLRequest *)request completionHandler:(void (^)(NSURLRequest * _Nullable))completionHandler {
    
    // Identify the operation that runs this task and pass it the delegate method
    NSOperation<SDWebImageDownloaderOperation> *dataOperation = [self operationWithTask:task];
    if ([dataOperation respondsToSelector:@selector(URLSession:task:willPerformHTTPRedirection:newRequest:completionHandler:)]) {
        [dataOperation URLSession:session task:task willPerformHTTPRedirection:response newRequest:request completionHandler:completionHandler];
    } else {
        if (completionHandler) {
            completionHandler(request);
        }
    }
}

- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler {

    // Identify the operation that runs this task and pass it the delegate method
    NSOperation<SDWebImageDownloaderOperation> *dataOperation = [self operationWithTask:task];
    if ([dataOperation respondsToSelector:@selector(URLSession:task:didReceiveChallenge:completionHandler:)]) {
        [dataOperation URLSession:session task:task didReceiveChallenge:challenge completionHandler:completionHandler];
    } else {
        if (completionHandler) {
            completionHandler(NSURLSessionAuthChallengePerformDefaultHandling, nil);
        }
    }
}

- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didFinishCollectingMetrics:(NSURLSessionTaskMetrics *)metrics API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0)) {
    
    // Identify the operation that runs this task and pass it the delegate method
    NSOperation<SDWebImageDownloaderOperation> *dataOperation = [self operationWithTask:task];
    if ([dataOperation respondsToSelector:@selector(URLSession:task:didFinishCollectingMetrics:)]) {
        [dataOperation URLSession:session task:task didFinishCollectingMetrics:metrics];
    }
}

这些方法都调用了(NSOperation *)operationWithTask:(NSURLSessionTask *)task。
通过task获取对应的SDWebImageDownloaderOperation实例,将具体的操作放到了这个类里面,在这个类里面这里面对需要解码的图片做了解码操作和判断是否图片需要缩放等处理。

经过层层回调,结果会回到SDWebImageManager- (id <SDWebImageOperation>)loadImageWithURL:(nullable NSURL *)url options:(SDWebImageOptions)options progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock completed:(nullable SDInternalCompletionBlock)completedBlock方法中- (nullable SDWebImageDownloadToken *)downloadImageWithURL:(nullable NSURL *)url options:(SDWebImageDownloaderOptions)options progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock的completedBlock中,这里调用了[self storeImage:transformedImage imageData:cacheData forKey:key toDisk:cacheOnDisk completion:nil]; 方法做图片缓存操作,我们来看一下实现细节。

- (void)storeImage:(nullable UIImage *)image
         imageData:(nullable NSData *)imageData
            forKey:(nullable NSString *)key
          toMemory:(BOOL)toMemory
            toDisk:(BOOL)toDisk
        completion:(nullable SDWebImageNoParamsBlock)completionBlock {
    if (!image || !key) {
        if (completionBlock) {
            completionBlock();
        }
        return;
    }
    // if memory cache is enabled
    if (toMemory && self.config.shouldCacheImagesInMemory) {
        NSUInteger cost = image.sd_memoryCost;
        [self.memoryCache setObject:image forKey:key cost:cost];
    }
    
    if (!toDisk) {
        if (completionBlock) {
            completionBlock();
        }
        return;
    }
    dispatch_async(self.ioQueue, ^{
        @autoreleasepool {
            NSData *data = imageData;
            if (!data && [image conformsToProtocol:@protocol(SDAnimatedImage)]) {
                // If image is custom animated image class, prefer its original animated data
                data = [((id<SDAnimatedImage>)image) animatedImageData];
            }
            if (!data && image) {
                // Check image's associated image format, may return .undefined
                SDImageFormat format = image.sd_imageFormat;
                if (format == SDImageFormatUndefined) {
                    // If image is animated, use GIF (APNG may be better, but has bugs before macOS 10.14)
                    if (image.sd_isAnimated) {
                        format = SDImageFormatGIF;
                    } else {
                        // If we do not have any data to detect image format, check whether it contains alpha channel to use PNG or JPEG format
                        format = [SDImageCoderHelper CGImageContainsAlpha:image.CGImage] ? SDImageFormatPNG : SDImageFormatJPEG;
                    }
                }
                data = [[SDImageCodersManager sharedManager] encodedDataWithImage:image format:format options:nil];
            }
            [self _storeImageDataToDisk:data forKey:key];
            [self _archivedDataWithImage:image forKey:key];
        }
        
        if (completionBlock) {
            dispatch_async(dispatch_get_main_queue(), ^{
                completionBlock();
            });
        }
    });
}

可以看到self.memCache直接存入内存

if (toMemory && self.config.shouldCacheImagesInMemory) {
        NSUInteger cost = image.sd_memoryCost;
        [self.memoryCache setObject:image forKey:key cost:cost];
    }

如果需要存入磁盘的话,在异步队列self.ioQueue中调用方法写入磁盘,
所有的缓存都有时效和大小的限制,在SDImageCache中有着一些列的清理策略 默认图片的有效缓存期为7天:

  1. 内存清理
[self.memCache removeAllObjects];
  1. 磁盘清理 SDWebImage监听了app进入后台的通知,当进入后台时,如果当前缓存量大于等于设置的最大缓存量,那么按时间顺序递归删除一般的内容,直到缓存量满足条件。
第八块

这个方法是下载图片操作,

// 下载图片操作
// 将生成的加载操作赋值给UIView的自定义属性
[self sd_setImageLoadOperation:operation forKey:validOperationKey];

看看它是怎么实现的:

- (void)sd_setImageLoadOperation:(nullable id<SDWebImageOperation>)operation forKey:(nullable NSString *)key {
    if (key) {
    	// 如果之前已经有过该图片的下载操作,则取消之前的图片下载操作
        [self sd_cancelImageLoadOperationWithKey:key];
        if (operation) {
            SDOperationsDictionary *operationDictionary = [self sd_operationDictionary];
            @synchronized (self) {
                [operationDictionary setObject:operation forKey:key];
            }
        }
    }
}

所有的操作都是由一个operationDictionary字典维护的,执行新的操作之前,cancel所有的operation。

总结

这个方法涉及的方法很多,简单说一下流程:

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

相关类名与功能描述

  1. SDWebImageDownloader:是专门用来下载图片和优化图片加载的,跟缓存没有关系。
  2. SDWebImageDownloaderOperation:继承于 NSOperation,用来处理下载任务的
  3. SDImageCache:用来处理内存缓存和磁盘缓存(可选)的,其中磁盘缓存是异步进行的,因此不会阻塞主线程。
  4. SDWebImageDecoder:图片解码器,用于图片下载完成后进行解码。
  5. SDWebImagePrefetcher:预下载图片,方便后续使用,图片下载的优先级低,其内部由
  6. SDWebImageManager :来处理图片下载和缓存
  7. UIView+WebCacheOperation:用来记录图片加载的 operation,方便需要时取消和移除图片加载的 operation
  8. UIImageView+WebCache:集成 SDWebImageManager 的图片下载和缓存功
  9. UIImageView 的方法中,方便调用方的简单使用
  10. UIImageView+HighlightedWebCache:跟 UIImageView+WebCache 类似,也是包装了 SDWebImageManager,只不过是用于加载 highlighted 状态的图片。
  11. UIButton+WebCache:跟 UIImageView+WebCache 类似,集成 SDWebImageManager 的图片下载和缓存功能到 UIButton 的方法中,方便调用方的简单使用MKAnnotationView+WebCache:跟 UIImageView+WebCache 类似。
  12. NSData+ImageContentType:用于获取图片数据的格式(JPEG、PNG等)。
  13. UIImage+GIF:用于加载 GIF 动图。
  14. UIImage+MultiFormat:根据不同格式的二进制数据转成 UIImage 对象。

缓冲

SDWebImage 的图片缓存采用的是 Memory(内存) 和 Disk(硬盘) 双重 Cache 机制。该功能是由SDImageCache 类来完成的。该类负责处理内存缓存及一个可选的磁盘缓存。其中磁盘缓存的写操作是异步的,这样就不会对 UI 操作造成影响。

内存缓冲

内存缓冲主要是利用NSCache对象来实现的。NSCache是一个类似于集合的容器,提供了类似可变字典存储 key-value 对的实现方式,但它比可变字典更适用于实现缓存。

  1. 最重要的原因是NSCache是线程安全的,使用NSMutableDictionary自定义实现缓存的时候需要考虑加锁和释放锁,NSCache已经帮我们做好了这一步。
  2. 其次,内存不足时NSCache会自动释放存储的对象,不需要手动干预,如果是自定义实现需要监听内存状态然后做进一步删除对象的操作。
  3. 还有一点NSCache的键key不会被复制,所以key不需要实现NSCopying协议。

磁盘缓冲

使用NSFileManager对象来实现的。图片存储的位置是位于Cache文件夹。另外,SDImageCache 还定义了一个串行队列,来异步存储图片。

SDImageCache 提供了大量方法来缓存、获取、移除及清空图片。而对于每个图片,为了方便地在内存或磁盘中对它进行这些操作,我们需要一个 key 值来索引它。 在内存中,我们将其作为 NSCache 的 key 值,而在磁盘中,我们用这个 key 作为图片的文件名。 对于一个远程服务器下载的图片,其 url 是作为这个 key 的最佳选择了。

清理缓冲的策略

Disk(硬盘)缓存清理策略:SDWebImage 会在每次 APP 结束的时候执行清理任务。 清理缓存的规则分两步进行。 第一步先清除掉过期的缓存文件。 如果清除掉过期的缓存之后,空间还不够。 那么就继续按文件时间从早到晚排序,先清除最早的缓存文件, 直到剩余空间达到要求。

相关问题

  1. SDWebImage是如何区分不同格式的图像的?
    答: 根据图像数据第一个字节来判断的!
    PNG:压缩比没有JPG高,但是无损压缩,解压缩性能高,苹果推荐的图像格式!
    JPG:压缩比最高的一种图片格式,有损压缩!最多使用的场景,照相机!解压缩的性能不好!
    GIF:序列桢动图,特点:只支持256种颜色!最流行的时候在1998~1999,有专利的!
    下面是其方法的具体实现:
+ (SDImageFormat)sd_imageFormatForImageData:(nullable NSData *)data {
    if (!data) {
        return SDImageFormatUndefined;
    }
    
    // File signatures table: http://www.garykessler.net/library/file_sigs.html
    uint8_t c;
    [data getBytes:&c length:1];
    switch (c) {
        case 0xFF:
            return SDImageFormatJPEG;
        case 0x89:
            return SDImageFormatPNG;
        case 0x47:
            return SDImageFormatGIF;
        case 0x49:
        case 0x4D:
            return SDImageFormatTIFF;
        case 0x52: {
            if (data.length >= 12) {
                //RIFF....WEBP
                NSString *testString = [[NSString alloc] initWithData:[data subdataWithRange:NSMakeRange(0, 12)] encoding:NSASCIIStringEncoding];
                if ([testString hasPrefix:@"RIFF"] && [testString hasSuffix:@"WEBP"]) {
                    return SDImageFormatWebP;
                }
            }
            break;
        }
        case 0x00: {
            if (data.length >= 12) {
                //....ftypheic ....ftypheix ....ftyphevc ....ftyphevx
                NSString *testString = [[NSString alloc] initWithData:[data subdataWithRange:NSMakeRange(4, 8)] encoding:NSASCIIStringEncoding];
                if ([testString isEqualToString:@"ftypheic"]
                    || [testString isEqualToString:@"ftypheix"]
                    || [testString isEqualToString:@"ftyphevc"]
                    || [testString isEqualToString:@"ftyphevx"]) {
                    return SDImageFormatHEIC;
                }
                //....ftypmif1 ....ftypmsf1
                if ([testString isEqualToString:@"ftypmif1"] || [testString isEqualToString:@"ftypmsf1"]) {
                    return SDImageFormatHEIF;
                }
            }
            break;
        }
        case 0x25: {
            if (data.length >= 4) {
                //%PDF
                NSString *testString = [[NSString alloc] initWithData:[data subdataWithRange:NSMakeRange(1, 3)] encoding:NSASCIIStringEncoding];
                if ([testString isEqualToString:@"PDF"]) {
                    return SDImageFormatPDF;
                }
            }
        }
        case 0x3C: {
            // Check end with SVG tag
            if ([data rangeOfData:[kSVGTagEnd dataUsingEncoding:NSUTF8StringEncoding] options:NSDataSearchBackwards range: NSMakeRange(data.length - MIN(100, data.length), MIN(100, data.length))].location != NSNotFound) {
                return SDImageFormatSVG;
            }
        }
    }
    return SDImageFormatUndefined;
}

直接截取前一个字节长度的数据:

uint8_t c;
[data getBytes:&c length:1];
  1. SDWebImage 的内存警告是如何处理的?
    答:
    • 利用通知中心观察
    • UIApplicationDidReceiveMemoryWarningNotification 接收到内存警告的通知,执行 clearMemory 方法,清理内存缓存!
    • UIApplicationWillTerminateNotification 接收到应用程序将要终止通知,执行 cleanDisk 方法,清理磁盘缓存!
    • UIApplicationDidEnterBackgroundNotification 接收到应用程序进入后台通知,执行 backgroundCleanDisk 方法,后台清理磁盘!
    • 通过以上通知监听,能够保证缓存文件的大小始终在控制范围之内!clearDisk 清空磁盘缓存,将所有缓存目录中的文件,全部删除!实际工作,将缓存目录直接删除,再次创建一个同名空目录!

以后遇到再补充吧

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值