SDWebImage源码学习笔记 ☞ 结构及基本流程
一、前言
决定动笔之前想了好久,到底应该按照什么顺序分析才能更有条理性,最终决定采用这样的方式:先从一个简单的案例入手,一步步走完整个流程,然后针对过程中遇到的主要功能点逐个讨论,最终达到对整个框架有一个相对完整的印象。
本篇的目标就是通过案例梳理流程,作为后边章节的主线。
二、目录结构
为了查看 SDWebImage
的完整目录结构,首先需要导入 SDWebImage,由 上一篇 我们了解到,新版 SDWebImage
总共分了 4 个子 pod,默认只导入了 Core,为了将它们全部导入 demo 中,Podfile 文件需要这么来写:
source 'https://github.com/CocoaPods/Specs.git'
platform :ios, '9.0'
target 'HHSDWebImageStudy' do
pod 'SDWebImage', '4.4.2' // 为了讨论方便,这里选定了一个比较新的版本
pod 'SDWebImage/WebP'
pod 'SDWebImage/GIF'
pod 'SDWebImage/MapKit'
end
执行 pod install
之后得到的目录结构如下图所示,点此查看完整目录结构 。
三、示例:用 UIImageView 展示一张静态网络图片
下面是使用时的代码,加载图片时调用的是 UIImageView+WebCache
中的方法 sd_setImageWithURL:
。
// 1.创建 UIImageView
UIImageView *imgV = [[UIImageView alloc] initWithFrame:CGRectMake(125, 70, 160, 160)];
[self.view addSubview:imgV];
// 2.加载网络图片
[imgV sd_setImageWithURL:[NSURL URLWithString:@"https://img.zcool.cn/community/01c81558a2723ca801219c77a1e34e.jpg"]];
查看 sd_setImageWithURL:
的实现,内部调用了一个参数很全的方法 sd_setImageWithURL: placeholderImage: options: progress: completed:
,只不过其他参数已经给了默认值。
- (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 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_setImageWithPreviousCachedImageWithURL:(nullable NSURL *)url
placeholderImage:(nullable UIImage *)placeholder
options:(SDWebImageOptions)options
progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
completed:(nullable SDExternalCompletionBlock)completedBlock {
// 1.取出本地缓存
NSString *key = [[SDWebImageManager sharedManager] cacheKeyForURL:url];
UIImage *lastPreviousCachedImage = [[SDImageCache sharedImageCache] imageFromCacheForKey:key];
// 2.调用本类中的 参数很多的方法,并将缓存数据 (没有时用 placeholder) 传给 placeholder
[self sd_setImageWithURL:url
placeholderImage:lastPreviousCachedImage ?: placeholder
options:options
progress:progressBlock
completed:completedBlock];
}
再来看看这个参数巨多的方法 sd_setImageWithURL: placeholderImage: options: progress: completed:
究竟是如何实现的:
- (void)sd_setImageWithURL:(nullable NSURL *)url
placeholderImage:(nullable UIImage *)placeholder
options:(SDWebImageOptions)options
progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
completed:(nullable SDExternalCompletionBlock)completedBlock {
// 跳转 UIView+WebCache 中的 sd_internalSetImageWithURL: 方法执行,若 <= 3.x.x 时,sd_internalSetImageWithURL: 在本类内部
[self sd_internalSetImageWithURL:url
placeholderImage:placeholder
options:options
operationKey:nil
setImageBlock:nil
progress:progressBlock
completed:completedBlock];
}
还是继续调用别的方法,不过这次是调用父类分类 (UIView+WebCache) 中的方法,之所以要调用直接或间接父类的方法,是为了让其他控件 (如 UIButton 等) 可以复用加载网络图片的方法,他们间的关系如下:
UIView和直接或间接子类间的关系.png现在去父类的分类 UIView+WebCache
看看吧,为了缩减篇幅,以下代码做了适当精简。
- (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 {
return [self sd_internalSetImageWithURL:url
placeholderImage:placeholder
options:options
operationKey:operationKey
setImageBlock:setImageBlock
progress:progressBlock
completed:completedBlock
context:nil];
}
// *** 核心方法
- (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
context:(nullable NSDictionary<NSString *, id> *)context {
// 1.取消当前 validOperationKey 对应的 loadOperation
NSString *validOperationKey = operationKey ?: NSStringFromClass([self class]);
[self sd_cancelImageLoadOperationWithKey:validOperationKey];
// 创建 manager(两种:1.用户自定义 2.此库自带的单例)
SDWebImageManager *manager;
if ([context valueForKey:SDWebImageExternalCustomManagerKey]) {
manager = (SDWebImageManager *)[context valueForKey:SDWebImageExternalCustomManagerKey];
} else {
manager = [SDWebImageManager sharedManager];
}
// 2.下载
id <SDWebImageOperation> operation = [manager loadImageWithURL:url
options:options
progress:combinedProgressBlock
completed:^(UIImage *image, NSData *data, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL)
{
__strong __typeof (wself) sself = wself;
// ...
SDWebImageNoParamsBlock callCompletedBlockClojure = ^{
if (!sself) { return; }
if (!shouldNotSetImage) {
[sself sd_setNeedsLayout];
}
if (completedBlock && shouldCallCompletedBlock) {
completedBlock(image, error, cacheType, url);
}
};
// ...
dispatch_main_async_safe(^{
#if SD_UIKIT || SD_MAC
[sself sd_setImage:targetImage imageData:targetData basedOnClassOrViaCustomSetImageBlock:setImageBlock transition:transition cacheType:cacheType imageURL:imageURL];
#else
[sself sd_setImage:targetImage imageData:targetData basedOnClassOrViaCustomSetImageBlock:setImageBlock];
#endif
callCompletedBlockClojure();
});
}];
// 3.为 UIImageView 绑定新的 operation,即上边这个 operation
[self sd_setImageLoadOperation:operation forKey:validOperationKey];
} else { // 如果图片 URL 不存在,执行 completedBlock,返回错误提示
dispatch_main_async_safe(^{
if (completedBlock) {
NSError *error = [NSError errorWithDomain:SDWebImageErrorDomain code:-1 userInfo:@{NSLocalizedDescriptionKey : @"Trying to load a nil url"}];
completedBlock(nil, error, SDImageCacheTypeNone, url);
}
});
}
}
可以看到,父类最终调用了 - (void)sd_internalSetImageWithURL: ...
这个核心方法,虽然实现代码较多,其实主要就做了这么 3 件事:
① 取消 validOperationKey 对应的已经存在的 loadOperation。因为传进来的参数 validOperationKey 是 nil,所以 validOperationKey 实际取的是当前类名对应的字符串 NSStringFromClass([self class])。
② 执行 SDWebImageManager
的方法 - (id <SDWebImageOperation>)loadImageWithURL: ...
下载图片,并返回一个 operation 对象,其实是这个类 SDWebImageCombinedOperation
的实例,与上一步 cancel 的对象是同一类型,下面就会用到。
③ 保存 validOperationKey 与刚刚生成的 operation 之间的映射关系,以备取消时使用(如 ①)。
对于 ② 用到的方法,其实现代码太长,此处代码也做了精简:
// SDWebImageManager
- (id <SDWebImageOperation>)loadImageWithURL:(nullable NSURL *)url
options:(SDWebImageOptions)options
progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
completed:(nullable SDInternalCompletionBlock)completedBlock
{
// ...
// 1.创建 operation
SDWebImageCombinedOperation *operation = [SDWebImageCombinedOperation new];
operation.manager = self; // 肯定是 weak 属性
__weak typeof(strongOperation) weakSubOperation = strongOperation;
// 2.使用 imageCache 的方法查询缓存
operation.cacheOperation = [self.imageCache queryCacheOperationForKey:key
options:cacheOptions
done:^(UIImage *cachedImage, NSData *cachedData, SDImageCacheType cacheType)
{
// 以下都是查询结束后 (查到/没查到) 的操作
if (shouldDownload) {
// 3. 需要下载
__weak typeof(strongOperation) weakSubOperation = strongOperation;
strongOperation.downloadToken = [self.imageDownloader downloadImageWithURL:url
options:downloaderOptions
progress:progressBlock
completed:^(UIImage *downloadedImage, NSData *downloadedData, NSError *error, BOOL finished)
{
// 缩放图片(如果可以的话)
// 缓存图片
// 执行完成的回调
}
} else if (cachedImage) {
// 4. 如果取到了缓存
// 执行完成的回调
[self callCompletionBlockForOperation:strongOperation completion:completedBlock image:cachedImage data:cachedData error:nil cacheType:cacheType finished:YES url:url];
// 从正在运行的 operation 数组中移除当前 operation
[self safelyRemoveOperationFromRunning:strongOperation];
} else {
// 5. 没取到缓存 && 不允许下载
// 执行完成的回调
// Image not in cache and download disallowed by delegate
[self callCompletionBlockForOperation:strongOperation completion:completedBlock image:nil data:nil error:nil cacheType:SDImageCacheTypeNone finished:YES url:url];
// 从正在运行的 operation 数组中移除当前 operation
[self safelyRemoveOperationFromRunning:strongOperation];
}
}
return operation;
}
该方法主要是在创建 SDWebImageCombinedOperation
对象,大概的操作见上边的代码注释。只对其中 2 个自己认为更重要点的方法做一个简要说明,详细的讨论可以查看后边的相关篇章。
-
SDImageCache
中查询缓存的方法- (nullable NSOperation *)queryCacheOperationForKey: ...
。这个方法的作用是查询二级缓存,也就依次从内存、磁盘两处缓存查询我们需要的图片数据,前者查不到时,才在后者中查找。
// SDImageCache
- (nullable NSOperation *)queryCacheOperationForKey:(nullable NSString *)key
options:(SDImageCacheOptions)options
done:(nullable SDCacheQueryCompletedBlock)doneBlock;
-
SDWebImageDownloader
中下载图片的方法- (nullable SDWebImageDownloadToken *)downloadImageWithURL: ...
。此方法的实现通过 operation 和 operationQueue 配合使用来执行下载操作的,即 先分别创建 operation 和 operationQueue,然后将 operation 添加到 operationQueue 中,就自动启动任务了。
// SDWebImageDownloader
- (nullable SDWebImageDownloadToken *)downloadImageWithURL:(nullable NSURL *)url
options:(SDWebImageDownloaderOptions)options
progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock;
- 无论是查询缓存还是请求网络数据,最终都会给 imageView 赋值,也就是我们通常希望达到的效果。
四、小结
至此,我们大概理了一下 使用 SDWebImage
加载网络图片的主要流程,最后借用 SDWebImage
作者提供的一张时序图做个简单总结吧。