您现在的位置是:首页 >技术杂谈 >【iOS】SDWebImage源码学习--未完网站首页技术杂谈

【iOS】SDWebImage源码学习--未完

瓯海剑 2024-06-17 11:28:04
简介【iOS】SDWebImage源码学习--未完

SDWebImage的主要功能及相关知识点

SDWebImage是一个流行的第三方库,用于在iOS和macOS应用程序中异步下载和缓存图像。它提供了一种简单而强大的方式来处理网络图像加载和缓存,具有以下主要功能:

  1. 异步下载:SDWebImage使用多线程机制,允许在后台异步下载图像,以避免阻塞应用程序的用户界面。
  2. 图像缓存:它具有内存缓存和磁盘缓存机制,可以自动将下载的图像保存在内存和磁盘中。这样,在后续的加载中,它可以快速从缓存中获取图像,而不必再次下载。
  3. 占位图和渐进式加载:SDWebImage支持在图像下载期间显示占位图,以及渐进式加载图像,使用户可以逐步看到图像的加载进度。
  4. 缓存清理:SDWebImage还提供了清理缓存的选项,可以根据需要手动清理过期或不再需要的缓存。

工具类及其功能

  • NSData+ImageContentType 通过Image data判断当前图片的格式
  • SDImageCache 缓存 定义了 Disk 和 memory二级缓存(NSCache)负责管理cache 单例
  • SDWebImageCompat 保证不同平台/版本/屏幕等兼容性的宏定义和内联 图片缩放
  • SDWebImageDecoder 图片解压缩,内部只有一个接口
  • SDWebImageDownloader 异步图片下载管理,管理下载队列,管理operation 管理网络请求 处理结果和异常 单例
    存放网络请求回调的block 自己理解的数据结构大概是
    // 结构{“url”:[{“progress”:“progressBlock”},{“complete”:“completeBlock”}]}
  • SDWebImageDownloaderOperation 实现了异步下载图片的NSOperation,网络请求给予NSURLSession 代理下载
    自定义的Operation任务对象,需要手动实现start cancel等方法
  • SDWebImageManager 核心管理类 主要对缓存管理 + 下载管理进行了封装 主要接口downloadImageWithURL单利
  • SDWebImageOperation operation协议 只定义了cancel operation这一接口 上面的downloaderOperation的代理
  • SDWebImagePrefetcher 低优先级情况下预先下载图片,对SDWebImageViewManager进行简单封装 很少用
  • MKAnnotationView+WebCache – 为MKAnnotationView异步加载图片
  • UIButton+WebCache 为UIButton异步加载图片
  • UIImage+GIF 将Image data转换成指定格式图片
  • UIImage+MultiFormat 将image data转换成指定格式图片
  • UIImageView+HighlightedWebCache 为UIImageView异步加载图片
  • UIImageView+WebCache 为UIImageView异步加载图片
  • UIView+WebCacheOperation 保存当前MKAnnotationView / UIButton / UIImageView异步下载图片的operations

下载流程

基本使用流程

在这里插入图片描述

实现流程

  1. SDWebImage首先会检查所请求的图片是否存在缓存中(包括内存缓存和磁盘缓存)。如果图片在缓存中找到,将立即从缓存中加载,以提供更快的访问速度。
  2. 如果在缓存中未找到图片,则SDWebImage会启动一个异步下载任务,从提供的URL下载图片。它会在后台处理网络请求和图片下载,而不会阻塞用户界面。
  3. 在图片下载期间,SDWebImage可以显示指定的占位图像(如果提供了)在UIImageView中。
  4. 图片下载完成后,SDWebImage会将其加载到UIImageView中,并自动处理缓存,以便在将来的请求中能够快速获取图片。

源码解析

我们根据调用流程一步一步来。

调用1

调用UIImageView+WebCache中的sd_setImageWithURL系列方法:

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

这些方法最后都调用了其全能方法:

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

调用2

上面的全能方法,实际上调用了UIView+WebCache类的一个方法:

- (nullable id<SDWebImageOperation>)sd_internalSetImageWithURL:(nullable NSURL *)url
                                              placeholderImage:(nullable UIImage *)placeholder
                                                       options:(SDWebImageOptions)options
                                                       context:(nullable SDWebImageContext *)context
                                                 setImageBlock:(nullable SDSetImageBlock)setImageBlock
                                                      progress:(nullable SDImageLoaderProgressBlock)progressBlock
                                                     completed:(nullable SDInternalCompletionBlock)completedBlock {
    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];
        });
    }
    
    id <SDWebImageOperation> operation = nil;
    
    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);
        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);
            }
        });
    }
    
    return operation;
}

首先是方法名:

其总共有五个参数,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图形api个人资料图片。
	 * 如果一个缓存的图片被刷新,完成块被调用一次缓存的图片和最后的图片。
	 * 使用此标志,只有当你不能使你的url与嵌入缓存破坏参数静态。
     */
    SDWebImageRefreshCached = 1 << 3,
    
    /**
     * 在iOS 4+中,如果应用进入后台,继续下载图片。这是通过询问系统来实现的
	 * 额外的后台时间让请求完成。如果后台任务过期,操作将被取消。
     */
    SDWebImageContinueInBackground = 1 << 4,
    
    /**
     * 处理存储在NSHTTPCookieStore中的cookie
     * NSMutableURLRequest.HTTPShouldHandleCookies = YES;
     */
    SDWebImageHandleCookies = 1 << 5,
    
    /**
     * 启用允许不受信任的SSL证书。
	 * 用于测试目的。在生产中请谨慎使用。
     */
    SDWebImageAllowInvalidSSLCertificates = 1 << 6,
    
    /**
     * 默认情况下,图像按照它们排队的顺序加载。这个标志把他们推在队伍的前面。
     */
    SDWebImageHighPriority = 1 << 7,
    
    /**
     * 默认情况下,在加载图像时加载占位符图像。此标志将延迟加载占位符图像,直到图像完成加载。
	 * @注:这用于将占位符视为**错误占位符**,而不是默认的**加载占位符**。如果图像加载被取消或出现错误,占位符将始终被设置。
	 * 因此,如果你想**错误占位符**和**加载占位符**存在,使用' SDWebImageAvoidAutoSetImage '手动设置两个占位符和最终加载的图像由你的手取决于加载结果。
     */
    SDWebImageDelayPlaceholder = 1 << 8,
    
    /**
     * 我们通常不会在动画图像上应用变换,因为大多数Transform无法管理动画图像。
	 * 无论如何的药变换,使用此标志来转换它们。
     */
    SDWebImageTransformAnimatedImage = 1 << 9,
    
    /**
     * 默认情况下,图片下载后会添加到imageView中。但在某些情况下,我们想要
	 * 在设置图像之前先设置一下(例如应用滤镜或添加交叉渐变动画)
	 * 使用此标志,如果你想手动设置图像在完成时成功
     */
    SDWebImageAvoidAutoSetImage = 1 << 10,
/**
     * 默认情况下,根据图像的原始大小对其进行解码。
	 * 此标志将缩小图像到与设备受限内存兼容的大小。
	 * 要控制内存限制,请检查' SDImageCoderHelper.defaultScaleDownLimitBytes ' (iOS上默认为60MB)
	 * 这将实际转化为使用上下文选项'。imageThumbnailPixelSize '从v5.5.0(在iOS上默认为(3966,3966))。以前没有。
	 * 从v5.5.0开始,这个标志也会影响渐进式和动画图像。以前没有。
	   如果你需要细节控件,最好使用上下文选项' imageThumbnailPixelSize '和' imagePreserveAspectRatio '代替。
     */
    SDWebImageScaleDownLargeImages = 1 << 11,
    
    /**
     * 默认情况下,当图像已经缓存在内存中时,我们不会查询图像数据。此掩码可以强制同时查询图像数据。然而,这个查询是异步的,除非你指定' SDWebImageQueryMemoryDataSync '
     */
    SDWebImageQueryMemoryData = 1 << 12,
    
    /**
     * 默认情况下,当您只指定' SDWebImageQueryMemoryData '时,我们将异步查询内存图像数据。并结合此掩码同步查询内存图像数据。
	 * @note不建议同步查询数据,除非你想确保在同一个运行循环中加载图像,以避免在单元重用期间闪烁。
     */
    SDWebImageQueryMemoryDataSync = 1 << 13,
    /**
     * 默认情况下,当内存缓存丢失时,我们异步查询磁盘缓存。此掩码可以强制同步查询磁盘缓存(当内存缓存丢失时)。
	 * @注这3个查询选项可以组合在一起。有关这些掩码组合的完整列表,请参阅wiki页面。
	 * @note不建议同步查询数据,除非你想确保在同一个运行循环中加载图像,以避免在单元重用期间闪烁。
     */
    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 '上下文选项(如使用' SDAnimatedImageView '设计		使用' SDAnimatedImage '),我们可能仍然使用' UIImage '当内存缓存hit,或图像解码器是不可用的产生一个完全匹配你的自定义类作为后备解决方案。
	 * 使用此选项,可以确保我们总是回调图像与您提供的类。如果未能产生一个,一个错误代码' SDWebImageErrorBadImageData '将被使用。
	 * 注意这个选项不兼容' SDWebImageDecodeFirstFrameOnly ',它总是产生一个UIImage/NSImage。
     */
    SDWebImageMatchAnimatedImageClass = 1 << 21,
    
    /**
     * 默认情况下,当我们从网络加载图像时,图像将被写入缓存(内存和磁盘,由' storeCacheType '上下文选项控制)。
	 * 这可能是一个异步操作,最终的' SDInternalCompletionBlock '回调不能保证磁盘缓存写入完成,可能导致逻辑错误。(例如,您在完成块中修改了磁盘数据,但是磁盘缓存还没有准备好)
	 * 如果你需要在完成块中处理磁盘缓存,你应该使用这个选项来确保回调时磁盘缓存已经被写入。
	 * 注意,如果您在使用自定义缓存序列化器或使用转换器时使用此功能,我们也将等待输出图像数据写入完成。
     */
    SDWebImageWaitStoreCache = 1 << 22,
    
    /**
     * 我们通常不会在矢量图像上应用变换,因为矢量图像支持动态更改为任何大小,栅格化到固定大小会丢失细节。要修改矢量图像,可以在运行时处理矢量数据(例如修改PDF标记/ SVG元素)。
	 * 无论如何都要在矢量图片上应用变换,使用此标志来转换它们。
     */
    SDWebImageTransformVectorImage = 1 << 23
};

1. context

if (context) {
        // copy to avoid mutable object
        // 避免可变对象
        context = [context copy];
    } else {
        context = [NSDictionary dictionary];
    }
    // 利用context获取validOperationKey
    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;

对于

[self sd_cancelImageLoadOperationWithKey:validOperationKey];

其源码:

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

这段代码在UIView+WebCacheOperation中,这个类主要负责对下载operation的操作。它使用关联对象针对每个UIKit对象在内存中维护一个字典operationDictionary。可以对不同的key值添加对应的下载operation,也可以在下载操作没有完成的时候根据key取到operation进行取消。

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

然后我们来看看operationDictionary的源码:

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

通过键值返回一个关联对象。如果operations为空,那么就创建一个对象,与键值绑定。

如果有两个相同的键值进入,得到的关联对象也是一样的。传入key在返回的字典中查找是否已经存在,如果存在则取消所有操作,conformsToProtocol方法如果符合这个协议(协议中声明了取消方法),也调用协议中的取消方法。

好处:

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

2. 创建图片管理器

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

如果没有创建过,就使用单例创建;如果创建过,就删除context中的管理器,避免保留循环。

3. 设置占位图

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

这段代码主要用于根据特定条件设置占位图像,并在需要时触发弱缓存的同步逻辑。请注意,这段代码可能随着库的更新和重构而发生变化或移除。

4. 判断url

if (url) {
        // 重置进度
        NSProgress *imageProgress = objc_getAssociatedObject(self, @selector(sd_imageProgress));
        if (imageProgress) {
            imageProgress.totalUnitCount = 0;
            imageProgress.completedUnitCount = 0;
        }

判断传入的url是否为空,如果不为空,则获取图片加载进度并重置为0。

5. 加载图像指示器

// 代码片段中的条件编译指令#if SD_UIKIT || SD_MAC用于在UIKit或AppKit环境下执行相应的逻辑。
// 在这段代码中,首先调用sd_startImageIndicator方法来启动图片加载指示器。然后获取self.sd_imageIndicator的值,该值表示图片指示器对象。
#if SD_UIKIT || SD_MAC
        // check and start image indicator
        // 检查并启动图像指示器
        [self sd_startImageIndicator];
        id<SDWebImageIndicator> imageIndicator = self.sd_imageIndicator;
#endif
        // 定义了一个名为combinedProgressBlock的块变量,它会在图片加载进度更新时被调用。该块变量接收三个参数:receivedSize表示已接收的数据大小,expectedSize表示预期的总数据大小,targetURL表示目标URL。
        SDImageLoaderProgressBlock combinedProgressBlock = ^(NSInteger receivedSize, NSInteger expectedSize, NSURL * _Nullable targetURL) {
        // 在块内部,首先检查imageProgress对象是否存在,并根据已接收和预期大小更新其totalUnitCount和completedUnitCount属性,以便跟踪加载进度。
            if (imageProgress) {
                imageProgress.totalUnitCount = expectedSize;
                imageProgress.completedUnitCount = receivedSize;
            }
#if SD_UIKIT || SD_MAC
			// 通过条件编译指令检查imageIndicator对象是否响应updateIndicatorProgress:方法。如果是,则计算进度值,并将其限制在0到1之间。然后,使用dispatch_async将更新进度的代码块调度到主队列中,以在主线程上执行更新操作。
            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
			// 如果存在progressBlock,则调用该块来传递接收大小、预期大小和目标URL。
            if (progressBlock) {
                progressBlock(receivedSize, expectedSize, targetURL);
            }
        };

这段代码片段在SDWebImage库中用于处理图片加载过程中的进度更新。

6. 创建SDWebImageOperation

		@weakify(self);
        operation = [manager loadImageWithURL:url options:options context:context progress:combinedProgressBlock completed:^(UIImage *image, NSData *data, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
            @strongify(self);
            if (!self) { return; }
            // 在加载完成且没有错误的情况下,检查进度是否未被更新。如果是这样,并且进度的totalUnitCount和completedUnitCount都为0,则将其标记为未知进度(SDWebImageProgressUnitCountUnknown)。
            if (imageProgress && finished && !error && imageProgress.totalUnitCount == 0 && imageProgress.completedUnitCount == 0) {
                imageProgress.totalUnitCount = SDWebImageProgressUnitCountUnknown;
                imageProgress.completedUnitCount = SDWebImageProgressUnitCountUnknown;
            }

调用- (SDWebImageCombinedOperation *)loadImageWithURL:(nullable NSURL *)url options:(SDWebImageOptions)options context:(nullable SDWebImageContext *)context progress:(nullable SDImageLoaderProgressBlock)progressBlock completed:(nonnull SDInternalCompletionBlock)completedBlock方法,创建下载任务。

6.1 看看上面创建下载任务得源码

- (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
    // 检查url的合法性
    NSAssert(completedBlock != nil, @"If you mean to prefetch the image, use -[SDWebImagePrefetcher prefetchURLs] instead");

    // Very common mistake is to send the URL using NSString object instead of NSURL. For some strange reason, Xcode won't
    // throw any warning for this type mismatch. Here we failsafe this error by allowing URLs to be passed as NSString.
    // 检查url是否是NSString类,如果是转换为url
    if ([url isKindOfClass:NSString.class]) {
        url = [NSURL URLWithString:(NSString *)url];
    }

    // Prevents app crashing on argument type error like sending NSNull instead of NSURL
    // 检查url是否是NSURL,如果不是,将url置为空
    if (![url isKindOfClass:NSURL.class]) {
        url = nil;
    }

	// 新建SDWebImageCombinedOperation对象,将对象的manager设置为self
    SDWebImageCombinedOperation *operation = [SDWebImageCombinedOperation new];
    operation.manager = self;

	// failedURLs是NSMutableSet<NSURL *>,里面保存了失败过的URL。如果url的地址为空,或者该URL请求失败过且没有设置重试SDWebImageRetryFailed选项,则直接直接调用完成。
    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;
    }

	// 保存SDWebImageCombinedOperation对象
    SD_LOCK(_runningOperationsLock);
    [self.runningOperations addObject:operation];
    SD_UNLOCK(_runningOperationsLock);
    
    // 预处理选项和上下文参数,以决定管理器的最终结果
    SDWebImageOptionsResult *result = [self processedResultForURL:url options:options context:context];
    
    // 启动条目以从缓存加载图像
    [self callCacheProcessForOperation:operation url:url options:result.options context:result.context progress:progressBlock completed:completedBlock];

    return operation;
}

6.2 上方法经过url验证后会开始缓存查找

调用方法:- (void)callCacheProcessForOperation:(nonnull SDWebImageCombinedOperation *)operation url:(nonnull NSURL *)url options:(SDWebImageOptions)options context:(nullable SDWebImageContext *)context progress:(nullable SDImageLoaderProgressBlock)progressBlock completed:(nullable SDInternalCompletionBlock)completedBlock

- (void)callCacheProcessForOperation:(nonnull SDWebImageCombinedOperation *)operation
                                 url:(nonnull NSURL *)url
                             options:(SDWebImageOptions)options
                             context:(nullable SDWebImageContext *)context
                            progress:(nullable SDImageLoaderProgressBlock)progressBlock
                           completed:(nullable SDInternalCompletionBlock)completedBlock {
    // 获取要使用的图像缓存
    id<SDImageCache> imageCache;
    if ([context[SDWebImageContextImageCache] conformsToProtocol:@protocol(SDImageCache)]) {
        imageCache = context[SDWebImageContextImageCache];
    } else {
        imageCache = self.imageCache;
    }
    // 获取查询缓存类型
    SDImageCacheType queryCacheType = SDImageCacheTypeAll;
    if (context[SDWebImageContextQueryCacheType]) {
        queryCacheType = [context[SDWebImageContextQueryCacheType] integerValue];
    }
    
    // 检查是否需要查询缓存
    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) {
                // 图像合并操作被用户取消
                [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 (!cachedImage) {
                BOOL mayInOriginalCache = context[SDWebImageContextImageTransformer] || context[SDWebImageContextImageThumbnailPixelSize];
                // 有机会查询原始缓存,而不是下载,然后应用转换
				// 缩略图解码是在SDImageCache的解码部分完成的,它不需要转换的后期处理
                if (mayInOriginalCache) {
                    [self callOriginalCacheProcessForOperation:operation url:url options:options context:context progress:progressBlock completed:completedBlock];
                    return;
                }
            }
            
            // 继续下载过程
            [self callDownloadProcessForOperation:operation url:url options:options context:context cachedImage:cachedImage cachedData:cachedData cacheType:cacheType progress:progressBlock completed:completedBlock];
        }];
    } else {
        // 继续下载过程
        [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 {
	// 进行下载操作
}

未完—

风语者!平时喜欢研究各种技术,目前在从事后端开发工作,热爱生活、热爱工作。