SDWebImage源码分析(一)

前言

SDWebImage 是什么,相信会 iOS 开发的人都知道。官方是这样定义的:

Asynchronous image downloader with cache support as a UIImageView category

从上面的句子里,我们可以知道这几个信息,SDWebImage 首先是以 UIImageView 的 category 存在的(当然现在其实不止 UIImageView),它是一个异步下载器,同时支持缓存。

所以呢,category异步下载缓存 这就是 SDWebImage 最重要的三个东西了。

说起来,SDWebImage 是我 iOS 开发过程中,第二用到的第三方类库,第一个当然是大名鼎鼎的 AFNetworking 了。

因为我对 SDWebImage 中使用到的 异步下载缓存 原理很感兴趣,所以我就去看了一下它的源码。所以我准备写几篇博客来记录一下对 SDWebImage 源码的分析。

当然,网上关于 SDWebImage 源码剖析的文章有一大把,但是,我还是想从我的理解出发,来分析一下 SDWebImage 的源码。

源码下载

SDWebImage 的源码放在 GitHub 上面,地址为 https://github.com/rs/SDWebImage

可以直接 download 下载源码,也可以使用 git 把它 clone 下来。

直接 download 会有一个问题,因为现在 SDWebImage 同时也依赖别的库,比如 FLAnimatedImage,直接 download 只会下载 SDWebImage 本身,并不会下载它依赖的其他库,这样子 SDWebImage 整个源码下载下来是无法编译通过的。所以最好使用 git 来 clone。

1
$ git clone --recursive git@github.com:rs/SDWebImage.git

--recursive表示同时 clone 它的子模块。

clone 下来之后,用 Xcode 打开里面的 SDWebImage.xcworkspace。这是一个 workspace,所以可以看到里面有很多东西。有 Demo,有 test 工程,还有一些文档文件。我们的主角 SDWebImage 就藏在 SDWebImage Demo 这个工程下面,而最核心的源代码就是下面红框框起来的那部分。

源码结构

在开始看源码之前,我们首先要先看一下官方给出的两张文档图,一张是 UML 图,另一张是执行流程图。
首先是 UML 图,流程图等之后再说。

分析一下可以看到,左上角是一些 category,比如 UIImageView。中间是 SDWebImageManager,左下角和 cache 相关,主要的类是 SDImageCache,右下角则是和 downloader 相关,主要的类是 SDWebImageDownloader。另外当然还有一些辅助类。
UML 的一些图示可以网上查,现在看这个图,先了解一下大概结构。等到介绍完全部源码,回过头来再看这张图,就会觉得豁然开朗。

我觉得要看源码,首先就要全局宏观上了解整个源代码的结构,这样才不至于看的云里雾里。代码的逻辑目录组织也很重要,组织的好,代码就很容易理解,否则就会显得很乱。现在回到之前的工程源码结构图上面,我们可以看到 SDWebImage 下面有 3 个文件,再加上 6 个文件夹。对其中那三个文件来说,SDWebImageCompat 是跟兼容性有关的,其中 SDWebImageCompat.h 里面定义了许多的宏,因为 SDWebImage 同时支持苹果四大平台,所以需要针对不同的平台,执行不同的代码。而 SDWebImageOperation.h 里面则定义了一个协议 SDWebImageOperation,它会是很多协议的父协议。另外几个文件夹,根据名字,就能区分开来作用,Downloader主要和下载有关,Cache则与缓存相关,Utils里面放了 Manager 类和其他一些工具类,Categories 是一些类的分类,主要用来完成一些功能,WebCache Categories类则是我们使用 SDWebImage 主要用到的一些类的 category,比如最常用的 UIImageView+WebCache,而最后的 FLAnimatedImage 则是 4.0 版本加入的对 GIF 的支持。

了解了工程目录结构之后,我们就可以看之前跳过的执行流程图了。

整个 SDWebImage 的运行流程大概是这个样子:

某个工程的某个类先调用 UIImage 的 category 的方法 sd_setImageWithURL() 方法来设置图片,这个方法,也是我们与 SDWebImage 交互的方法,之后的动作,我们就不必管了,SDWebImage 会帮我们处理。SDWebImage 会先调用 UIView 的 category 里面的方法 sd_internalSetImageWithURL() 方法。之后在这个方法里,它会调用 SDWebImageManager 的 loadImageWithURL() 来加载图片。SDWebImageManager 顾名思义就是管理类,它会统筹调度整个获取图片的策略,根据图示的策略,它会首先去缓存中找是否缓存中有我们需要的图片,即调用 SDImageCache 的 queryDiskCacheForKey() ,如果在缓存中找到了所要的图片,就直接返回。否则就调用 SDWebImageDownloader 的 downloadImage(url, options, progress, completed) 方法去网上下载。下载到图片后,返回该图片,并且调用 SDImageCache 的 storeImage() 方法存入缓存 ,这样下次就可以直接从缓存里面取,不用每次都下载。取到照片后,一级级返回上去,最后将图片设置好。

这就是整个流程,从上面的流程图中,我们可以清晰地将 SDWebImage 分为四个部分:一些 category,Mangager,Cache 和 Downloader。
所以我也准备写四篇博客也分析,今天是第一篇,所以接下来我先讲一下这些 category 相关的源码,也即工程目录中 WebCache Categories 目录下的源码。

WebCache Categories

在我的理解下,更加粗一点,我们可以将 SDWebImage 分为两个部分,“前端”和“后端”。所谓的“前端”就是这些 Categories,就是与外界代码交互的部分,而剩下的都是属于“后端”。在使用中,我们只要关心“前端”就好了,只要“前端”的 API 会调用就可以。但是要想真正理解 SDWebImage 的工作原理,则必须要接触“后端”代码,那才是核心部分。

最开始“前端”应该只有 UIImageView,后来增加了 UIButton,所以说前端是可以变得,但是“后端”则可以不用跟着改变。

下面我就从我们最熟悉最常用的 UIImageView+WebCache 着手来分析源码。

首先看 UIImageView+WebCache.h 文件,我们会发现很多 sd_setImageWithURL 方法,这就是我们使用的 API 接口方法。它们最后都是调用了下面这个方法,它们之间无非是参数有没有给出,是不是使用默认的区别。

1
2
3
4
5
6
7
8
9
10
11
12
13
- (void)sd_setImageWithURL:(nullable NSURL *)url
placeholderImage:(nullable UIImage *)placeholder
options:(SDWebImageOptions)options
progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
completed:(nullable SDExternalCompletionBlock)completedBlock {
[self sd_internalSetImageWithURL:url
placeholderImage:placeholder
options:options
operationKey:nil
setImageBlock:nil
progress:progressBlock
completed:completedBlock];
}

而在这个方法中,我们又看到它调用的是 UIView+WebCache 中定义的方法:

1
2
3
4
5
6
7
- (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;

因为 UIImageView 是 UIView 的子类,所以它可以直接调用该方法。而这个方法,就是我们这篇博客最重要的方法了,从流程图中也写了这个方法,可以想到它的重要性。下面我直接贴出这个方法的源码,并且会以在代码旁边加上注释的形式来分析这个方法。

- (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 {

    // 首先得到一个 operationKey,如果该值直接传进来,就直接使用,否则就以当前类名来作为 key
    NSString *validOperationKey = operationKey ?: NSStringFromClass([self class]);

    // 先移除当前 view 绑定的 operation,防止 cell 复用的时候出现错误
    // 当前 view 会动态绑定一个属性 operations ,它是一个字典格式的变量,里面存储了当前 view 正在执行的一个 operation
    // 通过传入当前 operationKey 来取消该 operationKey 对应的一些 operation
    [self sd_cancelImageLoadOperationWithKey:validOperationKey];

    // 动态添加图片的 url 作为当前 view 的属性
    objc_setAssociatedObject(self, &imageURLKey, url, OBJC_ASSOCIATION_RETAIN_NONATOMIC);

    // options 中如果不包含 SDWebImageDelayPlaceholder,则立即设置 placeholder占位图,否则等到后面再设置
    if (!(options & SDWebImageDelayPlaceholder)) {
        // 因为要修改 UI,所以主线程异步执行
        dispatch_main_async_safe(^{
            [self sd_setImage:placeholder imageData:nil basedOnClassOrViaCustomSetImageBlock:setImageBlock];
        });
    }

    // 只有传入了 url 才执行之后的操作
    if (url) {
        // check if activityView is enabled or not
        if ([self sd_showActivityIndicatorView]) {
            [self sd_addActivityIndicator];
        }

        // SDWebImageManager 类登场,开始加载图片(具体实现,下次再说),下面是 loadImageWithURL 方法的 completedBlock 里面的内容,获取到图片后执行
        __weak __typeof(self)wself = self;
        id <SDWebImageOperation> operation = [SDWebImageManager.sharedManager loadImageWithURL:url options:options progress:progressBlock completed:^(UIImage *image, NSData *data, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {

            // weak 了之后再 strong 的原因是因为怕执行过程中 self 被释放了而导致出错
            __strong __typeof (wself) sself = wself;

            [sself sd_removeActivityIndicator];
            if (!sself) {
                return;
            }

            // 设置获取到的 image
            dispatch_main_async_safe(^{
                if (!sself) {
                    return;
                }

                // options 包含 SDWebImageAvoidAutoSetImage 表示不自动设置 image,所以直接执行外层 completedBlock 后返回
                if (image && (options & SDWebImageAvoidAutoSetImage) && completedBlock) {
                    completedBlock(image, error, cacheType, url);
                    return;
                }
                // 否则就自动设置 image 了
                // sd_setImage:imageData:basedOnClassOrViaCustomSetImageBlock 方法也很有意思
                // 如果有 setImageBlock 参数存在,就回调这个 block,使用这个 block 来手动设置 image(针对不是 UIImageView 和 UIButton 的情况)
                // 否则就使用 UIImageView 和 UIButton 自带的方法来设置 image
                else if (image) {
                    [sself sd_setImage:image imageData:data basedOnClassOrViaCustomSetImageBlock:setImageBlock];
                    [sself sd_setNeedsLayout];
                }
                // image 没有获取到的情况下
                else {
                    // 如果当时是延迟设置 placeholder,此时就设置 placeholder
                    if ((options & SDWebImageDelayPlaceholder)) {
                        [sself sd_setImage:placeholder imageData:nil basedOnClassOrViaCustomSetImageBlock:setImageBlock];
                        [sself sd_setNeedsLayout];
                    }
                }
                // 执行外层 completedBlock
                if (completedBlock && finished) {
                    completedBlock(image, error, cacheType, url);
                }
            });
        }];
        // 给当前 view 绑定操作
        [self sd_setImageLoadOperation:operation forKey:validOperationKey];
    }
    // url 不存在的情况下,直接执行外层 completedBlock,返回错误
    else {
        dispatch_main_async_safe(^{
            [self sd_removeActivityIndicator];
            if (completedBlock) {
                NSError *error = [NSError errorWithDomain:SDWebImageErrorDomain code:-1 userInfo:@{NSLocalizedDescriptionKey : @"Trying to load a nil url"}];
                completedBlock(nil, error, SDImageCacheTypeNone, url);
            }
        });
    }
}

上面的注释应该写的蛮清楚了,对于“前端”这一层的代码逻辑,基本上就是这样子,其中 SDWebImageManager 类的 loadImageWithURL 没有展开。但是考虑到之前的执行流程图中有这个方法,我们知道这是一个十分重要的方法,这将是下一次要讲的内容,SDWebImageManager 到底是如何来执行那些策略的。

虽然我们是以 UIImageView+WebCache 为例, 但是 UIButton+WebCache 其实也是一样的,因为大家最后调用的都是同一个方法 sd_internalSetImageWithURL

其他还有一些方法,我就不一一说了,比如 GIF 相关的代码,我今天就讲了主要的那个方法,下几次也一样,我就主要讲那个流程图中提到了的方法,因为那些就是重点方法,当然其他重要的方法,以及整体过程也会介绍到。

后记

这次介绍了 SDWebImage 的整体结构,以及“前端”部分,也就是 UIImageView 的 category,了解了我们经常会调用的 API 具体到底干了什么。但是我们没有深入到下一层,即 SDWebImageManager 中,下一次我们就了解一下这个 manager 到底是如何调度的。