前言
开发iOS有一段时间了,平时工作中主要还是完成业务功能。类似网络请求,图片加载等等都直接使用现成的开源类库,项目主要还是以稳定为先。
但长期这样感觉难以进步,想要进阶除了看书外就得多看看开源类库的源码了。
于是就从SDWebImage入手,在深入学习后发现它的代码各层职责分工明确,代码量也不是很多,利用业余时间断断续续学习花费了大约三周时间,感觉比较适合作为第一个供学习的开源类库。
大致涉及到的知识点:
- Block
- GCD
- NSOperation
- Associated Objects
- NSURLRequest
- NSCache
- 图片类型识别与处理
文章中难免出现问题,望各位给予纠正,有问题欢迎一起讨论。
源码分析
SDWebImage使用起来非常简单,只需调用sd_setImageWithURL
方法,就可以将图片异步的加载并显示在UIImageView上。
所以接下来我们就从sd_setImageWithURL
开始说起:
NSURL * url = [NSURL URLWithString:@"http://hbimg.b0.upaiyun.com/ddd2cee8ff21d4a09a86b68972b78b15ba7bc2a035fa4-sGYzEJ_fw658"];
[imageView sd_setImageWithURL:url];
上面代码所使用的是sd_setImageWithURL
最简单的版本,我们跟进去看一下,发现方法里其实帮我们设置好了默认参数,最终调用到的是另一个方法:
- (void)sd_setImageWithURL:(NSURL *)url {
[self sd_setImageWithURL:url placeholderImage:nil options:0 progress:nil completed:nil];
}
我们跟进去看看,通过注释可以得知这个方法的用途:
/**
* �根据url给imageView设置image,占位图和各种自定义设置
*
* 使用异步下载和缓存
*
* @param url 图片的url
* @param placeholder 占位图
* @param options 下载图片时的各种设置. @see SDWebImageOptions.
* @param progressBlock 当图片正在下载时会被回调到
* @param completedBlock 当任务完成时会被回调到 。该block没有返回值使用UIImage作为第一个参数
* 如果下载中出现错误UIIMage为nil并且第二个参数会包含NSError
* 第三个参数是一个枚举(*原注释这块写的是布尔值),表示图片是从本地缓存中还是网络中取回的
* 第四个参数是原生的image url
*/
- (void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder options:(SDWebImageOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageCompletionBlock)completedBlock;
//completedBlock,参数与注释对应
typedef void(^SDWebImageCompletionBlock)(UIImage *image, NSError *error, SDImageCacheType cacheType, NSURL *imageURL);
接着我们看代码,然后一步步分析:
- (void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder options:(SDWebImageOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageCompletionBlock)completedBlock {
//取消当前UIImageView正在加载的图片任务
[self sd_cancelCurrentImageLoad];
//相当于给当前UIImageView对象上绑定图片url属性
objc_setAssociatedObject(self, &imageURLKey, url, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
//如果options中没有传入SDWebImageDelayPlaceholder参数,则设置占位图
//这里出现了dispatch_main_async_safe,其实是SDWebImage定义的宏,其实就是将UI操作放入主线程中用的
if (!(options & SDWebImageDelayPlaceholder)) {
dispatch_main_async_safe(^{
self.image = placeholder;
});
}
if (url) {
// 检查是否打开了"会转动的菊花"选项
if ([self showActivityIndicatorView]) {
[self addActivityIndicator]; //< 界面上会出现转动的菊花
}
__weak __typeof(self)wself = self;
//从方法名中可以猜出它是用来下载图片用的,目前只需要这么理解就好,后面章节会具体谈到
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添加进UIViewImage对象,而是放置到completedBlock中交由调用方自己处理,比如做个滤镜或者添加淡出淡入效果什么的
if (image && (options & SDWebImageAvoidAutoSetImage) && completedBlock)
{
completedBlock(image, error, cacheType, url);
return;
}
else if (image) {
wself.image = image; //< 设置image
[wself setNeedsLayout];
} else { //< 当image为nil
if ((options & SDWebImageDelayPlaceholder)) {
wself.image = placeholder;//< 此时再将占位图设置进去
[wself setNeedsLayout];
}
}
if (completedBlock && finished) {
completedBlock(image, error, cacheType, url);
}
});
}];
//保存本次operation,如果发生多次图片请求加载可以用来取消
//先取消当前UIImageView正在加载的任务,再保存operation
[self sd_setImageLoadOperation:operation forKey:@"UIImageViewImageLoad"];
} else {
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);
}
});
}
}
这里先提几个点:
1.在代码中我们会发现有dispatch_main_async_safe
这么一个神奇的东东,其实它是SDWebImage定义的宏,将UI操作放入主线程中用的:
#define dispatch_main_async_safe(block)
if ([NSThread isMainThread]) { //< 如果当前在主线程中
block();
} else { //< 不在主线程就将它放入主线程
dispatch_async(dispatch_get_main_queue(), block);
}
2.代码中偶尔会出现objc_setAssociatedObject
,简单的说使用该技巧可以很方便的将变量动态绑定在该实例下,原因在于Category中是不允许添加实例变量。
回到主题来,代码在请求下载图片前执行了[self sd_cancelCurrentImageLoad]
,从方法名上可以猜出它的大意“取消当前图片的加载”,他是作什么用的呢,为什么在加载图片前会需要用到取消这么一个方法?带着疑问我们继续,发现调用了另一个方法,看来这里只负责传入对应的“key”
- (void)sd_cancelCurrentImageLoad {
[self sd_cancelImageLoadOperationWithKey:@"UIImageViewImageLoad"];
}
再跟进来我们可以看到具体的实现了
- (void)sd_cancelImageLoadOperationWithKey:(NSString *)key {
//利用AssociatedObject维护的字典,用于存放当前任务中的operation(图片请求)
NSMutableDictionary *operationDictionary = [self operationDictionary];
//key为"UIImageViewImageLoad"
id operations = [operationDictionary objectForKey:key];
if (operations) { //< 当前有正在执行的operation,需要取消任务
//多个operation的是gif(多帧),单个的是普通图片
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的对象
//每次对应UIView有一个图片请求的任务时,都会设置对应的key,所以可以根据这个key来判断是否有正在执行的任务
[operationDictionary removeObjectForKey:key];
}
}
看完上面这段代码后,我们大致有了一个概念,同时也发现这两段代码的“key”是一样的:
//取消当前UIImageView正在加载的图片任务
[self sd_cancelCurrentImageLoad];
...
//保存本次operation,如果发生多次图片请求加载可以用来取消
//先取消当前UIImageView正在加载的任务,再保存operation
[self sd_setImageLoadOperation:operation forKey:@"UIImageViewImageLoad"];
再回到刚才的疑问,举个例子来说就能明白方法的意图和具体流程了:
UIImageView *imageView = [[UIImageView alloc] initWithFrame:CGRectMake(100, 100, 50, 50)];
[imageView sd_setImageWithURL:[NSURL URLWithString:@"http://www.example.com/1.png"] placeholderImage:nil];
[imageView sd_setImageWithURL:[NSURL URLWithString:@"http://www.example.com/2.png"] placeholderImage:nil];
一个imageView请求了两张图片,1.png 和 2.png,但我们只希望显示 2.png,所以需要取消 1.png的请求。原因有两点:
1.在异步请求中(先后顺序不定),有可能 1.png 会在 2.png 后面获取到,会覆盖掉2.png
2.减少网络请求,网络请求是一个很耗时的操作