1. 前言
一直没有系统的阅读过整套源码,仅仅是看过几篇总结的文章,了解了设计思路,年前项目刚结束,终于有大块时间仔细阅读一下 SDWebImage 框架了。开始阅读的时候感觉很简单,也没有添加自己的注释,最后越看越乱,发现很多细节处理和逻辑完整性和我之前自己思考的不太一样,就这样乱糟糟的看了一下午,感觉这样通读的效果不是很好。今天换了一种思维来理解,看别人的源码和做英语阅读一样,先笼统的浏览一遍,然后带着问题阅读,效果特别好。简单看了一遍,记录一下学习笔记。
2. 概况
首先看 UIImageView+WebCache 这个扩展类,给 imageView 添加图片的方法一共有以下几种:
- (void)sd_setImageWithURL:(NSURL *)url;
- (void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder;
- (void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder options:(SDWebImageOptions)options;
- (void)sd_setImageWithURL:(NSURL *)url completed:(SDWebImageCompletionBlock)completedBlock;
- (void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder completed:(SDWebImageCompletionBlock)completedBlock;
- (void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder options:(SDWebImageOptions)options completed:(SDWebImageCompletionBlock)completedBlock;
- (void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder options:(SDWebImageOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageCompletionBlock)completedBlock;
- (void)sd_setImageWithPreviousCachedImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder options:(SDWebImageOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageCompletionBlock)completedBlock;
以上方法最后都会调用下面的方法:
- (void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder options:(SDWebImageOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageCompletionBlock)completedBlock;
也就可以确定,这个方法就是我们阅读源码的入口了。大概知道这些参数都是什么意思:
url 和 placeholder 不用再介绍了
SDWebImageOptions 应该是枚举,进入所在类查看,是位掩码,每个字段含义不明确,后面再分析
typedef NS_OPTIONS(NSUInteger, SDWebImageOptions) {
...
};
SDWebImageDownloaderProgressBlock 表示正在下载的时候所要处理的事情
SDWebImageCompletionBlock 表示下载完成后所要做的事
3. 详解
接下来看看方法里面的具体实现:
- (void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder options:(SDWebImageOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageCompletionBlock)completedBlock {
// 先将原来的 operation 停止(详情见 3.1)
[self sd_cancelCurrentImageLoad];
// 给 imageView 关联要下载的图片的url
objc_setAssociatedObject(self, &imageURLKey, url, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
// SDWebImageDelayPlaceholder : 占位图延迟加载标识
// 默认情况下,占位图先赋值给imageView,然后加载图像。这个标志是图像加载完毕之前占位图不先赋值给imageVIew。
// 在没有发送请求获取网络端图片之前、如果 options 不等于 SDWebImageDelayPlaceholder
if (!(options & SDWebImageDelayPlaceholder)) {
// 刚看到这里懵逼了一下,印象中没有 dispatch_main_async_safe 这个方法啊,进去发现是一个宏定义,目的是UI相关操作要在主线程,没啥技术点,略过
dispatch_main_async_safe(^{
self.image = placeholder;
});
}
// 如果 url 存在
if (url) {
// setShowActivityIndicatorView: 暴露在头文件中,设置加载图片的时候是否在 imageView 中央添加菊花
// showActivityIndicatorView : 是 setShowActivityIndicatorView 的 get 方法、返回 BOOL
// 此状态是用 runtime 关联到 imageView 上的
// check if activityView is enabled or not
// 是否添加菊花
if ([self showActivityIndicatorView]) {
// 添加菊花
[self addActivityIndicator];
}
__weak __typeof(self)wself = self;
// 下载图片(详情见 3.2)
id <SDWebImageOperation> operation = [SDWebImageManager.sharedManager downloadImageWithURL:url options:options progress:progressBlock completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
// 图片加载完成、移除菊花
[wself removeActivityIndicator];
if (!wself) return;
dispatch_main_sync_safe(^{
if (!wself) return;
// SDWebImageAvoidAutoSetImage : 不允许自动将 image 添加到 imageView
// 默认情况下,图像下载后直接添加到 imageView,但是在某些情况下,我们希望在设置图像之前先有交互(例如应用滤镜或添加交叉淡入淡出动画),如果要在成功完成时手动设置图像,请使用此标志.
// 如果图片下载成功 并且 (options = SDWebImageAvoidAutoSetImage)
// 这个就是我之前常用的方法,获得 image 再赋予 button,现在才发现有 UIButton+WebCache 扩展类
if (image && (options & SDWebImageAvoidAutoSetImage) && completedBlock)
{
// 直接返回 image,不添加到 imagView
completedBlock(image, error, cacheType, url);
return;
}
else if (image) {
// 如果image获取成功,直接添加 image 到 imageView
wself.image = image;
[wself setNeedsLayout];
} else {
// 如果 image获取失败,并且 (options = SDWebImageDelayPlaceholder),才将 placeholder 添加到 imageView,赤裸裸的备胎
if ((options & SDWebImageDelayPlaceholder)) {
wself.image = placeholder;
[wself setNeedsLayout];
}
}
if (completedBlock && finished) {
completedBlock(image, error, cacheType, url);
}
});
}];
// 将正在下载的 operation 添加到 operation 字典中(详情见 3.3)
[self sd_setImageLoadOperation:operation forKey:@"UIImageViewImageLoad"];
} else { // 如果 url 为空,则抛出错误
dispatch_main_async_safe(^{
// 移除菊花
[self removeActivityIndicator];
if (completedBlock) {
NSError *error = [NSError errorWithDomain:SDWebImageErrorDomain code:-1 userInfo:@{NSLocalizedDescriptionKey : @"Trying to load a nil url"}];
completedBlock(nil, error, SDImageCacheTypeNone, url);
}
});
}
}
3.1. 将原来的 operation 停止
如果刚刚已经请求了图片,当 imageView 再次加载图片之前,需要取消之前的请求。
进入 sd_cancelCurrentImageLoad 方法
- (void)sd_cancelCurrentImageLoad {
[self sd_cancelImageLoadOperationWithKey:@"UIImageViewImageLoad"];
}
再进入 sd_cancelImageLoadOperationWithKey: 方法
- (void)sd_cancelImageLoadOperationWithKey:(NSString *)key {
// Cancel in progress downloader from queue
// 取消正在进行的下载队列
// 获取正在下载的队列
NSMutableDictionary *operationDictionary = [self operationDictionary];
// 获取 @"UIImageViewImageLoad" 对应的 value
id operations = [operationDictionary objectForKey:key];
if (operations) {
// 如果 value 是数组类型
if ([operations isKindOfClass:[NSArray class]]) {
for (id <SDWebImageOperation> operation in operations) {
// 如果任务存在,则取消任务
if (operation) {
[operation cancel];
}
}
} else if ([operations conformsToProtocol:@protocol(SDWebImageOperation)]){ // 如果符合协议
[(id<SDWebImageOperation>) operations cancel];
}
// 最后移除key对应的value
[operationDictionary removeObjectForKey:key];
}
}
代码很容易理解,先获取到 operation 的序列,即 [self operationDictionary]。然后根据 key 索引到对应的 operation,如果 operation 存在,就要取消该 operation。这里需要注意的地方就是,索引到的 operation 其实是一组 operation 集合,那么久需要遍历一个个取消序列中的 operation。最后移除 key 对应的 object。
这里有个疑惑:为啥 operation 都是 id ?而且进入 SDWebImageOperation 后发现只有一个 cancel 方法。为什么这样设计,还有待进一步研究。
3.2. 下载图片
id <SDWebImageOperation> operation = [SDWebImageManager.sharedManager downloadImageWithURL:url options:options progress:progressBlock completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
...
}];
进入后发现这是 SDWebImageManager 的方法,这就是我们继续探索源码核心的下一个入口,这里不做过多介绍,我们将在下一篇中重点介绍。
3.3. 添加新的operation 到队列
进入 sd_setImageLoadOperation: forKey: 方法
- (void)sd_setImageLoadOperation:(id)operation forKey:(NSString *)key {
// 取消正在进行的下载队列
[self sd_cancelImageLoadOperationWithKey:key];
NSMutableDictionary *operationDictionary = [self operationDictionary];
[operationDictionary setObject:operation forKey:key];
}
看到方法内的实现,就感觉没有什么难度了,sd_cancelImageLoadOperationWithKey: 方法在 3.1 中介绍了,接下来将该 operation 添加到下载队列中。
4. 总结
到此为止,这个类中主要的方法、属性都解析出来了。其他的比如运用 runtime 关联属性等等就不一一介绍了,都比较简单。这个扩展类其实没有太多核心思想,主要就是设计了严密的判断,保证运用该框架的其他开发者能够在各种环境下请求图片。遗留问题就是 SDWebImageManager 下载图片方法内的实现,我们将在下一篇着重介绍。
SDWebImage 源码解析 github 地址:https://github.com/Mayan29/SDWebImageAnalysis