源码分析--SDWebImage
这个开源框架的主要作用就是:一个异步下载图片并且支持缓存的 `UIImageView'分类
框架中最常用的方法:
[self.imageView sd_setImageWithURL:[NSURL URLWithString:@"url"] placeholderImage:[UIImage imageNamed:@"placeholder.png"]];
框架中还有'UIButton'的分类,可以给'UIButton'异步加载图片,不过没有'UIImageView'分类中的这个方法常用。
接下来我们就以 `UIImageView+WebCache` 中的
- (void)sd_setImageWithURL:(NSURL *)url
placeholderImage:(UIImage *)placeholder;
这个方法为入口研究一下'SDWebImage'是如何工作的。
这个方法的实现:
- (void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder { [self sd_setImageWithURL:url placeholderImage:placeholder options:0 progress:nil completed:nil]; }
方法的唯一作用就是调用了另一个方法:
[self sd_setImageWithURL:placeholderImage:options:progress:completed:]
这个方法也是`UIImageView+WebCache` 中的核心方法了。
操作的管理
这是这个方法的第一行代码:
[self sd_cancelCurrentImageLoad];
框架中的所有操作实际上都是通过一个 `operationDictionary` 来管理, 而这个字典实际上是动态的添加到 `UIView` 上的一个属性, 至于为什么添加到 `UIView` 上, 主要是因为这个 `operationDictionary` 需要在 `UIButton` 和 `UIImageView` 上重用, 所以需要添加到它们的根类上.
这行代码是要保证没有当前正在进行的异步下载操作, 不会与即将进行的操作发生冲突, 它会调用:
[self sd_cancelImageLoadOperationWithKey:@"UIImageViewImageLoad"]
而这个方法会使当前 `UIImageView` 中的所有操作都被 `cancel`. 不会影响之后进行的下载操作.
占位图的实现
if (!(options & SDWebImageDelayPlaceholder)) { self.image = placeholder; }
如果传入的 `options` 中没有 `SDWebImageDelayPlaceholder`(默认情况下 `options == 0`), 那么就会为 `UIImageView` 添加一个临时的 `image`, 也就是占位图.
获取图片
检测传入的 `url` 是否非空, 如果非空那么一个全局的 `SDWebImageManager` 就会调用以下的方法获取图片:
[SDWebImageManager.sharedManager downloadImageWithURL:options:progress:completed:]
下载完成后会调用 `(SDWebImageCompletionWithFinishedBlock)completedBlock` 为 `UIImageView.image` 赋值, 添加上最终所需要的图片.
dispatch_main_sync_safe(^{ if (!wself) return; if (image) { wself.image = image; [wself setNeedsLayout]; } else { if ((options & SDWebImageDelayPlaceholder)) { wself.image = placeholder; [wself setNeedsLayout]; } } if (completedBlock && finished) { completedBlock(image, error, cacheType, url); } });
而最后, 在 `[SDWebImageManager.sharedManager downloadImageWithURL:options:progress:completed:]` 返回 `operation` 的**同时**, 也会向 `operationDictionary` 中添加一个键值对, 来表示操作的正在进行:
[self sd_setImageLoadOperation:operation forKey:@"UIImageViewImageLoad"];
它将 `opertion` 存储到 `operationDictionary` 中方便以后的 `cancel`.到此为止我们已经对 `SDWebImage` 框架中的这一方法分析完了, 接下来我们将要分析 `SDWebImageManager` 中的方法
[SDWebImageManager.sharedManager downloadImageWithURL:options:progress:completed:]
SDWebImageManager
这个类就是隐藏在 `UIImageView+WebCache` 背后, 用于处理异步下载和图片缓存的类, 当然你也可以直接使用 `SDWebImageManager` 的上述方法`downloadImageWithURL:options:progress:completed:` 来直接下载图片.可以看到, 这个类的主要作用就是为 `UIImageView+WebCache` 和 `SDWebImageDownloader, SDImageCache` 之间构建一个桥梁, 使它们能够更好的协同工作, 我们在这里分析这个核心方法的源代码, 它是如何协调异步下载和图片缓存的.
if ([url isKindOfClass:NSString.class]) { url = [NSURL URLWithString:(NSString *)url]; } if (![url isKindOfClass:NSURL.class]) { url = nil; }
这块代码的功能是确定 `url` 是否被正确传入, 如果传入参数的是 `NSString` 类型就会被转换为 `NSURL`. 如果转换失败, 那么 `url` 会被赋值为空, 这个下载的操作就会出错.
既然我们获取了 `url`, 再通过 `url` 获取对应的 `key`
NSString *key = [self cacheKeyForURL:url];
下一步是使用 `key` 在缓存中查找以前是否下载过相同的图片.
operation.cacheOperation = [self.imageCache queryDiskCacheForKey:key done:^(UIImage *image, SDImageCacheType cacheType) { ... }];
这里调用 `SDImageCache` 的实例方法 `queryDiskCacheForKey:done:` 来尝试在缓存中获取图片的数据. 而这个方法返回的就是货真价实的 `NSOperation`.
如果我们在缓存中查找到了对应的图片, 那么我们直接调用 `completedBlock` 回调块结束这一次的图片下载操作.
dispatch_main_sync_safe(^{ completedBlock(image, nil, cacheType, YES, url);
});
如果我们没有找到图片, 那么就会调用 `SDWebImageDownloader` 的实例方法:
id <SDWebImageOperation> subOperation = [self.imageDownloader downloadImageWithURL:url options:downloaderOptions progress:progressBlock completed:^(UIImage *downloadedImage, NSData *data, NSError *error, BOOL finished) { ... }];
如果这个方法返回了正确的 `downloadedImage`, 那么我们就会在全局的缓存中存储这个图片的数据:
[self.imageCache storeImage:downloadedImage recalculateFromImage:NO imageData:data forKey:key toDisk:cacheOnDisk];
并调用 `completedBlock` 对 `UIImageView` 或者 `UIButton` 添加图片, 或者进行其它的操作.
最后, 我们将这个 `subOperation` 的 `cancel` 操作添加到 `operation.cancelBlock` 中. 方便操作的取消.
operation.cancelBlock = ^{ [subOperation cancel]; }
SDWebImageCache
它维护了一个内存缓存和一个可选的磁盘缓存, 我们先来看一下在上一阶段中没有解读的两个方法, 首先是:
- (NSOperation *)queryDiskCacheForKey:(NSString *)key done:(SDWebImageQueryCompletedBlock)doneBlock;
这个方法的主要功能是异步的查询图片缓存. 因为图片的缓存可能在两个地方, 而该方法首先会在内存中查找是否有图片的缓存.
UIImage *image = [self imageFromMemoryCacheForKey:key];
这个 `imageFromMemoryCacheForKey` 方法会在 `SDWebImageCache` 维护的缓存 `memCache` 中查找是否有对应的数据, 而 `memCache` 就是一个 `NSCache`.
如果在内存中并没有找到图片的缓存的话, 就需要在磁盘中寻找了, 这个就比较麻烦了..
在这里会调用一个方法 `diskImageForKey` 这个方法的具体实现我在这里就不介绍了, 涉及到很多底层 `Core Foundation` 框架的知识, 不过这里文件名字的存储使用 `MD5` 处理过后的文件名.
CC_MD5(str, (CC_LONG)strlen(str), r);
对于其它的实现细节也就不多说了...
如果在磁盘中查找到对应的图片, 我们会将它复制到内存中, 以便下次的使用.
UIImage *diskImage = [self diskImageForKey:key]; if (diskImage) { CGFloat cost = diskImage.size.height * diskImage.size.width * diskImage.scale; [self.memCache setObject:diskImage forKey:key cost:cost]; }
这些就是 `SDImageCache` 的核心内容了, 而接下来将介绍如果缓存没有命中, 图片是如何被下载的.
SDWebImageDownloader
这个类的核心功能就是下载图片, 而核心方法就是上面提到的:
- (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url
options:(SDWebImageDownloaderOptions)options
progress:(SDWebImageDownloaderProgressBlock)progressBlock
completed:(SDWebImageDownloaderCompletedBlock)completedBlock;
这个方法直接调用了另一个关键的方法:
- (void)addProgressCallback:(SDWebImageDownloaderProgressBlock)progressBlock andCompletedBlock:(SDWebImageDownloaderCompletedBlock)completedBlock forURL:(NSURL *)url createCallback:(SDWebImageNoParamsBlock)createCallback
它为这个下载的操作添加回调的块, 在下载进行时, 或者在下载结束时执行一些操作, 先来阅读一下这个方法的源代码:
BOOL first = NO; if (!self.URLCallbacks[url]) { self.URLCallbacks[url] = [NSMutableArray new]; first = YES; } // Handle single download of simultaneous download request for the same URL NSMutableArray *callbacksForURL = self.URLCallbacks[url]; NSMutableDictionary *callbacks = [NSMutableDictionary new]; if (progressBlock) callbacks[kProgressCallbackKey] = [progressBlock copy]; if (completedBlock) callbacks[kCompletedCallbackKey] = [completedBlock copy]; [callbacksForURL addObject:callbacks]; self.URLCallbacks[url] = callbacksForURL; if (first) { createCallback(); }
方法会先查看这个 `url` 是否有对应的 `callback`, 使用的是 `downloader` 持有的一个字典 `URLCallbacks`.
如果是第一次添加回调的话, 就会执行 `first = YES`, 这个赋值非常的关键, 因为 `first` 不为 `YES` 那么 HTTP 请求就不会被初始化, 图片也无法被获取.
然后, 在这个方法中会重新修正在 `URLCallbacks` 中存储的回调块.
NSMutableArray *callbacksForURL = self.URLCallbacks[url]; NSMutableDictionary *callbacks = [NSMutableDictionary new]; if (progressBlock) callbacks[kProgressCallbackKey] = [progressBlock copy]; if (completedBlock) callbacks[kCompletedCallbackKey] = [completedBlock copy]; [callbacksForURL addObject:callbacks]; self.URLCallbacks[url] = callbacksForURL;
如果是第一次添加回调块, 那么就会直接运行这个 `createCallback` 这个 block, 而这个 block, 就是我们在前一个方法 `downloadImageWithURL:options:progress:completed:` 中传入的回调块.
[self addProgressCallback:progressBlock andCompletedBlock:completedBlock forURL:url createCallback:^{ ... }];
我们下面来分析这个传入的无参数的代码. 首先这段代码初始化了一个 `NSMutableURLRequest`:
NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url cachePolicy:... timeoutInterval:timeoutInterval];
这个 `request` 就用于在之后发送 `HTTP` 请求.
在初始化了这个 `request` 之后, 又初始化了一个 `SDWebImageDownloaderOperation` 的实例, 这个实例, 就是用于请求网络资源的操作. 它是一个 `NSOperation` 的子类,
operation = [[SDWebImageDownloaderOperation alloc] initWithRequest:request options:options progress:... completed:... cancelled:...}];
但是在初始化之后, 这个操作并不会开始(`NSOperation` 实例只有在调用 `start` 方法或者加入 `NSOperationQueue` 才会执行), 我们需要将这个操作加入到一个 `NSOperationQueue` 中.
[wself.downloadQueue addOperation:operation];
只有将它加入到这个下载队列中, 这个操作才会执行.
SDWebImageDownloaderOperation
这个类就是处理 HTTP 请求, URL 连接的类, 当这个类的实例被加入队列之后, `start` 方法就会被调用, 而 `start` 方法首先就会产生一个 `NSURLConnection`.
@synchronized (self) { if (self.isCancelled) { self.finished = YES; [self reset]; return; } self.executing = YES; self.connection = [[NSURLConnection alloc] initWithRequest:self.request delegate:self startImmediately:NO]; self.thread = [NSThread currentThread]; }
而接下来这个 `connection` 就会开始运行:
[self.connection start];
它会发出一个 `SDWebImageDownloadStartNotification` 通知
[[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStartNotification object:self];
代理
在 `start` 方法调用之后, 就是 `NSURLConnectionDataDelegate` 中代理方法的调用.
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response; - (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response; - (void)connectionDidFinishLoading:(NSURLConnection *)aConnection;
在这三个代理方法中的前两个会不停回调 `progressBlock` 来提示下载的进度.
而最后一个代理方法会在图片下载完成之后调用 `completionBlock` 来完成最后 `UIImageView.image` 的更新.
而这里调用的 `progressBlock` `completionBlock` `cancelBlock` 都是在之前存储在 `URLCallbacks` 字典中的.
这大概就是
[self.imageView sd_setImageWithURL:[NSURL URLWithString:@"url"] placeholderImage:[UIImage imageNamed:@"placeholder.png"]];
这个方法执行的全部过程了.
小结
`SDWebImage` 的图片加载过程:
* 查看缓存
* 缓存命中
* 返回图片
* 更新 `UIImageView`
* 缓存未命中
* 异步下载图片
* 加入缓存
* 更新 `UIImageView`