iOS--SDWebImage源码解析
SDWebImage是很有名的图片下载和缓存的第三方开源框架,最近接受的需求App都使用了它,于是决定静下心来好好学习一下这个开源框架。
前言:
在iOS的图片加载框架中,SDWebImage使用频率非常高。它支持从网络中下载且缓存图片,并设置图片到对应的UIImageView
控件或者UIButton
控件。在项目中使用SDWebImage
来管理图片加载相关操作可以极大地提高开发效率,让我们更加专注于业务逻辑实现。由于最新版本适配了VisionOS,项目更为庞大,所以本文讲解的版本旧的4.4.2版本。
SDWebImage
是个支持异步下载与缓存的UIImageView
扩展。项目主要提供了以下功能:
1.提供了一个
UIImageView
的category用来加载网络图片并且对网络图片的缓存进行管理
2.采用异步方式来下载网络图片
3.采用异步方式,使用内存+磁盘来缓存网络图片,拥有自动的缓存过期处理机制。
4.支持GIF动画
5.支持WebP格式
6.同一个URL的网络图片不会被重复下载
7.失效,虚假的URL不会被无限重试
8.耗时操作都在子线程,确保不会阻塞主线程
9.使用GCD和ARC
10.支持Arm64
11.支持后台图片解压缩处理
12.项目支持的图片格式包括 PNG,JPEG,GIF,Webp等
学习第一步--使用:
- Swift
img.sd_setImage(with: URL(string: imgUrl)){[weak self] (image, error, _, _) in
guard let self = self, let image = image else { return }
// 可选:在这里处理图片加载完成后的逻辑
}
简单的一行代码,其实SD在背后做了很多事:
其流程步骤对应的具体框架如下图:
- sd_setImageWithURL:UIimageView/UIButton根据URL设置网络图片
- sd_internalSetImageWithURL:统一为UIView根据URL设置网络图片
- loadImageWithURL:加载图片
- queryDiskCacheForKey:根据URL转成的key从缓存或者硬盘存储中搜寻图片
- disk result:如果有结果,则返回搜寻结果
- downloadImage:如果搜寻没有结果,则开始从网络下载图片
- network result:返回网络下载结果
- storeImage:存储下载图片
- image:网络下载的图片
- set Image:设置图片
SDWebImage加载图片的更详细流程如下:
- 对象调用暴露的接口方法
sd_setImageWithURL()
时,会再调用setImageWithURL:placeholderImage:options:
方法,先把占位图placeholderImage显示,然后SDWebImageManager根据URL开始处理图片。 - SDImageCache类先从内存缓存查找是否有图片缓存,如果内存中已经有图片缓存,则直接回调到前端进行图片的显示。
- 如果内存缓存中没有,则生成NSInvocationOperation添加到队列开始从硬盘中查找图片是否已经缓存。根据url为key在硬盘缓存目录下尝试读取图片文件,这一步是在NSOperation下进行的操作,所以需要回到主线程进行查找结果的回调。如果从硬盘读取到了图片,则将图片添加到内存缓存中,然后再回调到前端进行图片的显示。如果从硬盘缓存目录读取不到图片,说明所有缓存都不存在该图片,则需要下载图片。
- 共享或重新生成一个下载器SDWebImageDownloader开始下载图片。图片的下载由NSURLConnection来处理,实现相关delegate来判断的下载状态:下载中、下载完成和下载失败。
- 图片数据下载完成之后,交给SDWebImageDecoder类做图片解码处理,图片的解码处理在NSOperationQueue完成,不会阻塞主线程。在图片解码完成后,会回调给SDWebImageDownloader,然后回调给SDWebImageManager告知图片下载完成,通知所有的downloadDelegates下载完成,回调给需要的地方显示图片。
- 最后将图片通过SDImageCache类,同时保存到内存缓存和硬盘缓存中。写文件到硬盘的过程也在以单独NSInvocationOperation完成,避免阻塞主线程。
大致看下架构,然后我们先从接口入手:
img.sd_setImage(with: URL(string: imgUrl)){[weak self] (image, error, _, _) in
guard let self = self, let image = image else { return }
// 可选:在这里处理图片加载完成后的逻辑
}
跳转到实现方法:
- (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
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);
}
}];
}
- url:需要下载并展示的网络图片的URL
- Placehoder:占位图片(一个
UIImage?
类型的参数,代表在远程图像下载完成之前,你想要显示的占位符图像。如果远程图像下载失败或正在加载中,这个占位符图像就会被显示出来,为用户提供视觉反馈,告诉他们有一个图像即将加载。) - options:一组配置选项,用来定制图像加载的行为(例如是否使用缓存)。
context
: 包含额外信息的字典,这些信息可能会影响图像加载过程。- progressBlock:下载进度的回调,可用于更新用户界面以反映下载进度。
- completedBlock:完成下载后的block
当调用了上面几个接口时,又会调用UIView+WebCache分类的sd_internalSetImageWithURL
方法来做图片加载请求。具体是通过SDWebImageManager调用来实现的。同时实现了Operation取消、ActivityIndicator的添加和取消。下面先来看下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) {
// SDWebImageContext 是一个字典,用于传递配置参数。此处深拷贝是为了防止外部修改影响内部逻辑。
context = [context copy];
} else {
context = [NSDictionary dictionary];
}
//生成一个操作键,其作用:唯一标识某个视图的图片加载任务。例如,同一个 UIImageView 可能有多个独立任务(如头像和背景图),需用不同 Key 区分。
NSString *validOperationKey = context[SDWebImageContextSetImageOperationKey];
if (!validOperationKey) {
//将操作键传递到下游,可用于跟踪操作或图像视图类
validOperationKey = NSStringFromClass([self class]);
SDWebImageMutableContext *mutableContext = [context mutableCopy];
//若未在上下文中指定 SDWebImageContextSetImageOperationKey,则使用视图的类名(如 "UIImageView")。
mutableContext[SDWebImageContextSetImageOperationKey] = validOperationKey;
context = [mutableContext copy];
}
//取消当前 View 上关联的未完成操作(避免重复下载,防止多次调用导致的图片错乱)
self.sd_latestOperationKey = validOperationKey;
[self sd_cancelImageLoadOperationWithKey:validOperationKey];
self.sd_imageURL = url;
//设置占位图
if (!(options & SDWebImageDelayPlaceholder)) {
dispatch_main_async_safe(^{
[self sd_setImage:placeholder imageData:nil basedOnClassOrViaCustomSetImageBlock:setImageBlock cacheType:SDImageCacheTypeNone imageURL:url];
});
}
if (url) {
// 重置进度
NSProgress *imageProgress = objc_getAssociatedObject(self, @selector(sd_imageProgress));
if (imageProgress) {
imageProgress.totalUnitCount = 0;
imageProgress.completedUnitCount = 0;
}
#if SD_UIKIT || SD_MAC
//检查并启动图像指示器
[self sd_startImageIndicator];
id<SDWebImageIndicator> imageIndicator = self.sd_imageIndicator;
#endif
SDWebImageManager *manager = context[SDWebImageContextCustomManager];
if (!manager) {
manager = [SDWebImageManager sharedManager];
} else {
// 删除这个 manager 以此来避免循环引用 (manger -> loader -> operation -> context -> manager)
SDWebImageMutableContext *mutableContext = [context mutableCopy];
mutableContext[SDWebImageContextCustomManager] = nil;
context = [mutableContext copy];
}
//图片加载进度管理
SDImageLoaderProgressBlock combinedProgressBlock = ^(NSInteger receivedSize, NSInteger expectedSize, NSURL * _Nullable targetURL) {
if (imageProgress) {
imageProgress.totalUnitCount = expectedSize;
imageProgress.completedUnitCount = receivedSize;
}
#if SD_UIKIT || SD_MAC
if ([imageIndicator respondsToSelector:@selector(updateIndicatorProgress:)]) {
double progress = 0;
if (expectedSize != 0) {
progress = (double)receivedSize / expectedSize;
}
progress = MAX(MIN(progress, 1), 0); // 0.0 - 1.0
dispatch_async(dispatch_get_main_queue(), ^{
[imageIndicator updateIndicatorProgress:progress];
});
}
#endif
if (progressBlock) {
progressBlock(receivedSize, expectedSize, targetURL);
}
};
//启动图片加载任务
@weakify(self);
id <SDWebImageOperation> operation = [manager loadImageWithURL:url options:options context:context progress:combinedProgressBlock completed:^(UIImage *image, NSData *data, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
@strongify(self);
if (!self) { return; }
// 如果进度尚未更新,请将其标记为完成状态
if (imageProgress && finished && !error && imageProgress.totalUnitCount == 0 && imageProgress.completedUnitCount == 0) {
imageProgress.totalUnitCount = SDWebImageProgressUnitCountUnknown;
imageProgress.completedUnitCount = SDWebImageProgressUnitCountUnknown;
}
#if SD_UIKIT || SD_MAC
// 检查并停止图像指示器
if (finished) {
[self sd_stopImageIndicator];
}
#endif
BOOL shouldCallCompletedBlock = finished || (options & SDWebImageAvoidAutoSetImage);
BOOL shouldNotSetImage = ((image && (options & SDWebImageAvoidAutoSetImage)) ||
(!image && !(options & SDWebImageDelayPlaceholder)));
SDWebImageNoParamsBlock callCompletedBlockClojure = ^{
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(callCompletedBlockClojure);
return;
}
UIImage *targetImage = nil;
NSData *targetData = nil;
if (image) {
// case 2a: we got an image and the SDWebImageAvoidAutoSetImage is not set
targetImage = image;
targetData = data;
} else if (options & SDWebImageDelayPlaceholder) {
// case 2b: we got no image and the SDWebImageDelayPlaceholder flag is set
targetImage = placeholder;
targetData = nil;
}
#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
callCompletedBlockClojure();
});
}];
[self sd_setImageLoadOperation:operation forKey:validOperationKey];
} else {//处理 URL 为空的情况
#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);
}
});
}
}
我们可以看到,sd_internalSetImageWithURL
方法前面设置了一个操作键,这个操作键的作用就是标识这个任务(因为可能
有多个独立任务(如头像和背景图)),紧接着
每次加载新图片前,通过这个标识
,先检查有没有旧任务(比如上次未完成的下载),如果有就立刻取消。(举个例子:比如在列表里快速滑动,同一个控件可能被重复设置不同URL的图片,如果不处理,可能出现“先加载的图片后回来覆盖新图片”的混乱)
我们接下来再集中看下如何取消旧任务的函数:[self sd_cancelImageLoadOperationWithKey:validOperationKey];
sd_cancelImageLoadOperationWithKey的详细代码如下:
/// 取消当前key对应的所有实现了SDWebImageOperation协议的Operation对象
- (void)sd_cancelImageLoadOperationWithKey:(nullable NSString *)key {
if (key) {
// 获取当前view对应的所有key
SDOperationsDictionary *operationDictionary = [self sd_operationDictionary];
id<SDWebImageOperation> operation;
// 获取对应的图片加载operation(一个对象,通常代表一个异步的图片加载操作)
@synchronized (self) {
operation = [operationDictionary objectForKey:key];
}
// 取消所有当前view对应的所有operation
if (operation) {
if ([operation conformsToProtocol:@protocol(SDWebImageOperation)]) {
[operation cancel];
}
@synchronized (self) {
[operationDictionary removeObjectForKey:key];
}
}
}
}
实际上,所有的操作都是由一个operationDictionary
字典维护的,执行新的操作之前,会cancel
所有的operation
。这里取消旧任务的过程大致是:获取当前view的所有key,然后找到对应的加载operation,接着取消所有的operation(可以看成出厂设置初始化)。其中利用了同步锁(@synchronized)这一机制保护共享资源(操作字典)的读写,避免多线程同时修改导致崩溃。
接着我们在看下设置占位图的函数:
[self sd_setImage:placeholder imageData:nil basedOnClassOrViaCustomSetImageBlock:setImageBlock cacheType:SDImageCacheTypeNone imageURL:url];
接下来我们需要重点关注图片加载的过程:loadImageWithURL函数:
[manager loadImageWithURL:url options:options context:context progress:combinedProgressBlock completed:^(UIImage *image, NSData *data, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL)]
跳转后到SDWebImageManager.h:
- (SDWebImageCombinedOperation *)loadImageWithURL:(nullable NSURL *)url
options:(SDWebImageOptions)options
context:(nullable SDWebImageContext *)context
progress:(nullable SDImageLoaderProgressBlock)progressBlock
completed:(nonnull SDInternalCompletionBlock)completedBlock {
// 利用NSAssert预处理宏,进行判断参数completedBlock,如果为nil,则抛出异常,反之继续执行
NSAssert(completedBlock != nil, @"If you mean to prefetch the image, use -[SDWebImagePrefetcher prefetchURLs] instead");
// 如果传入的url是NSString类型的,则转换为NSURL类型再进行处理.
if ([url isKindOfClass:NSString.class]) {
url = [NSURL URLWithString:(NSString *)url];
}
// 如果url不是NSURL类型的对象,则将其置为nil
if (![url isKindOfClass:NSURL.class]) {
url = nil;
}
// 图片加载获取过程中绑定一个 SDWebImageCombinedOperation 对象,方便接下来再通过找个对象对url的加载控制
SDWebImageCombinedOperation *operation = [SDWebImageCombinedOperation new];
operation.manager = self;
BOOL isFailedUrl = NO;
// 判断url是否在加载失败的url集合里面
if (url) {
SD_LOCK(self.failedURLsLock);
isFailedUrl = [self.failedURLs containsObject:url];
SD_UNLOCK(self.failedURLsLock);
}
// 如果URL为空或者不在重试失败选项中且是已知失败的URL,则直接返回错误
if (url.absoluteString.length == 0 || (!(options & SDWebImageRetryFailed) && isFailedUrl)) {
NSString *description = isFailedUrl ? @"Image url is blacklisted" : @"Image url is nil";
NSInteger code = isFailedUrl ? SDWebImageErrorBlackListed : SDWebImageErrorInvalidURL;
[self callCompletionBlockForOperation:operation completion:completedBlock error:[NSError errorWithDomain:SDWebImageErrorDomain code:code userInfo:@{NSLocalizedDescriptionKey : description}] url:url];
return operation;
}
// 将当前操作添加到运行中的操作列表中
SD_LOCK(self.runningOperationsLock);
[self.runningOperations addObject:operation];
SD_UNLOCK(self.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;
}
此代码展示了如何通过给定的URL、选项、进度块和完成块来加载图片,并处理可能遇到的各种情况,如无效URL、黑名单URL等。此外,还涉及到对图片加载操作的管理和控制,包括利用锁机制确保线程安全,以及通过创建SDWebImageCombinedOperation
实例来管理具体的加载任务。
接着,我们需要从缓存中加载图片的过程callCacheProcessForOperation这个函数入手查看其中缓存的逻辑。
跳转到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 {
// 获取图像缓存以使用。优先使用上下文中指定的缓存,否则使用默认的imageCache。
id<SDImageCache> imageCache;
if ([context[SDWebImageContextImageCache] conformsToProtocol:@protocol(SDImageCache)]) {
imageCache = context[SDWebImageContextImageCache];
} else {
imageCache = self.imageCache;
}
// 获取查询缓存类型,默认为SDImageCacheTypeAll,表示同时查询内存和磁盘缓存。
SDImageCacheType queryCacheType = SDImageCacheTypeAll;
if (context[SDWebImageContextQueryCacheType]) {
queryCacheType = [context[SDWebImageContextQueryCacheType] integerValue];
}
// 检查是否应该查询缓存。如果选项中包含SDWebImageFromLoaderOnly,则不查询缓存。
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 (context[SDWebImageContextImageTransformer] && !cachedImage) {
[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];
}
}
首先确定使用的图像缓存(可能是上下文提供的特定缓存或默认的缓存),然后根据上下文中的配置决定查询哪种类型的缓存(如仅内存、仅磁盘或两者)。接着,它会检查是否需要进行缓存查询。如果不需要,就直接启动下载流程;如果需要,就会使用生成的缓存键值查询缓存,并在回调中根据查询结果决定是继续下载新图片还是使用缓存中的图片。此外,还处理了操作被取消的情况,确保资源被正确释放。
接着我们查看下载过程callDownloadProcessForOperation函数:
- (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 {
// 获取要使用的图像加载器,优先使用上下文中指定的加载器,否则使用默认的imageLoader。
id<SDImageLoader> imageLoader;
if ([context[SDWebImageContextImageLoader] conformsToProtocol:@protocol(SDImageLoader)]) {
imageLoader = context[SDWebImageContextImageLoader];
} else {
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 canRequestImageForURL:url];
if (shouldDownload) {
if (cachedImage && options & SDWebImageRefreshCached) {
// 如果缓存中有图片且指定了刷新缓存,则首先通知缓存中的图片,并尝试重新下载以更新缓存。
[self callCompletionBlockForOperation:operation completion:completedBlock image:cachedImage data:cachedData error:nil cacheType:cacheType finished:YES url:url];
// 将缓存的图片传递给图像加载器,以便检查远程图片是否与缓存图片相同。
SDWebImageMutableContext *mutableContext;
if (context) {
mutableContext = [context mutableCopy];
} else {
mutableContext = [NSMutableDictionary dictionary];
}
mutableContext[SDWebImageContextLoaderCachedImage] = cachedImage;
context = [mutableContext copy];
}
@weakify(operation); // 避免循环引用
operation.loaderOperation = [imageLoader requestImageWithURL:url options:options context:context progress:progressBlock completed:^(UIImage *downloadedImage, NSData *downloadedData, NSError *error, BOOL finished) {
@strongify(operation); // 恢复强引用
if (!operation || operation.isCancelled) {
// 如果操作被取消,则调用完成块并移除操作
[self callCompletionBlockForOperation:operation completion:completedBlock error:[NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorCancelled userInfo:@{NSLocalizedDescriptionKey : @"Operation cancelled by user during sending the request"}] url:url];
} else if (cachedImage && options & SDWebImageRefreshCached && [error.domain isEqualToString:SDWebImageErrorDomain] && error.code == SDWebImageErrorCacheNotModified) {
// 如果刷新缓存时没有修改,则不调用完成块
} else if ([error.domain isEqualToString:SDWebImageErrorDomain] && error.code == SDWebImageErrorCancelled) {
// 下载操作在发送请求前被用户取消,不阻塞失败的URL
[self callCompletionBlockForOperation:operation completion:completedBlock error:error url:url];
} else if (error) {
// 出现错误时调用完成块,并根据情况阻止失败的URL
[self callCompletionBlockForOperation:operation completion:completedBlock error:error url:url];
BOOL shouldBlockFailedURL = [self shouldBlockFailedURLWithURL:url error:error options:options context:context];
if (shouldBlockFailedURL) {
SD_LOCK(self.failedURLsLock);
[self.failedURLs addObject:url];
SD_UNLOCK(self.failedURLsLock);
}
} else {
// 成功下载后,如果设置了重试失败选项,则从失败列表中移除该URL
if ((options & SDWebImageRetryFailed)) {
SD_LOCK(self.failedURLsLock);
[self.failedURLs removeObject:url];
SD_UNLOCK(self.failedURLsLock);
}
// 继续存储缓存的过程
[self callStoreCacheProcessForOperation:operation url:url options:options context:context downloadedImage:downloadedImage downloadedData:downloadedData finished:finished progress:progressBlock completed:completedBlock];
}
if (finished) {
[self safelyRemoveOperationFromRunning:operation];
}
}];
} else if (cachedImage) {
// 如果不需要下载并且缓存中有图片,则直接使用缓存中的图片
[self callCompletionBlockForOperation:operation completion:completedBlock image:cachedImage data:cachedData error:nil cacheType:cacheType finished:YES url:url];
[self safelyRemoveOperationFromRunning:operation];
} else {
// 如果图片不在缓存中且不允许下载,则调用完成块
[self callCompletionBlockForOperation:operation completion:completedBlock image:nil data:nil error:nil cacheType:SDImageCacheTypeNone finished:YES url:url];
[self safelyRemoveOperationFromRunning:operation];
}
}
我们可以看到代码使用了context,context其实就是一个字典,然后字典中存储了用户特定的配置信息,每次我们说是使用上下文来处理,其实就是看用户是否有特殊处理。
并且,如果操作被取消,则调用完成块并移除操作。这也是一个很好的机制。
我们可以看到callCompletionBlockForOperation函数就是使用图片的函数,跳转后:
- (void)callCompletionBlockForOperation:(nullable SDWebImageCombinedOperation*)operation
completion:(nullable SDInternalCompletionBlock)completionBlock
image:(nullable UIImage *)image
data:(nullable NSData *)data
error:(nullable NSError *)error
cacheType:(SDImageCacheType)cacheType
finished:(BOOL)finished
url:(nullable NSURL *)url {
dispatch_main_async_safe(^{
if (completionBlock) {
completionBlock(image, data, error, cacheType, finished, url);
}
});
}
最后一个步骤,就是拿到结果,这个结果可能是查询缓存查到的缓存图片数据,也可能是网络下载的图片数据,最后都要回到主线程去给imageView.image
设置图片。
小结:
以上只是通过一个常用接口逐步深入源码,学习篇幅虽有限但在代码中我们仍能学到不少东西:
1.通过定义协议来抽象行为,并利用依赖注入技术提供具体实现,极大提高了代码的可测试性和模块化程度。
2.代码中,使用上下文(context)作为传递额外配置信息的方式,使得功能扩展更加灵活且不需要修改核心方法签名。
3.利用强弱引用避免循环引用,优化内存使用。
4.使用锁机制来保证多线程安全。
5.如果操作被取消,则调用完成块并移除操作safelyRemoveOperationFromRunning,考虑是否操作已经被取消这么一个结果。
6.代码模块化,实现项目的高聚合低耦合。
7.缓存的妙用。
参考:
GitHub - SDWebImage/SDWebImage: Asynchronous image downloader with cache support as a UIImageView category
https://juejin.cn/post/6893704263407501325
SDWebImage源码解析(一) | 幸运四叶草
SDWebImage源码解析(二) | 幸运四叶草
SDWebImage源码解析(三) | 幸运四叶草
源码分析之SDWebImage(一)-腾讯云开发者社区-腾讯云
iOS/Swift学习/SDWebImage深入学习.md at master · sebarina/iOS · GitHub