简述
Asynchronous image downloader with cache support as a UIImageView category.
上面的是关于SDWebImage的官方说明, 可以翻译为: 一个支持缓存的, 采用UIImageView分类形式的图片下载器.
共包含以下功能:
- 以UIImageView的分类, 来支持网络图片的加载和缓存管理
- 一个异步的图片加载器
- 一个异步的内存+磁盘图片缓存
- 支持GIF
- 支持WebP
- 后台图片解压缩处理
- 确保同一个URL的图片不被多次下载
- 确保虚假的URL不会被反复加载
- 确保下载及缓存时, 主线程不被阻塞
- 使用GCD和ARC
- 支持ARM64
源码分析
以UIImageView图片加载为例, 最常用的方法就是如下方法(类目 UIImageView + WebCache 里):
- (void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder
内部调用以下方法:
- (void)sd_setImageWithURL:(NSURL *)url
placeholderImage:(UIImage *)placeholder
options:(SDWebImageOptions)options
progress:(SDWebImageDownloaderProgressBlock)progressBlock
completed:(SDWebImageCompletionBlock)completedBlock
对这个方法进行如下结构解析:
取消当前图片加载
为防止同一个UIImageView对象同时加载多个image, 需首先取消当前UIImageView对象的图片加载, 如下:
[self sd_cancelCurrentImageLoad];
下载图片操作的存储和取消单独放在一个UIView的类目里, 这样的设计让代码更简洁, 耦合性更低. UIView+WebCacheOperation, 这个类目主要有三个方法,
- 添加下载操作
- (void)sd_setImageLoadOperation:(id)operation forKey:(NSString *)key {
[self sd_cancelImageLoadOperationWithKey:key];
NSMutableDictionary *operationDictionary = [self operationDictionary];
[operationDictionary setObject:operation forKey:key];
}
- 取消下载操作
- (void)sd_cancelImageLoadOperationWithKey:(NSString *)key {
// Cancel in progress downloader from queue
NSMutableDictionary *operationDictionary = [self operationDictionary];
id operations = [operationDictionary objectForKey:key];
if (operations) {
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];
}
[operationDictionary removeObjectForKey:key];
}
}
- 移除下载操作
- (void)sd_removeImageLoadOperationWithKey:(NSString *)key {
NSMutableDictionary *operationDictionary = [self operationDictionary];
[operationDictionary removeObjectForKey:key];
}
上述代码普遍使用了关联对象,主要会用到以下两个方法:
- void objc_setAssociatedObject(id object, void *key, id value, objc_AssociationPolicy policy)
用来给对象添加成员关联对象 - id objc_getAssociatedObject(id object, void *key)
通过key值获取对应的关联对象
一般来说, 关联对象在OC中主要解决如下这样的问题:
我们需要在类目中保存一些状态,可以使用成员变量的形式来实现,但是,这些状态又是相对独立的, 只在这个类目中会被用到。所以,我们想把这个成员变量隐藏在这个类目中,对于其他的类目,这个成员变量都不可见。
如果类目允许扩展成员变量的话,这个就很好解决, 直接在类目的实现文件里声明一个成员变量即可。这样既对外部隐藏这个成员变量,又能在这个类目中使用它。很完美的解决方案。但是不幸的是,OC不允许在类目中扩展成员变量。所以不得不在类的声明中添加需要的成员变量,而且还需要把它暴露出来,以使你的分类能够使用它。这样,随着类目越来越多,你不得不在类中声明越来越多原本需要对外隐藏的成员变量。
以上代码所用的关联对象就很好的解决了这个问题,不需要在声明中创建对外可见的成员变量operationDictionary,只用key: loadOperationKey 关联了字典operationDictionary,就可以用如下的方式来使用此对象。
NSMutableDictionary *operationDictionary = [self operationDictionary];
[operationDictionary setObject:operation forKey:key];
具体可参考雷纯峰的博客:http://blog.leichunfeng.com/blog/2015/06/26/objective-c-associated-objects-implementation-principle/
取消图片加载操作的内部逻辑是:
利用key: UIImageViewImageLoad 去操作字典中查找, 如果存在下载图片的操作则取消.
代理 SDWebImageOperation 只有一个 -cancel 方法, 目的是为服从此代理的所有 Operation 类都提供一个统一的方法名, 而不需要在 SDWebImageCombinedOperation 和 SDWebImageDownloaderOperation 的头文件中再单独声明取消方法. - sd_cancelImageLoadOperationWithKey 方法中的 operation属于 SDWebImageCombinedOperation 类, 它的取消方法如下:
- (void)cancel {
self.cancelled = YES;
if (self.cacheOperation) {
[self.cacheOperation cancel];
self.cacheOperation = nil;
}
if (self.cancelBlock) {
self.cancelBlock();
// TODO: this is a temporary fix to #809.
// Until we can figure the exact cause of the crash, going with the ivar instead of the setter
// self.cancelBlock = nil;
_cancelBlock = nil;
}
}
上面的方法包含两个取消操作
- [self.cacheOperation cancel], 这个是取消查找存储(缓存)图片操作
- self.cancelBlock(), 这个block 为如下代码:
operation.cancelBlock = ^{
[subOperation cancel];
@synchronized (self.runningOperations) {
__strong __typeof(weakOperation) strongOperation = weakOperation;
if (strongOperation) {
[self.runningOperations removeObject:strongOperation];
}
}
};
其中 subOperation 属于SDWebImageDownloaderOperation类, 为下载图片的操作类, 它将执行最终的取消下载方法, 如下:
- (void)cancel {
@synchronized (self) {
if (self.thread) {
[self performSelector:@selector(cancelInternalAndStop) onThread:self.thread withObject:nil waitUntilDone:NO];
}
else {
[self cancelInternal];
}
}
}
如上所示, 如果operations对象在单独的线程里, 需要单独处理线程, 取消下载后, 要停止当前线程的runloop, CFRunLoopStop(CFRunLoopGetCurrent()), 具体如下代码:
- (void)cancelInternalAndStop {
if (self.isFinished) return;
[self cancelInternal];
CFRunLoopStop(CFRunLoopGetCurrent());
}
一系列的操作, 最终会通过苹果提供的NSURLConnection类的方法 cancel 来完成本次取消下载任务, 如下代码所示:
- (void)cancelInternal {
if (self.isFinished) return;
[super cancel];
if (self.cancelBlock) self.cancelBlock();
if (self.connection) {
// 取消下载的操作最终会来到这里
[self.connection cancel];
dispatch_async(dispatch_get_main_queue(), ^{
[[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStopNotification object:self];
});
// As we cancelled the connection, its callback won't be called and thus won't
// maintain the isFinished and isExecuting flags.
if (self.isExecuting) self.executing = NO;
if (!self.isFinished) self.finished = YES;
}
[self reset];
}
以上就是 [self sd_cancelCurrentImageLoad] 这个方法的逻辑步骤.
②. 然后通过runtime把图片的url地址和UIImageView对象设置依赖关系, 代码如下:
objc_setAssociatedObject(self, &imageURLKey, url, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
接下来紧跟着两个判断语句:
③. 第一个是判断如果只有placeHolder, 那么就显示它.
④. 第二判断语句内部是真正的下载.下面来详细分析:
如果图片url不为空, 进入处理下载图片的逻辑:
1> 首先判断是否需要显示图片加载Indicator, 代码如下:
if ([self showActivityIndicatorView]) {
[self addActivityIndicator];
}
这里也用到了关联对象的方法.
第一步:
使用key: TAG_ACTIVITY_SHOW 保存 传入的布尔值(转化为NSNumber类型保存), 调用共有方法:
- (void)setShowActivityIndicatorView:(BOOL)show{
objc_setAssociatedObject(self, &TAG_ACTIVITY_SHOW, [NSNumber numberWithBool:show], OBJC_ASSOCIATION_RETAIN);
}
第二步:
通过key: TAG_ACTIVITY_SHOW 获取传入的是否显示图片加载的布尔值.
- (BOOL)showActivityIndicatorView{
return [objc_getAssociatedObject(self, &TAG_ACTIVITY_SHOW) boolValue];
}
2> 通过单例SDWebImageManager,创建一个下载操作operation,这个操作operation不是真正的 NSOperation 类,而是一个服从代理 SDWebImageOperation 的继承 NSObject的类 SDWebImageCombinedOperation,这个类的作用是用来组合查找存储图片操作 cacheOperation 和 下载图片操作 subOperation(属于类SDWebImageDownloaderOperation) ,当需要取消这两个操作时,如上分析,只需要调用 operation (类SDWebImageCombinedOperation) 的 -cancel 方法。
通过 id<SDWebImageOperation> operation 接收 SDWebImageManager 的下载操作,为了能够确保单个 UIImageView 同一时刻只有单个对应的图片被下载,需要保存这个 operation。这个时候你会想到什么类,没错,真是苹果提供的字典类。
把operation放进字典, 对应的key 为“UIImageViewImageLoad”。在下载图片前要在字典里通过key查找是否有正在进行的下载操作,如果有就取消。
图片下载就在这个operation的block里执行,并通过它向外输出下载图片最终的信息。在这里要插入一个runtime的关联对象的概念,在把 operation对象 放字典里这个过程中, 作者使用了runtime的关联对象的方法, 代码如下:
- (NSMutableDictionary *)operationDictionary {
NSMutableDictionary *operations = objc_getAssociatedObject(self, &loadOperationKey);
if (operations) {
return operations;
}
operations = [NSMutableDictionary dictionary];
objc_setAssociatedObject(self, &loadOperationKey, operations, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
return operations;
}
SDWebImageManager的下载操作
- 如下方法
- (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url
options:(SDWebImageOptions)options
progress:(SDWebImageDownloaderProgressBlock)progressBlock
completed:(SDWebImageCompletionWithFinishedBlock)completedBlock
内部逻辑如下
- 判断URL格式如果是字符串类型,转换成URL类型
if ([url isKindOfClass:NSString.class]) {
url = [NSURL URLWithString:(NSString *)url];
}
- 防止传入NSNull导致程序崩溃
if (![url isKindOfClass:NSURL.class]) {
url = nil;
}
- 确保虚假的URL不会被反复下载
BOOL isFailedUrl = NO;
@synchronized (self.failedURLs) {
isFailedUrl = [self.failedURLs containsObject:url];
}
- 这几种情况下执行是下载错误, 通过block携带错误信息并返回, 不执行之后的下载动作
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;
}
- 把刚刚创建的这个操作放在“正在运行的操作数组”里
@synchronized (self.runningOperations) {
[self.runningOperations addObject:operation];
}
- 根据是否有过滤条件返回对应的key
NSString *key = [self cacheKeyForURL:url];
- (NSString *)cacheKeyForURL:(NSURL *)url {
if (self.cacheKeyFilter) {
return self.cacheKeyFilter(url);
}
else {
return [url absoluteString];
}
}
- 从缓存或磁盘中查找图片
- (NSOperation *)queryDiskCacheForKey:(NSString *)key done:(SDWebImageQueryCompletedBlock)doneBlock {
if (!doneBlock) {
return nil;
}
if (!key) {
// 如果key为nil, 则返回传空递信息回去
doneBlock(nil, SDImageCacheTypeNone);
return nil;
}
// First check the in-memory cache...
// 1. 首先查找缓存中是否有图片, 如果有则返回这个图片和图片存储类型
UIImage *image = [self imageFromMemoryCacheForKey:key];
if (image) {
doneBlock(image, SDImageCacheTypeMemory);
return nil;
}
NSOperation *operation = [NSOperation new];
dispatch_async(self.ioQueue, ^{
if (operation.isCancelled) {
return;
}
@autoreleasepool {
// 2. 从磁盘中查找图片, 并且判断是否需要保存, 然后返回
UIImage *diskImage = [self diskImageForKey:key];
if (diskImage && self.shouldCacheImagesInMemory) {
NSUInteger cost = SDCacheCostForImage(diskImage);
[self.memCache setObject:diskImage forKey:key cost:cost];
}
dispatch_async(dispatch_get_main_queue(), ^{
doneBlock(diskImage, SDImageCacheTypeDisk);
});
}
});
return operation;
}
上面代码使用@autoreleasepool {},目的是在doneBlock执行完毕后,立即销毁临时变量 diskImage,优化内存管理。
执行上述代码的 doneBlock ,
- 如果操作被取消了,移除操作,并返回
if (operation.isCancelled) {
@synchronized (self.runningOperations) {
[self.runningOperations removeObject:operation];
}
return;
}
- 如果从缓存或者文件中查找到图片并返回,执行如下逻辑
else if (image) {
dispatch_main_sync_safe(^{
__strong __typeof(weakOperation) strongOperation = weakOperation;
if (strongOperation && !strongOperation.isCancelled) {
completedBlock(image, nil, cacheType, YES, url);
}
});
@synchronized (self.runningOperations) {
[self.runningOperations removeObject:operation];
}
}
- 如果未查找到图片且进入不了下载操作,执行如下逻辑
else {
// Image not in cache and download disallowed by delegate
dispatch_main_sync_safe(^{
__strong __typeof(weakOperation) strongOperation = weakOperation;
if (strongOperation && !weakOperation.isCancelled) {
completedBlock(nil, nil, SDImageCacheTypeNone, YES, url);
}
});
@synchronized (self.runningOperations) {
[self.runningOperations removeObject:operation];
}
}
接下来分析下载逻辑
- 下载前的 判断语句
if ((!image || options & SDWebImageRefreshCached)
&&
(![self.delegate respondsToSelector:@selector(imageManager:shouldDownloadImageForURL:)]
|| [self.delegate imageManager:self shouldDownloadImageForURL:url]))
上述判断语句,一个和查找图片有关,一个和是否允许下载的代理有关,以下几种情况会进入下载逻辑:
num | 可下载 |
---|---|
1 | 无代理方法,且未找到图片 |
2 | 无代理方法,找到图片,但需更新存储 |
3 | 有代理方法,允许下载图片,且未找到图片 |
4 | 有代理方法,找到图片,但需更新存储 |
- 找到图片,到需要更新存储,先走如下逻辑,然后再下载更新图片
if (image && options & SDWebImageRefreshCached) {
dispatch_main_sync_safe(^{
// If image was found in the cache but SDWebImageRefreshCached is provided, notify about the cached image
// AND try to re-download it in order to let a chance to NSURLCache to refresh it from server.
completedBlock(image, nil, cacheType, YES, url);
});
}
options & SDWebImageRefreshCached 相当于 options
== SDWebImageRefreshCached
downloadImagewithURL:options:progress:completed:方法里面来处理的, 该方法调用了addProgressCallback:andCompletedBlock:forURL:createCallback方法来将请求的信息存入管理器中,同时在回调block中创建新的操作, 配置之后将其放入downloadQueue操作队列中, 最后方法返回新创作的操作. 其具体实现如下:
- (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url options:(SDWebImageDownloaderOptions)options
progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageDownloaderCompletedBlock)completedBlock {
__block SDWebImageDownloaderOperation *operation;
__weak __typeof(self)wself = self;
[self addProgressCallback:progressBlock completedBlock:completedBlock forURL:url createCallback:^{
NSTimeInterval timeoutInterval = wself.downloadTimeout;
if (timeoutInterval == 0.0) {
timeoutInterval = 15.0;
}
// 1. 创建请求对象, 并根据options参数设置其属性
// 为了避免潜在的重复缓存(NSURLCache + SDImageCache), 如果没有明确告知需要缓存, 则禁用图片请求的缓存操作
NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url
cachePolicy:(options & SDWebImageDownloaderUseNSURLCache ? NSURLRequestUseProtocolCachePolicy : NSURLRequestReloadIgnoringLocalCacheData) timeoutInterval:timeoutInterval];
request.HTTPShouldHandleCookies = (options & SDWebImageDownloaderHandleCookies);
request.HTTPShouldUsePipelining = YES;
if (wself.headersFilter) {
request.allHTTPHeaderFields = wself.headersFilter(url, [wself.HTTPHeaders copy]);
}
else {
request.allHTTPHeaderFields = wself.HTTPHeaders;
}
// 2. 创建SDWebImageDownloaderOperation操作对象,并进行配置
// 配置信息包括是否需要认证,优先级
operation = [[wself.operationClass alloc] initWithRequest:request
options:options
progress:^(NSInteger receivedSize, NSInteger expectedSize) {
// 3. 从管理器的callbacksForURL中找出该URL所有的进度处理回调并调用
SDWebImageDownloader *sself = wself;
if (!sself) return;
__block NSArray *callbacksForURL;
dispatch_sync(sself.barrierQueue, ^{
callbacksForURL = [sself.URLCallbacks[url] copy];
});
for (NSDictionary *callbacks in callbacksForURL) {
dispatch_async(dispatch_get_main_queue(), ^{
SDWebImageDownloaderProgressBlock callback = callbacks[kProgressCallbackKey];
if (callback) callback(receivedSize, expectedSize);
});
}
}
completed:^(UIImage *image, NSData *data, NSError *error, BOOL finished) {
// 4. 从管理器的callbacksForURL中找出该URL所有的完成处理回调并调用
// 如果finished为YES, 则将该url对应的回调信息从URLCallbacks中删除
SDWebImageDownloader *sself = wself;
if (!sself) return;
__block NSArray *callbacksForURL;
dispatch_barrier_sync(sself.barrierQueue, ^{
callbacksForURL = [sself.URLCallbacks[url] copy];
if (finished) {
[sself.URLCallbacks removeObjectForKey:url];
}
});
for (NSDictionary *callbacks in callbacksForURL) {
SDWebImageDownloaderCompletedBlock callback = callbacks[kCompletedCallbackKey];
if (callback) callback(image, data, error, finished);
}
}
cancelled:^{
// 5. 取消操作讲该url对应的回调信息从URLCallBacks中删除
SDWebImageDownloader *sself = wself;
if (!sself) return;
dispatch_barrier_async(sself.barrierQueue, ^{
[sself.URLCallbacks removeObjectForKey:url];
});
}];
operation.shouldDecompressImages = wself.shouldDecompressImages;
if (wself.urlCredential) {
operation.credential = wself.urlCredential;
} else if (wself.username && wself.password) {
operation.credential = [NSURLCredential credentialWithUser:wself.username password:wself.password persistence:NSURLCredentialPersistenceForSession];
}
if (options & SDWebImageDownloaderHighPriority) {
operation.queuePriority = NSOperationQueuePriorityHigh;
} else if (options & SDWebImageDownloaderLowPriority) {
operation.queuePriority = NSOperationQueuePriorityLow;
}
// 6. 将操作加入到操作队列downloadQueue中
// 如果是LIFO顺序, 则将新的操作作为原队列中最后一个操作的依赖,然后将新操作设置为最后一个操作
[wself.downloadQueue addOperation:operation];
if (wself.executionOrder == SDWebImageDownloaderLIFOExecutionOrder) {
// Emulate LIFO execution order by systematically adding new operations as last operation's dependency
[wself.lastAddedOperation addDependency:operation];
wself.lastAddedOperation = operation;
}
}];
return operation;
}
在上面的代码中创建完请求对象, 并且配置好请求信息之后, 真正的下载操作是在SDWebImageDownLoadeerOperation类中完成的, 本类的初始化方法传入了三个block, progress, completed, 和cancelled, 分别用来处理进度 下载完成以及取消操作.
本类使用了foundation框架提供的NURLConnection类而不是7.0之后推出的NSURLSession类来执行请求的. 本类是NSOperation的子类, 执行请求是通过触发本类的start方法.
最近通过阅读这个框架的源码, 感受最深的是设计之巧妙, 我用伪代码用自己的方式试着实现一遍图片下载, 然后再和这个框架的设计方法进行比较, 发现真是有天壤之别.