SDWebImage(以下简称SD)作为iOS开发不可或缺的图片工具库、GitHub上18K star的存在,不得不说,很强大。那么, 抱着敬畏之心,我们来看一看它的源码吧。
说到SD,最先想到的就是下面这个方法了,那么就先从这它开始一步步看具体实现吧。
/**
* 异步下载并缓存
* 第一个参数是图片URL,第二个参数是占位图
*/
- (void)sd_setImageWithURL:(nullable NSURL *)url
placeholderImage:(nullable UIImage *)placeholder;
一层一层点进去,最终进入这个方法:
// 所有设置图片调用的方法最终都会调用这个方法
- (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;
它的实现:
/**
* 首先,取消先前的下载任务
* 因为sd_setImageWithURL:placeholderImage:这个方法我们并没有传入operationKey,
* 所以它创建了一个新key,然后通过runtime添加为属性
*/
NSString *validOperationKey = operationKey ?: NSStringFromClass([self class]);
[self sd_cancelImageLoadOperationWithKey:validOperationKey];//取消先前的下载任务
objc_setAssociatedObject(self, &imageURLKey, url, OBJC_ASSOCIATION_RETAIN_NONATOMIC);//动态添加属性
/**
* 之后,设置占位图
* 这里options & SDWebImageDelayPlaceholder用了按位与操作
* 举个例子:3 & 7 -> 00000011 & 00000111 = 00000011
* 也就是说,如果我们选择的options中包含SDWebImageDelayPlaceholder,就设置占位图
*/
if (!(options & SDWebImageDelayPlaceholder)) {
dispatch_main_async_safe(^{
[self sd_setImage:placeholder imageData:nil basedOnClassOrViaCustomSetImageBlock:setImageBlock];//设置占位图
});
}
/**
* 接下来,判断url是否为nil
* 如果是nil,block回调异常等信息
* 否则执行下载操作
*/
if (url) {
//下载操作
__weak __typeof(self)wself = self;
id operation = [SDWebImageManager.sharedManager loadImageWithURL:url options:options progress:progressBlock completed:^(UIImage *image, NSData *data, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
if (!sself) {
return;// 如果self被释放,return
}
//拿到image,给imageView设置图片并回调
coding....
}];
} else {
//回调异常...
}
下载回调中特别有意思的宏, strcmp函数判断当前队列与主队列的label是否相同,从而得出当前队列是否为主队列。如果是,因为主队列执行能等同于主线程执行,所以直接回调; 否则回到主线程回调。
#ifndef dispatch_main_async_safe
#define dispatch_main_async_safe(block)\
if (strcmp(dispatch_queue_get_label(DISPATCH_CURRENT_QUEUE_LABEL), dispatch_queue_get_label(dispatch_get_main_queue())) == 0) {\
block();\
} else {\
dispatch_async(dispatch_get_main_queue(), block);\
}
#endif
下载操作:
/**
* 第一个参数,图片url
* 第二个参数,下载设置
* 第三个参数,进度回调
* 第四个参数,完成回调
*/
- (id )loadImageWithURL:(nullable NSURL *)url
options:(SDWebImageOptions)options
progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
completed:(nullable SDInternalCompletionBlock)completedBlock;
下载具体实现:
//生成一个新的任务
__block SDWebImageCombinedOperation *operation = [SDWebImageCombinedOperation new];
__weak SDWebImageCombinedOperation *weakOperation = operation;
//判断url是不是失败过的URL
BOOL isFailedUrl = NO;
if (url) {
@synchronized (self.failedURLs) {
isFailedUrl = [self.failedURLs containsObject:url];
}
}
//url为nil或者 URL下载失败且设置了失败不再重试
if (url.absoluteString.length == 0 || (!(options & SDWebImageRetryFailed) && isFailedUrl)) {
//失败处理
dispatch_main_sync_safe(^{ //线程锁,保证线程安全
NSError *error = [NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorFileDoesNotExist userInfo:nil];
completedBlock(nil, error, SDImageCacheTypeNone, YES, url);
});
return operation;
}
//将任务添加到下载队列并生成key
@synchronized (self.runningOperations) {
[self.runningOperations addObject:operation];
}
NSString *key = [self cacheKeyForURL:url];//根据URL生成key
以下为获取Image操作,因为代码过长,只说说我对代码的理解:
- 首先,通过key在缓存中查找是否存在对应image。若存在,回调image并return;若不存在,继续尝试从磁盘中获取图片,如果找到了,更新缓存与磁盘的存储数据。不管image是不是存在,回调image;
- 得到NSOperation对象,目的是为了使用NSOperation的取消方法,通过设置取消方法来达到取消异步从磁盘中读取图片的操作;
- 在取消操作的block中取消下载,从下载队列中移除下载操作;
- 根据operation.isCancelled判断是否继续进行。不进行的话从下载队列中移除;
- 如果Image存在,complete回调;
- 调用downloadImageWithURL: options: progress: completed:下载Image,完成后将图片存进缓存并complete回调;
- 移除下载操作;
- return operation。
总结:
- 涉及下载的操作都放在支线程执行;
- 图片缓存;
- 有重试机制;
- 利用了NSOperation的可控性进行耗时操作。
谢谢@大尾巴熊Johnny指出文章的错误。