一, 前言
决定动笔之前想了好久, 到底应该按照什么顺序分析才能更有条理性, 最终决定采用这样的方式: 先从一个简单的案例入手, 一步步走完整个流程, 然后针对过程中遇到的主要功能点逐个讨论, 最终达到对整个框架有一个相对完整的印象.
本篇的目标就是通过案例梳理流程, 作为后边章节的主线.
二, 目录结构
为了查看 SDwebImage 的完整目录结构, 首先需要导入 https://github.com/SDWebImage/SDWebImage , 由 上一篇 我们了解到, 新版 SDWebImage 总共分了 4 个子 pod, 默认只导入了 Core, 为了将它们全部导入 demo 中, Podfile 文件需要这么来写:
- source 'https://github.com/CocoaPods/Specs.git'
- platform :iOS, '9.0'
- target 'HHSDWebImageStudy' do
- pod 'SDWebImage', '4.4.2' // 为了讨论方便, 这里选定了一个比较新的版本
- pod 'SDWebImage/WebP'
- pod 'SDWebImage/GIF'
- pod 'SDWebImage/MapKit'
- end
执行 pod install 之后得到的目录结构如下图所示, 点此查看完整目录结构 .
SDWebImage - 目录结构. PNG
三, 示例: 用 UIImageView 展示一张静态网络图片
下面是使用时的代码, 加载图片时调用的是 UIImageView+WebCache 中的方法 sd_setImageWithURL:.
- // 1. 创建 UIImageView
- UIImageView *imgV = [[UIImageView alloc] initWithFrame:CGRectMake(125, 70, 160, 160)];
- [self.view addSubview:imgV];
- // 2. 加载网络图片
- [imgV sd_setImageWithURL:[NSURL URLWithString:@"https://img.zcool.cn/community/01c81558a2723ca801219c77a1e34e.jpg"]];
查看 sd_setImageWithURL: 的实现, 内部调用了一个参数很全的方法 sd_setImageWithURL: placeholderImage: options: progress: completed:, 只不过其他参数已经给了默认值.
- - (void)sd_setImageWithURL:(nullable NSURL *)url {
- [self sd_setImageWithURL:url placeholderImage:nil options:0 progress:nil completed:nil];
- }
其实, 还有很多类似方法, 都是对这个方法不同程度的封装, 即给一些参数提供了默认值, 或者增加了一些额外操作, 比如下边这几个方法:
- - (void)sd_setImageWithURL:(nullable NSURL *)url placeholderImage:(nullable UIImage *)placeholder {
- [self sd_setImageWithURL:url placeholderImage:placeholder options:0 progress:nil completed:nil];
- }
- - (void)sd_setImageWithURL:(nullable NSURL *)url placeholderImage:(nullable UIImage *)placeholder options:(SDWebImageOptions)options {
- [self sd_setImageWithURL:url placeholderImage:placeholder options:options progress:nil completed:nil];
- }
- - (void)sd_setImageWithURL:(nullable NSURL *)url completed:(nullable SDExternalCompletionBlock)completedBlock {
- [self sd_setImageWithURL:url placeholderImage:nil options:0 progress:nil completed:completedBlock];
- }
- - (void)sd_setImageWithURL:(nullable NSURL *)url placeholderImage:(nullable UIImage *)placeholder completed:(nullable SDExternalCompletionBlock)completedBlock {
- [self sd_setImageWithURL:url placeholderImage:placeholder options:0 progress:nil completed:completedBlock];
- }
- - (void)sd_setImageWithURL:(nullable NSURL *)url placeholderImage:(nullable UIImage *)placeholder options:(SDWebImageOptions)options completed:(nullable SDExternalCompletionBlock)completedBlock {
- [self sd_setImageWithURL:url placeholderImage:placeholder options:options progress:nil completed:completedBlock];
- }
- - (void)sd_setImageWithPreviousCachedImageWithURL:(nullable NSURL *)url
- placeholderImage:(nullable UIImage *)placeholder
- options:(SDWebImageOptions)options
- progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
- completed:(nullable SDExternalCompletionBlock)completedBlock {
- // 1. 取出本地缓存
- NSString *key = [[SDWebImageManager sharedManager] cacheKeyForURL:url];
- UIImage *lastPreviousCachedImage = [[SDImageCache sharedImageCache] imageFromCacheForKey:key];
- // 2. 调用本类中的 参数很多的方法, 并将缓存数据 (没有时用 placeholder) 传给 placeholder
- [self sd_setImageWithURL:url
- placeholderImage:lastPreviousCachedImage ?: placeholder
- options:options
- progress:progressBlock
- completed:completedBlock];
- }
再来看看这个参数巨多的方法 sd_setImageWithURL: placeholderImage: options: progress: completed: 究竟是如何实现的:
- - (void)sd_setImageWithURL:(nullable NSURL *)url
- placeholderImage:(nullable UIImage *)placeholder
- options:(SDWebImageOptions)options
- progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
- completed:(nullable SDExternalCompletionBlock)completedBlock {
- // 跳转 UIView+WebCache 中的 sd_internalSetImageWithURL: 方法执行, 若 <= 3.x.x 时, sd_internalSetImageWithURL: 在本类内部
- [self sd_internalSetImageWithURL:url
- placeholderImage:placeholder
- options:options
- operationKey:nil
- setImageBlock:nil
- progress:progressBlock
- completed:completedBlock];
- }
还是继续调用别的方法, 不过这次是调用父类分类 (UIView+WebCache) 中的方法, 之所以要调用直接或间接父类的方法, 是为了让其他控件 (如 UIButton 等) 可以复用加载网络图片的方法, 他们间的关系如下:
UIView 和直接或间接子类间的关系. PNG
现在去父类的分类 UIView+WebCache 看看吧, 为了缩减篇幅, 以下代码做了适当精简.
- - (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 {
- return [self sd_internalSetImageWithURL:url
- placeholderImage:placeholder
- options:options
- operationKey:operationKey
- setImageBlock:setImageBlock
- progress:progressBlock
- completed:completedBlock
- context:nil];
- }
- //*** 核心方法
- - (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
- context:(nullable NSDictionary<NSString *, id> *)context {
- // 1. 取消当前 validOperationKey 对应的 loadOperation
- NSString *validOperationKey = operationKey ?: NSStringFromClass([self class]);
- [self sd_cancelImageLoadOperationWithKey:validOperationKey];
- // 创建 manager(两种: 1. 用户自定义 2. 此库自带的单例)
- SDWebImageManager *manager;
- if ([context valueForKey:SDWebImageExternalCustomManagerKey]) {
- manager = (SDWebImageManager *)[context valueForKey:SDWebImageExternalCustomManagerKey];
- } else {
- manager = [SDWebImageManager sharedManager];
- }
- // 2. 下载
- id <SDWebImageOperation> operation = [manager loadImageWithURL:url
- options:options
- progress:combinedProgressBlock
- completed:^(UIImage *image, NSData *data, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL)
- {
- __strong __typeof (wself) sself = wself;
- // ...
- SDWebImageNoParamsBlock callCompletedBlockClojure = ^{
- if (!sself) { return; }
- if (!shouldNotSetImage) {
- [sself sd_setNeedsLayout];
- }
- if (completedBlock && shouldCallCompletedBlock) {
- completedBlock(image, error, cacheType, url);
- }
- };
- // ...
- dispatch_main_async_safe(^{
- #if SD_UIKIT || SD_MAC
- [sself sd_setImage:targetImage imageData:targetData basedOnClassOrViaCustomSetImageBlock:setImageBlock transition:transition cacheType:cacheType imageURL:imageURL];
- #else
- [sself sd_setImage:targetImage imageData:targetData basedOnClassOrViaCustomSetImageBlock:setImageBlock];
- #endif
- callCompletedBlockClojure();
- });
- }];
- // 3. 为 UIImageView 绑定新的 operation, 即上边这个 operation
- [self sd_setImageLoadOperation:operation forKey:validOperationKey];
- } else { // 如果图片 URL 不存在, 执行 completedBlock, 返回错误提示
- dispatch_main_async_safe(^{
- if (completedBlock) {
- NSError *error = [NSError errorWithDomain:SDWebImageErrorDomain code:-1 userInfo:@{NSLocalizedDescriptionKey : @"Trying to load a nil url"}];
- completedBlock(nil, error, SDImageCacheTypeNone, url);
- }
- });
- }
- }
可以看到, 父类最终调用了 - (void)sd_internalSetImageWithURL: ... 这个核心方法, 虽然实现代码较多, 其实主要就做了这么 3 件事:
1 取消 validOperationKey 对应的已经存在的 loadOperation. 因为传进来的参数 validOperationKey 是 nil, 所以 validOperationKey 实际取的是当前类名对应的字符串 NSStringFromClass([self class]).
2 执行 SDWebImageManager 的方法 - (id <SDWebImageOperation>)loadImageWithURL: ... 下载图片, 并返回一个 operation 对象, 其实是这个类 SDWebImageCombinedOperation 的实例, 与上一步 cancel 的对象是同一类型, 下面就会用到.
3 保存 validOperationKey 与刚刚生成的 operation 之间的映射关系, 以备取消时使用 (如 1).
对于 2 用到的方法, 其实现代码太长, 此处代码也做了精简:
- // SDWebImageManager
- - (id <SDWebImageOperation>)loadImageWithURL:(nullable NSURL *)url
- options:(SDWebImageOptions)options
- progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
- completed:(nullable SDInternalCompletionBlock)completedBlock
- {
- // ...
- // 1. 创建 operation
- SDWebImageCombinedOperation *operation = [SDWebImageCombinedOperation new];
- operation.manager = self; // 肯定是 weak 属性
- __weak typeof(strongOperation) weakSubOperation = strongOperation;
- // 2. 使用 imageCache 的方法查询缓存
- operation.cacheOperation = [self.imageCache queryCacheOperationForKey:key
- options:cacheOptions
- done:^(UIImage *cachedImage, NSData *cachedData, SDImageCacheType cacheType)
- {
- // 以下都是查询结束后 (查到 / 没查到) 的操作
- if (shouldDownload) {
- // 3. 需要下载
- __weak typeof(strongOperation) weakSubOperation = strongOperation;
- strongOperation.downloadToken = [self.imageDownloader downloadImageWithURL:url
- options:downloaderOptions
- progress:progressBlock
- completed:^(UIImage *downloadedImage, NSData *downloadedData, NSError *error, BOOL finished)
- {
- // 缩放图片 (如果可以的话)
- // 缓存图片
- // 执行完成的回调
- }
- } else if (cachedImage) {
- // 4. 如果取到了缓存
- // 执行完成的回调
- [self callCompletionBlockForOperation:strongOperation completion:completedBlock image:cachedImage data:cachedData error:nil cacheType:cacheType finished:YES url:url];
- // 从正在运行的 operation 数组中移除当前 operation
- [self safelyRemoveOperationFromRunning:strongOperation];
- } else {
- // 5. 没取到缓存 && 不允许下载
- // 执行完成的回调
- // Image not in cache and download disallowed by delegate
- [self callCompletionBlockForOperation:strongOperation completion:completedBlock image:nil data:nil error:nil cacheType:SDImageCacheTypeNone finished:YES url:url];
- // 从正在运行的 operation 数组中移除当前 operation
- [self safelyRemoveOperationFromRunning:strongOperation];
- }
- }
- return operation;
- }
该方法主要是在创建 SDWebImageCombinedOperation 对象, 大概的操作见上边的代码注释. 只对其中 2 个自己认为更重要点的方法做一个简要说明, 详细的讨论可以查看后边的相关篇章.
SDImageCache 中查询缓存的方法
- (nullable NSOperation *)queryCacheOperationForKey: ...
. 这个方法的作用是查询二级缓存, 也就依次从内存, 磁盘两处缓存查询我们需要的图片数据, 前者查不到时, 才在后者中查找.
- // SDImageCache
- - (nullable NSOperation *)queryCacheOperationForKey:(nullable NSString *)key
- options:(SDImageCacheOptions)options
- done:(nullable SDCacheQueryCompletedBlock)doneBlock;
- SDWebImageDownloader
中下载图片的方法
- (nullable SDWebImageDownloadToken *)downloadImageWithURL: ...
. 此方法的实现通过 operation 和 operationQueue 配合使用来执行下载操作的, 即 先分别创建 operation 和 operationQueue, 然后将 operation 添加到 operationQueue 中, 就自动启动任务了.
- // SDWebImageDownloader
- - (nullable SDWebImageDownloadToken *)downloadImageWithURL:(nullable NSURL *)url
- options:(SDWebImageDownloaderOptions)options
- progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
- completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock;
无论是查询缓存还是请求网络数据, 最终都会给 imageView 赋值, 也就是我们通常希望达到的效果.
四, 小结
至此, 我们大概理了一下 使用 SDWebImage 加载网络图片的主要流程, 最后借用 SDWebImage 作者提供的一张时序图做个简单总结吧.
SDWebImageSequenceDiagram.PNG
源码注释及 demo https://github.com/riversea2015/CodeForBlogs/tree/master/HHSDWebImageStudy
来源: http://www.jianshu.com/p/99c9564043d2