文章目录
一、SDWebIamge简介
SDWebImage是iOS中提供图片加载的第三方库,可以给UIKit框架中的控件比如UIImageView和UIButton提供从网络上下载和缓存的图片。它的接口十分简洁,如果给UIImageView控件添加图片可以使用如下代码:
objectivec
[imageView sd_setImageWithURL:imageUrl placeholderImage:nil];//第一个参数是图片的URL第二个参数是占位图片加载失败时显示
如果给UIButton添加图片可以使用如下代码:
objectivec
[button sd_setImageWithURL:imageUrl forState:UIControlStateNormal placeholderImage:nil];//第一个参数是图片的URL,第二个参数是按钮状态,第三个参数是占位图片,加载失败时显示
SDWebImage有下面一些常见的功能:
- 通过异步方式加载图片
- 可以自动缓存到内存和磁盘中,并且可以自动清理过期的缓存
- 支持多种的图片格式包括jpg、jepg、png等,同时还支持多种动图格式包括GIF、APNG等
- 同一图片的URL不会重复下载
- 对失效的图片URL不会重复尝试下载
- 在子线程中进行操作,确保不会阻塞主线程
二、SDWebImage的调用流程
当使用[imageView sd_setImageWithURL:imageUrl placeholderImage:nil];
方法时,会执行UIImageView+WebCache类中的相应方法,当使用[button sd_setImageWithURL:imageUrl forState:UIControlStateNormal placeholderImage:nil];
方法时会执行UIBUtton+WebCache类中的相应方法,但是最后都会调用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 {};
方法。接着根据URL,通过SDWebImageManager的loadImageWithURL:options:context:progress:completed:
方法加载图片,接着通过sd_setImageLoadOperation
方法将operation加入到SDOperationsDictionary中。然后调用queryCacheOperationForKey
方法进行查询图片缓存,通过查询内存和磁盘中是否有缓存,如果有则通过回调函数显示照片,如果没有则调用downloadImageWithURL:options:context:progress: completed:
方法进行图片下载和缓存,最后显示图片。
SDWebImage源码分析
1.UIImageView+WebCache层
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];
}
- (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
//UIImageView+WebCache
- (void)sd_setImageWithURL:(nullable NSURL *)url
placeholderImage:(nullable UIImage *)placeholder
options:(SDWebImageOptions)options
progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
completed:(nullable SDExternalCompletionBlock)completedBlock;
2.UIView+WebCache层
接着在上面的方法中又会调用到UIView+WebCache层的下面这个方法
objectivec
/*
imageURL: NSURL 类型,指定了要加载图片的远程URL。这是图片请求的核心依据。
options: SDWebImageOptions 枚举类型,包含了多个可选标志位,用于控制图片加载的行为,如是否只从内存缓存加载、是否同步查询缓存、是否允许重定向、是否使用渐进式加载等。
progressBlock: SDWebImageDownloaderProgressBlock 类型,一个进度回调块,当图片下载过程中更新进度时会被调用,传递已下载数据量和总数据量。
completedBlock: SDWebImageCompletionBlock 类型,一个完成回调块,当图片加载成功、失败或被取消时会被调用。它接收以下参数:
image: 加载成功的UIImage对象,或在加载失败时为nil。
data: 图片对应的原始NSData对象,可能用于进一步处理或存储。
error: 如果加载失败,包含错误信息的NSError对象;否则为nil。
cacheType: 表示图片来源于哪种缓存类型的枚举值(内存、磁盘或无缓存)。
finished: 标记此次加载是否真正完成,即使加载失败,也可能因为有占位图而标记为YES。
imageURL: 当前请求的URL,与函数参数中的imageURL相同,提供上下文信息。
*/
- (void)sd_internalSetImageWithURL:(nullable NSURL *)url
placeholderImage:(nullable UIImage *)placeholder
options:(SDWebImageOptions)options
operationKey:(nullable NSString *)operationKey
setImageBlock:(nullable SDSetImageBlock)setImageBlock
progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
completed:(nullable SDExternalCompletionBlock)completedBlock;
上面这个函数主要目的是为控件如UIImageView、UIButton设置图片,处理从指定URL加载图片的相关逻辑,包括异步下载、缓存检查、下载操作、过渡动画应用以及完成回调的触发。
函数里有个
[self sd_cancelImageLoadOperationWithKey:validOperationKey];
方法,是根据key取消当前操作,针对于比如cell中的UIImageView被复用的时候,首先需要根据key取消当前imageView上的下载或者缓存操作
3.SDWebManager层
在上面的方法中又会调用到SDWebmanager层的loadImageWithURL: options: context: progress: completed:
方法,其具体实现细节如下:
objectivec
/**
* 加载指定URL的图像,支持多种选项、上下文以及进度和完成回调。
*
* @param url 图像URL,可为`nil`或无效。如果传入的是`NSString`类型,会自动转换为`NSURL`。若非`NSURL`类型,则置为`nil`。
* @param options 加载选项,如缓存策略、重试失败图片等。
* @param context 上下文信息字典,包含如回调队列、下载器、解码器等自定义设置。
* @param progressBlock 图像加载进度回调,返回已加载的数据长度和总长度。
* @param completedBlock 图像加载完成回调,返回加载结果(成功或失败)、图像、数据、缓存类型、URL等信息。
* @return 返回一个`SDWebImageCombinedOperation`对象,可用于取消或查询加载状态。
*/
- (SDWebImageCombinedOperation *)loadImageWithURL:(nullable NSURL *)url
options:(SDWebImageOptions)options
context:(nullable SDWebImageContext *)context
progress:(nullable SDImageLoaderProgressBlock)progressBlock
completed:(nonnull SDInternalCompletionBlock)completedBlock {
// 确保已完成回调块不为空,否则调用此方法无意义
NSAssert(completedBlock != nil, @"若要预取图像,请使用-[SDWebImagePrefetcher prefetchURLs]方法");
// 处理URL类型,允许传入NSString并转换为NSURL
if ([url isKindOfClass:NSString.class]) {
url = [NSURL URLWithString:(NSString *)url];
}
// 防止传入非NSURL类型的无效URL(如NSNull),将其置为nil
if (![url isKindOfClass:NSURL.class]) {
url = nil;
}
// 创建一个新的图像加载操作对象
SDWebImageCombinedOperation *operation = [SDWebImageCombinedOperation new];
operation.manager = self;
// 检查URL是否在失败列表中(已标记为失败的URL)
BOOL isFailedUrl = NO;
if (url) {
SD_LOCK(_failedURLsLock);
isFailedUrl = [self.failedURLs containsObject:url];
SD_UNLOCK(_failedURLsLock);
}
// 根据URL、选项和上下文预处理并生成最终加载结果
SDWebImageOptionsResult *result = [self processedResultForURL:url options:options context:context];
// 若URL无效或已标记为失败且不开启重试选项,直接调用完成回调并返回操作对象
if (url.absoluteString.length == 0 || (!(options & SDWebImageRetryFailed) && isFailedUrl)) {
NSString *description = isFailedUrl ? @"Image url is blacklisted" : @"Image url is nil";
NSInteger code = isFailedUrl ? SDWebImageErrorBlackListed : SDWebImageErrorInvalidURL;
[self callCompletionBlockForOperation:operation completion:completedBlock error:[NSError errorWithDomain:SDWebImageErrorDomain code:code userInfo:@{NSLocalizedDescriptionKey : description}] queue:result.context[SDWebImageContextCallbackQueue] url:url];
return operation;
}
// 将当前操作添加到正在运行的操作列表中
SD_LOCK(_runningOperationsLock);
[self.runningOperations addObject:operation];
SD_UNLOCK(_runningOperationsLock);
// 开始从缓存加载图像,后续步骤如下:
// 1. 查询缓存中的图像(可能涉及原始图像和经过变换的图像,取决于是否有变换器)
// 2. 缓存未命中时,下载数据和图像
// 3. 存储图像到缓存(可能同时存储原始图像和经过变换的图像)
// 4. 对图像进行CPU变换(若有变换器)
[self callCacheProcessForOperation:operation url:url options:result.options context:result.context progress:progressBlock completed:completedBlock];
return operation;
}
上面方法的主要功能就是异步加载指定URL的图像资源
1.首先确保completedBlock不为空,以便在加载完成后执行回调。同时,对传入的url参数进行类型检查和转换,确保其为有效的NSURL对象。
2.接着实例化一个SDWebImageCombinedOperation对象,用于管理整个图片加载过程,并将其与当前SDWebImageManager实例关联。
3.如果URL有效,检查其是否在失败URL列表中(即之前加载该URL时失败且未被重试)。这一步有助于避免重复尝试已知失败的请求。根据URL、选项和上下文,生成一个SDWebImageOptionsResult对象,其中包含了实际应用的加载选项和处理后的上下文信息。
4.如果URL为空、无效或者已标记为失败且未开启SDWebImageRetryFailed选项,立即调用完成回调,报告错误并返回操作对象。将当前加载操作加入到正在运行的操作列表中,便于全局管理所有正在进行的加载任务。
5.最后调用callCacheProcessForOperation方法,开始从缓存查找图像,如果缓存未命中,则启动网络下载,并在下载完成后存储图像到缓存。在整个过程中,可能会根据上下文中的变换器对图像进行CPU处理。同时,如果提供了progressBlock,会在加载过程中定期回调更新加载进度。
上面代码最后调用到callCacheProcessForOperation:
方法,这个方法的具体实现如下:
objectivec
- (void)callCacheProcessForOperation:(nonnull SDWebImageCombinedOperation *)operation
url:(nonnull NSURL *)url
options:(SDWebImageOptions)options
context:(nullable SDWebImageContext *)context
progress:(nullable SDImageLoaderProgressBlock)progressBlock
completed:(nullable SDInternalCompletionBlock)completedBlock {
// 获取要使用的图像缓存实例
id<SDImageCache> imageCache = context[SDWebImageContextImageCache];
if (!imageCache) {
imageCache = self.imageCache; // 如果上下文中没有指定缓存,则使用默认缓存
}
// 获取查询缓存类型
SDImageCacheType queryCacheType = SDImageCacheTypeAll; // 默认查询所有缓存类型
if (context[SDWebImageContextQueryCacheType]) {
queryCacheType = [context[SDWebImageContextQueryCacheType] integerValue]; // 如果上下文中指定了查询缓存类型,则使用指定值
}
// 判断是否需要查询缓存(根据options判断)
BOOL shouldQueryCache = !SD_OPTIONS_CONTAINS(options, SDWebImageFromLoaderOnly); // 如果选项中不包含仅从加载器加载,则需要查询缓存
if (shouldQueryCache) {
// 计算转换后的缓存键
NSString *key = [self cacheKeyForURL:url context:context];
// 为避免循环引用,对operation进行弱引用
@weakify(operation);
// 向缓存发起查询请求
operation.cacheOperation = [imageCache queryImageForKey:key options:options context:context cacheType:queryCacheType completion:^(UIImage * _Nullable cachedImage, NSData * _Nullable cachedData, SDImageCacheType cacheType) {
// 恢复对operation的强引用
@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"}] queue:context[SDWebImageContextCallbackQueue] url:url];
[self safelyRemoveOperationFromRunning:operation];
return;
} else if (!cachedImage) {
// 缓存中未找到图像
// 获取原始缓存键
NSString *originKey = [self originalCacheKeyForURL:url context:context];
// 判断是否有可能在原始缓存中找到图像(未经过转换)
BOOL mayInOriginalCache = ![key isEqualToString:originKey];
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];
}
}
这里主要是判断任务是否该走缓存查询,或者直接下载 。如果是缓存查询,就进入SDImageCache里面进行缓存查询,且在此处理缓存结果的回调。否则就调用callDownloadProcessForOperation进入下一步判断。
继续往下说loadImageWithURL中的工作流程,下面一个重要的方法就是queryCacheOperationForKey()
,在SDImageCache里查询是否存在缓存的图片
4.SDWebCache层
queryCacheOperationForKey()
方法的实现细节如下:
objectivec
@param key 缓存键值,用于标识特定的图片资源。如果为nil,则直接返回nil并调用完成回调。
* @param options 查询选项,如是否仅解码第一帧、是否检查动画图片类匹配等。
* @param context 上下文信息,包含如回调队列、期望的动画图片类、存储缓存类型等。
* @param queryCacheType 待查询的缓存类型(内存、磁盘或两者皆查)。
* @param doneBlock 完成回调,传递查询结果(图片、数据、缓存类型)。
*
* @return SDImageCacheToken对象,表示正在进行的查询操作。可用于取消查询。
*/
-(nullable SDImageCacheToken *)queryCacheOperationForKey:(nullable NSString *)key
options:(SDImageCacheOptions)options
context:(nullable SDWebImageContext *)context
cacheType:(SDImageCacheType)queryCacheType
done:(nullable SDImageCacheQueryCompletionBlock)doneBlock {
// 如果键值为空,则立即返回nil并调用完成回调
if (!key) {
if (doneBlock) {
doneBlock(nil, nil, SDImageCacheTypeNone);
}
return nil;
}
// 非法缓存类型,直接返回nil并调用完成回调
if (queryCacheType == SDImageCacheTypeNone) {
if (doneBlock) {
doneBlock(nil, nil, SDImageCacheTypeNone);
}
return nil;
}
// 首先检查内存缓存...
UIImage *image;
if (queryCacheType != SDImageCacheTypeDisk) {
image = [self imageFromMemoryCacheForKey:key]; // 获取内存缓存中的图片
}
// 若内存缓存命中,则根据选项进一步处理图片
if (image) {
if (options & SDImageCacheDecodeFirstFrameOnly) { // 仅解码第一帧选项
// 确保静态图片(即非动画图片)
if (image.sd_imageFrameCount > 1) {
#if SD_MAC
image = [[NSImage alloc] initWithCGImage:image.CGImage scale:image.scale orientation:kCGImagePropertyOrientationUp];
#else
image = [[UIImage alloc] initWithCGImage:image.CGImage scale:image.scale orientation:image.imageOrientation];
#endif
}
} else if (options & SDImageCacheMatchAnimatedImageClass) { // 检查动画图片类匹配选项
// 根据上下文中的期望动画图片类进行检查
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;
}
// 初始化查询操作令牌并设置相关属性
SDCallbackQueue *queue = context[SDWebImageContextCallbackQueue];
SDImageCacheToken *operation = [[SDImageCacheToken alloc] initWithDoneBlock:doneBlock];
operation.key = key;
operation.callbackQueue = queue;
// 判断是否需要同步查询磁盘缓存
// 1. 内存缓存命中且要求同步查询内存数据
// 2. 内存缓存未命中且要求同步查询磁盘数据
BOOL shouldQueryDiskSync = ((image && options & SDImageCacheQueryMemoryDataSync) ||
(!image && options & SDImageCacheQueryDiskDataSync));
// 定义磁盘数据查询闭包
NSData* (^queryDiskDataBlock)(void) = ^NSData* {
@synchronized (operation) {
if (operation.isCancelled) {
return nil;
}
}
return [self diskImageDataBySearchingAllPathsForKey:key]; // 从磁盘缓存中获取图片数据
};
// 定义磁盘图片解析闭包,根据磁盘数据生成UIImage对象
UIImage* (^queryDiskImageBlock)(NSData*) = ^UIImage*(NSData* diskData) {
@synchronized (operation) {
if (operation.isCancelled) {
return nil;
}
}
UIImage *diskImage;
if (image) { // 图片已从内存缓存获取,仅需根据数据生成UIImage
diskImage = image;
} else if (diskData) { // 从磁盘缓存获取到数据,需解析为UIImage
BOOL shouldCacheToMomery = YES;
if (context[SDWebImageContextStoreCacheType]) {
SDImageCacheType cacheType = [context[SDWebImageContextStoreCacheType] integerValue];
shouldCacheToMomery = (cacheType == SDImageCacheTypeAll || cacheType == SDImageCacheTypeMemory);
}
CGSize thumbnailSize = CGSizeZero;
NSValue *thumbnailSizeValue = context[SDWebImageContextImageThumbnailPixelSize];
if (thumbnailSizeValue != nil) {
#if SD_MAC
thumbnailSize = thumbnailSizeValue.sizeValue;
#else
thumbnailSize = thumbnailSizeValue.CGSizeValue;
#endif
}
if (thumbnailSize.width > 0 && thumbnailSize.height > 0) {
// 查询生成缩略图的全尺寸缓存键时,不应将缩略图写回全尺寸内存缓存
shouldCacheToMomery = NO;
}
// 特殊情况:当用户针对同一URL在列表中查询图片时,为了避免多次解码和写入相同图像对象到磁盘缓存,这里再次检查内存缓存
if (shouldCacheToMomery && self.config.shouldCacheImagesInMemory) {
diskImage = [self.memoryCache objectForKey:key];
}
// 如果内存缓存未命中,才进行解码
if (!diskImage) {
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;
};
// 根据是否同步查询磁盘缓存执行相应操作
if (shouldQueryDiskSync) {
__block NSData* diskData;
__block UIImage* diskImage;
dispatch_sync(self.ioQueue, ^{
diskData = queryDiskDataBlock();
diskImage = queryDiskImageBlock(diskData);
});
// 同步查询时,直接在当前线程调用完成回调
if (doneBlock) {
doneBlock(diskImage, diskData, SDImageCacheTypeDisk);
}
} else {
dispatch_async(self.ioQueue, ^{
NSData* diskData = queryDiskDataBlock();
UIImage* diskImage = queryDiskImageBlock(diskData);
@synchronized (operation) {
if (operation.isCancelled) {
return;
}
}
// 异步查询时,在指定回调队列或主线程异步调用完成回调
if (doneBlock) {
[(queue ?: SDCallbackQueue.mainQueue) async:^{
// 在从IO队列切换至主线程的过程中可能被取消,因此在此处再次检查是否已取消
@synchronized (operation) {
if (operation.isCancelled) {
return;
}
}
doneBlock(diskImage, diskData, SDImageCacheTypeDisk);
}];
}
});
}
return operation;
}
上面的代码很长,但总结下来就是做了三件事:
1.先检查键值是否为空并且图片类型是否合法,如果不为空并且合法的情况再执行下面的操作,否则直接执行回调
2.在内存中查找缓存,如果仅查询内存缓存或已从内存缓存获取到图片且不需要查询内存数据直接执行回调函数,如果没有查到的话接着执行下面的操作
3.在磁盘中查找缓存,这里分两种情况,第一种是在内存中已经查到缓存但是还接着要在磁盘中继续查找,第二种是在内存中没有查到缓存,在磁盘中尝试寻找。如果在磁盘中找到缓存的话并且内存中也有缓存的话则直接解析图片,如果在内存中没有的话则将数据传给内存再解析图片。如果磁盘中也没找到缓存的话则先回调再根据operation的属性值决定是否执行下载任务,如果执行下载操作的话则调用SDWebManager层 的callDownloadProcessForOperation:
方法进行下载前的一些配置,其实现细节如下:
objectivec
// 定义一个方法,用于调用图片下载过程。参数包括当前的图片组合操作(SDWebImageCombinedOperation)、图片URL、加载选项、上下文信息、已缓存的图片、已缓存的数据、缓存类型、进度回调和完成回调。
- (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 {
// 标记缓存操作结束
@synchronized (operation) {
operation.cacheOperation = nil; // 清空当前操作的缓存操作引用
}
// 获取图片加载器,优先使用上下文提供的,否则使用默认的imageLoader属性
id<SDImageLoader> imageLoader = context[SDWebImageContextImageLoader];
if (!imageLoader) {
imageLoader = self.imageLoader;
}
// 判断是否应该从网络下载图片
BOOL shouldDownload = !SD_OPTIONS_CONTAINS(options, SDWebImageFromCacheOnly); // 不仅限于缓存加载
shouldDownload &= (!cachedImage || options & SDWebImageRefreshCached); // 缓存图片不存在或要求刷新缓存时
shouldDownload &= (![self.delegate respondsToSelector:@selector(imageManager:shouldDownloadImageForURL:)] || [self.delegate imageManager:self shouldDownloadImageForURL:url]); // 委托方法允许下载
shouldDownload &= ([imageLoader respondsToSelector:@selector(canRequestImageForURL:options:context:)] ? [imageLoader canRequestImageForURL:url options:options context:context] : [imageLoader canRequestImageForURL:url]); // 图片加载器支持该请求
if (shouldDownload) { // 需要下载图片的情况
if (cachedImage && options & SDWebImageRefreshCached) { // 缓存存在且要求刷新缓存
// 通知已找到缓存图片并尝试重新下载以更新缓存
[self callCompletionBlockForOperation:operation completion:completedBlock image:cachedImage data:cachedData error:nil cacheType:cacheType finished:YES queue:context[SDWebImageContextCallbackQueue] url:url];
// 将缓存图片传递给图片加载器,以便比较远程图片是否与缓存图片一致
SDWebImageMutableContext *mutableContext;
if (context) {
mutableContext = [context mutableCopy];
} else {
mutableContext = [NSMutableDictionary dictionary];
}
mutableContext[SDWebImageContextLoaderCachedImage] = cachedImage;
context = [mutableContext copy]; // 更新上下文
}
// 弱引用operation,防止循环引用
@weakify(operation);
// 发起图片加载请求,传入URL、选项、上下文、进度回调和完成回调
operation.loaderOperation = [imageLoader requestImageWithURL:url options:options context:context progress:progressBlock completed:^(UIImage *downloadedImage, NSData *downloadedData, NSError *error, BOOL finished) {
@strongify(operation); // 强引用恢复operation
if (!operation || operation.isCancelled) { // 操作已被用户取消
// 调用完成回调,报告操作取消
[self callCompletionBlockForOperation:operation completion:completedBlock error:[NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorCancelled userInfo:@{NSLocalizedDescriptionKey : @"Operation cancelled by user during sending the request"}] queue:context[SDWebImageContextCallbackQueue] url:url];
} else if (cachedImage && options & SDWebImageRefreshCached && [error.domain isEqualToString:SDWebImageErrorDomain] && error.code == SDWebImageErrorCacheNotModified) { // 图片刷新命中NSURLCache,无需调用完成回调
// Do nothing
} else if ([error.domain isEqualToString:SDWebImageErrorDomain] && error.code == SDWebImageErrorCancelled) { // 下载操作被用户取消
// 调用完成回调,报告操作取消
[self callCompletionBlockForOperation:operation completion:completedBlock error:error queue:context[SDWebImageContextCallbackQueue] url:url];
} else if (error) { // 下载过程中出现错误
// 调用完成回调,报告错误
[self callCompletionBlockForOperation:operation completion:completedBlock error:error queue:context[SDWebImageContextCallbackQueue] url:url];
// 根据条件决定是否将失败的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 { // 图片下载成功
// 如果允许重试失败,移除失败URL
if (options & SDWebImageRetryFailed) {
SD_LOCK(self->_failedURLsLock);
[self.failedURLs removeObject:url];
SD_UNLOCK(self->_failedURLsLock);
}
// 调用图片转换过程,处理下载成功的图片
[self callTransformProcessForOperation:operation url:url options:options context:context originalImage:downloadedImage originalData:downloadedData cacheType:SDImageCacheTypeNone finished:finished completed:completedBlock];
}
if (finished) { // 图片加载(无论成功或失败)完成
[self safelyRemoveOperationFromRunning:operation]; // 从运行中的操作列表中移除当前操作
}
}];
} else if (cachedImage) { // 仅使用缓存图片的情况
// 调用完成回调,报告使用缓存图片
[self callCompletionBlockForOperation:operation completion:completedBlock image:cachedImage data:cachedData error:nil cacheType:cacheType finished:YES queue:context[SDWebImageContextCallbackQueue] url:url];
// 从运行中的操作列表中移除当前操作
[self safelyRemoveOperationFromRunning:operation];
} else { // 未找到缓存图片且不允许下载的情况
// 调用完成回调,报告未找到图片
[self callCompletionBlockForOperation:operation completion:completedBlock image:nil data:nil error:nil cacheType:SDImageCacheTypeNone finished:YES queue:context[SDWebImageContextCallbackQueue] url:url];
// 从运行中的操作列表中移除当前操作
[self safelyRemoveOperationFromRunning:operation];
}
}
这个函数首先根据传入的options参数判断是否需要下载图片,如果存在缓存图片并且请求要求刷新缓存,先通知客户端已找到缓存图片并开始重新下载以更新缓存,然后将缓存图片信息添加到上下文中,以便图片加载器在下载过程中进行比较。接着发起图片下载请求,并将返回值存到operation.loaderOperation中以便进行后续的取消操作。
5.SDWebImageDownloader层
对于下载图片的部分,会用到downloadImageWithURL:
方法,其具体实现细节如下:
objectivec
@param url 图片资源的URL。作为回调字典的键,不能为nil。若为nil,则立即调用完成回调并返回nil。
* @param options 下载选项,如重试次数、超时时间、HTTP头处理等。
* @param context 上下文信息,包含如解码选项、缓存键过滤器、代理等。
* @param progressBlock 下载进度回调,传递已下载数据大小和总大小。
* @param completedBlock 下载完成回调,传递图片、数据、错误信息以及是否从缓存加载。
*
* @return SDWebImageDownloadToken对象,表示正在进行的下载任务。可用于取消下载。
*/
- (nullable SDWebImageDownloadToken *)downloadImageWithURL:(nullable NSURL *)url
options:(SDWebImageDownloaderOptions)options
context:(nullable SDWebImageContext *)context
progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock {
// 如果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;
}
// 初始化下载操作取消令牌(用于取消关联的下载操作)
id downloadOperationCancelToken;
// 根据上下文中的缓存键过滤器生成缓存键(用于唯一标识图片资源)
id<SDWebImageCacheKeyFilter> cacheKeyFilter = context[SDWebImageContextCacheKeyFilter];
NSString *cacheKey;
if (cacheKeyFilter) {
cacheKey = [cacheKeyFilter cacheKeyForURL:url];
} else {
cacheKey = url.absoluteString;
}
// 根据上下文和下载选项生成解码选项
SDImageCoderOptions *decodeOptions = SDGetDecodeOptionsFromContext(context, [self.class imageOptionsFromDownloaderOptions:options], cacheKey);
// 加锁保护操作字典
SD_LOCK(_operationsLock);
// 从操作字典中获取与URL关联的下载操作(如果存在)
NSOperation<SDWebImageDownloaderOperation> *operation = [self.URLOperations objectForKey:url];
// 检查是否可以复用现有下载操作(未完成且未取消)
BOOL shouldNotReuseOperation;
if (operation) {
@synchronized (operation) {
shouldNotReuseOperation = operation.isFinished || operation.isCancelled;
}
} else {
shouldNotReuseOperation = YES;
}
if (shouldNotReuseOperation) {
// 创建新的下载操作
operation = [self createDownloaderOperationWithUrl:url options:options context:context];
if (!operation) {
SD_UNLOCK(_operationsLock);
if (completedBlock) {
NSError *error = [NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorInvalidDownloadOperation userInfo:@{NSLocalizedDescriptionKey : @"Downloader operation is nil"}];
completedBlock(nil, nil, error, YES);
}
return nil;
}
// 设置操作完成时从操作字典移除该操作
@weakify(self);
operation.completionBlock = ^{
@strongify(self);
if (!self) {
return;
}
SD_LOCK(self->_operationsLock);
[self.URLOperations removeObjectForKey:url];
SD_UNLOCK(self->_operationsLock);
};
// 将新创建的下载操作添加到操作字典
[self.URLOperations setObject:operation forKey:url];
// 在提交到操作队列之前添加进度和完成回调,避免操作完成前回调未设置导致的问题
downloadOperationCancelToken = [operation addHandlersForProgress:progressBlock completed:completedBlock decodeOptions:decodeOptions];
// 将下载操作添加到下载队列
[self.downloadQueue addOperation:operation];
} else {
// 复用已存在的下载操作并附加新的回调
@synchronized (operation) {
downloadOperationCancelToken = [operation addHandlersForProgress:progressBlock completed:completedBlock decodeOptions:decodeOptions];
}
}
SD_UNLOCK(_operationsLock);
// 创建并初始化下载任务令牌,关联下载操作、URL、请求及取消令牌
SDWebImageDownloadToken *token = [[SDWebImageDownloadToken alloc] initWithDownloadOperation:operation];
token.url = url;
token.request = operation.request;
token.downloadOperationCancelToken = downloadOperationCancelToken;
return token;
}
downloadImageWithURL
方法返回的是一个SDWebImageDownloadToken类型的token,这么做的目的是可以在取消的回调中及时取消下载操作。上面代码中的关键是 operation = [self createDownloaderOperationWithUrl:url options:options context:context];
这行代码中的createDownloaderOperationWithUrl:
方法执行的是真正执行网络请求的下载操作,在执行完成后返回一个operation通过operation进行后面的操作最后返回一个token。