美文网首页公众号【麦小丁】征集优质文章源码解读程序员
SDWebImage源码学习笔记 ☞ 结构及基本流程

SDWebImage源码学习笔记 ☞ 结构及基本流程

作者: RiverSea | 来源:发表于2018-12-06 13:11 被阅读9次
SDWebImage 源码学习笔记.png

一、前言

决定动笔之前想了好久,到底应该按照什么顺序分析才能更有条理性,最终决定采用这样的方式:先从一个简单的案例入手,一步步走完整个流程,然后针对过程中遇到的主要功能点逐个讨论,最终达到对整个框架有一个相对完整的印象。

本篇的目标就是通过案例梳理流程,作为后边章节的主线。

二、目录结构

为了查看 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 件事:

① 取消 validOperationKey 对应的已经存在的 loadOperation。因为传进来的参数 validOperationKey 是 nil,所以 validOperationKey 实际取的是当前类名对应的字符串 NSStringFromClass([self class])。

② 执行 SDWebImageManager 的方法 - (id <SDWebImageOperation>)loadImageWithURL: ... 下载图片,并返回一个 operation 对象,其实是这个类 SDWebImageCombinedOperation 的实例,与上一步 cancel 的对象是同一类型,下面就会用到。

③ 保存 validOperationKey 与刚刚生成的 operation 之间的映射关系,以备取消时使用(如 ①)。

对于 ② 用到的方法,其实现代码太长,此处代码也做了精简:

// 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

HHSDWebImageStudy

相关文章

网友评论

    本文标题:SDWebImage源码学习笔记 ☞ 结构及基本流程

    本文链接:https://www.haomeiwen.com/subject/smmcxqtx.html