iOS SDWebImage 源码分析及架构设计探索

2字数 4477阅读 1775

SDWebImage 常用于对图像的下载及缓存等。作者Olivier Poitrey,法国视频分享网站Dailymotion(后被法国电信运营商Orange收购,被和谐) 的 CTO,拥有多个开源项目。目前SDWebImage获得巨大成功,用户甚众,并且在github上已获取超过17.7k star。

和笔者另一篇文章 **iOS 李明杰 MJRefresh源码解析 **类似,本文主要素材来源有正在学hybrid开发的iOS开发者 J_Knight 的文章、杨千嬅染了红头发 的博客一行行看SDWebImage源码(一)一行行看SDWebImage源码(二)github上作者的用法介绍,再一次表示敬意。

本文分成两个部分对SDWebImage进行介绍上部分从框架层次方面进行概述,主要是笔者在学习使用和研究过程中的心得感悟;下半部分则是对具体各个类实现代码的详细解析因此笔者建议,若是已经对SDWebImage有一定了解的同学可以看一下上部分,若是刚开始使用此框架的同学大可略过第一部分,直接从代码实现看起,以免被笔者可能不太成熟的见解所误导。水平有限,还望大家不吝赐教。

第一部分

架构设计(干货)

"扪心自问",假如让我们自己设计一个具有图片异步加载并缓存功能的框架,我们该怎么设计。冥想3分钟,好好看看这张类图,好好看看这张类图,好好看看这张类图,在这里就先根据源码试着反推整个框架的设计思路。


SDWebImage-类图
SDWebImage-UML序列图

1. 封装、继承、多态特性

  • 毫无疑问,封装是单一职责原则的题中之义SDWebImage根据不同功能进行了不同的类的封装:
职责
UIView+WebCache 各种视图的基类,容纳图像的容器,操作绑定的对象
SDImageCache、SDImageCacheConfig 负责处理图像缓存、配置缓存参数
SDWebImageDownloader 核心代码,负责图像的异步下载
**SDWebImageManager ** 绑定显示图像的视图、图像下载、图像缓存3者的管理类,是操作的中枢
SDWebImagePrefetcher 预加载
SDWebImageDecoder 图像解压缩
  • 面向接口编码,而非面向实现编码,继承中的依赖倒置原则,即基类中定义接口,子类中对基类接口做具体实现。例如 SDWebImageUIImageView、UIButton等视图控件提供图像或Gif加载功能,所以在共同基类UIView中提供图像异步下载等功能。

2. 设计模式

  • 装饰模式

装饰模式,动态地给一个对象添加一些额外的职责。就扩展功能来说,装饰模式相比生成子类更为灵活(四人帮 1994)。 而类目就是OC语言框架对装饰模式的经典应用, SDWebImage作者通过类目的方式灵活的给视图类添加了各种功能,同时也完成了下载、存储功能与具体视图的绑定,值得我辈学习。

  • 单例模式

** 单例模式:保证一个类仅有一个实例,并提供一个访问它的全局访问点(四人帮 1994)。** 老生常谈的设计模式,亦是经典的设计模式。SDWebImage框架中对其有大量应用。

单例类 方法 职责
SDImageCache sharedImageCache 负责处理图像缓存、配置缓存参数
SDWebImageDownloader sharedDownloader 核心代码,负责图像的异步下载
SDWebImageManager sharedManager 绑定视图、下载、缓存的管理类,操作的中枢
SDWebImagePrefetcher sharedImagePrefetcher 预加载
  • 适配器模式、代理模式

** 适配器模式:将一个类的接口换成客户希望的另一个接口。使得原本由于接口不兼容而不能一起工作的那些类可以一起工作(四人帮 1994)。** 若不明白直接说协议--委托就是属于对象适配器就明白了,也对那句“OC中的多继承通过协议实现的”有更深的理解吧。在此不再详叙,有兴趣的同学可以看看四人帮的《设计模式》一书。

协议类 职责
SDWebImageOperation 提供取消操作的兼容接口cancel
SDWebImageDownloaderOperationInterface 自定义下载操作时使用
SDWebImageManagerDelegate 提供管理类的回调
SDWebImagePrefetcherDelegate 提供预加载回调
  • 生成器模式

生成器模式:将一个复杂对象的构建与它的表现分离,使得同样的构建过程可以创建不同的表现。 这也是笔者要着重推荐的一种设计模式,灵活运用能很好的达到视图与功能分离的解耦目的,我们编码中经常可见的各种Manager,包括SDWebImage中的SDWebImageManager、SDWebImageDownloader等,根据不同参数对视图表现或功能进行比较复杂的配置、操作,使用此方法可以很好地进行代码分层、解耦,使逻辑清晰。

3. 为什么要进行图像解压缩

/* UIImage *image = [UIImage sd_imageWithData:data];
  image = [UIImage decodedImageWithImage:image];
*/
+ (nullable UIImage *)decodedImageWithImage:(nullable UIImage *)image;

SDWebImageDecoder代码中显示是从UIImage返回UIImage,自己返回自己,何苦来哉?

请注意,这里有2个步骤.

  1. NSData-->UIImage
  2. UIImage-->UIImage

解压已经下载缓存起来的图片可以提高性能,但是会消耗大量的内存。
具体原理有兴趣的同学可参考以下文章:
SDWebImageDecoder,异步对图像进行了一次解压⋯⋯
谈谈 iOS 中图片的解压缩

4. 图像加载及缓存逻辑

一般来讲缓存分为两种:

  • 永久存储,又叫磁盘存储,在iOS 中即存储在沙盒
  • 内存存储,即存储在进程分配的内存,程序关闭后及消失。经常的做法是声明一全局变量来赋值进去(iOS 常用NSDictionary/NSCache),SDWebImage用的是NSCache

NSDictionaryNSCache区别可看下篇文章:
构建缓存时选用NSCache而非NSDictionary

  • 图像缓存
  1. 验证此URL没有(是否)被标记为不可用

SDImageCache里查询没有(是否)存在缓存的图片

  • 查看内存的缓存,根据keyNSCache获取Value
  • 查看磁盘的缓存,若存在计算缓存代价若允许存入内存,抛入主线程

用户(是否)通过参数SDWebImageOptions要求必须网络刷新

3.用户(是否)通过委托设置允许对此URL进行网络下载

  • 图像下载
  1. 创建下载请求
  2. 创建下载操作
  3. url证书
  4. 优先级
  5. 在下载队列里添加下载操作,执行下载操作

5. 用户认证 NSURLCredential

当连接客户端与服务端进行数据传输的时候,web服务器收到客户端请求时可能需要先验证客户端是否是正常用户,再决定是否返回该接口的真实数据。

iOS7.0之前使用的网络框架是NSURLConnection,在 2013 的 WWDC 上,苹果推出了NSURLConnection的继任者:NSURLSession,SDWebImage使用的是NSURLConnection,这两种网络框架的认证调用的方法也是不一样的,有兴趣的可以去google一下这里只看下NSURLConnection的认证

认证过程:

1. web服务器接收到来自客户端的请求

2. web服务并不直接返回数据,而是要求客户端提供认证信息,也就是说挑战是服务端向客户端发起的

2.1 要求客户端提供用户名与密码挑战NSInternetPassword

2.2 要求客户端提供客户端证书 NSClientCertificate

2.3 要求客户端信任该服务器

3. 客户端回调执行,接收到需要提供认证信息,然后提供认证信息,并再次发送给web服务

4. web服务验证认证信息

4.1 认证成功,将最终的数据结果发送给客户端

4.2 认证失败,错误此次请求,返回错误码401

6. 下载操作的 任务调度和多线程安全问题

dispatch_barrier_sync是SD选用的GCD函数,self.barrierQueue是存放任务的队列,block里面是要执行的任务。
SD添加下载任务是同步的,而且都是在self.barrierQueue这个并行队列中,同步添加任务。这样也保证了根据executionOrder设置依赖关是正确的。
换句话说如果创建下载任务不是使用dispatch_barrier_sync完成的,而是使用异步方法 ,虽然依次添加创建下载操作A、B、C的任务,但实际创建顺序可能为A、C、B,这样当executionOrder的值是SDWebImageDownloaderLIFOExecutionOrder,设置的操作依赖关系就变成了A依赖C,C依赖B

typedef NS_ENUM(NSInteger, SDWebImageDownloaderExecutionOrder) {
//先进先出, 默认值,所有的下载操作以队列类型执行,先被加入下载队列的操作先执行
SDWebImageDownloaderFIFOExecutionOrder,
// 先进后出,所有的下载操作以栈类型执行,后被加入下载队列的操作先执行
SDWebImageDownloaderLIFOExecutionOrder
};

有兴趣的同学可以看看:
通过GCD中的dispatch_barrier_(a)sync加强对sync中所谓等待的理解

dispatch_barrier_sync VS dispatch_barrier_sync

** Dispatch Barrier解决多线程并发读写一个资源发生死锁 **

sync说明了这是个同步函数,任务不会立即返回,会等到任务执行结束才返回。

使用dispatch_barrier_sync函数创建的任务会首先查看队列是否有别的任务要执行,如果有则等待已有任务执行完毕再执行;同时在此方法后添加的任务必须等到此方法中的任务执行后才能执行,利用这个方法可以控制执行顺序。

Dispatch Barrier确保提交的block是指定队列中特定时段唯一在执行的一个。在所有先于Dispatch Barrier的任务都完成的情况下这个block才开始执行。轮到这个block时barrier会执行这个block并且确保队列在此过程 不会执行其他任务。block完成后才恢复队列。

_barrierQueue = dispatch_queue_create("com.hackemist.SDWebImageDownloaderBarrierQueue", DISPATCH_QUEUE_CONCURRENT);

这是用户自己创建的队列,DISPATCH_QUEUE_CONCURRENT代表的是它是一个并行队列

为什么选择并发队列而不是串行队列?

串行队列可以保证任务按照添加顺序挨个开始执行,并且上个任务结束才开始下一个任务,这已经可以保证任务的执行顺序(或者说是任务结束的顺序)了,但是并发队列只能保证任务的开始,不能保证任务的结束顺序,解决办法就是:并发队列使用Barrier保证控制任务结束顺序。

这部分就先到这里继续向下看:

dispatch_barrier_sync(self.barrierQueue, ^{
        BOOL first = NO;
        if (!self.URLCallbacks[url]) {
            self.URLCallbacks[url] = [NSMutableArray new];
            first = YES;
        }
        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;
这些代码的目的都是为了给url绑定回调

URLCallbacks是一个可变字典,keyNSURL类型,valueNSMutableArray类型,value(数组里面)只包含一个元素,这个元素的类型是NSMutableDictionary类型,这个字典的keyNSString类型代表着回调类型,valueblock,是对应的回调

继续向下看:

   if (first) {
            createCallback();
    }

如果url第一次绑定它的回调,也就是第一次使用这个url创建下载任务,则执行一次创建回调

如何确保同一url对应的图片不会被重复下载?

在创建回调中 创建下载操作(下载操作并不是在这里创建的),dispatch_barrier_sync执行确保同一时间只有一个线程操作URLCallbacks属性,也就是确保了下面创建过程中在给operation传递回调的时候能取到正确的self.URLCallbacks[url]值,同时确保后面有相同的url再次创建的时候if (!self.URLCallbacks[url])分支不再进入,first==NO,也就不再继续调用创建回调,这样就确保了同一个url对应的图片不会重复下载

以上这部分代码总结起来只做了一件事情:在barrierQueue队列中创建下载任务

功能

  • UIImageViewUIButtonMKAnnotationView的类别添加网页图像和缓存管理
  • 异步图像下载器
  • 具有自动到期处理的异步 内存+磁盘 缓存
  • 背景图像解压缩
  • 同一个URL不会下载多次
  • 虚假无效网址不会重复请求
  • 主线程永远不会被阻塞
  • 性能优势
  • 用GCD 和 ARC

优势

相对于原生NSURLRequestNSURLCache处理磁盘缓存,SDWebImage有什么优势?
  • 从iOS 105.0起, NSURLCache在内存和磁盘上缓存的是原始HTTP响应数据,每次击中缓存时,程序都必须将原始缓存数据转换为UIImage才能使用。而这个转化过程涉及复杂且广泛的操作,如数据解析(解析被编码过的HTTP数据),内存复制等。

  • SDWebImage采用UIImage的形式在内存中缓存图片数据,并将已解码过的HTTP数据的压缩文件存储在磁盘上。 使用NSCache将UIImage原样存储在内存中,因此不会涉及任何副本,并且只要程序或系统需要,内存就会被释放。

  • 一般第一次在UIImageView中使用UIImage时的图像解压缩是在主线程完成的,而SDWebImageDecoder强制其在后台线程中。

  • SDWebImage完全绕过复杂且经常配置错误的HTTP缓存控制协议,大大加速了缓存查找。

相对于AFNetworkingUIImageView提供的类似功能,SDWebImage有什么优势?
  • AFNetworking默认使用NSCacheUIKit中对UIImageViewUIButton配置内存缓存。
  • SDWebImage同时利用Foundation框架中的NSURLCacheURL系统加载缓存,也用到了NSCacheAFNetworking还提供了如图像数据的后台解压缩功能。
  • 也就是说AFNetworking UIKit部分实现了简单的异步图像加载类别,是SDWebImage的部分功能

第二部分

在使用这个框架的时候,只需要提供一个下载的url和占位图就可以在回调里拿到下载后的图片:

[imageview sd_setImageWithURL:[NSURL URLWithString:@"pic.jpg"] placeholderImage:[UIImage imageNamed:@"placeholder"] completed:^(UIImage * _Nullable image, NSError * _Nullable error, SDImageCacheType cacheType, NSURL * _Nullable imageURL) {

        imageview.image = image;
        NSLog(@"图片加载完成");

    }];

而且我们还可以不设置占位图片,也可以不使用回调的block,非常灵

//图片下载完成后直接显示下载后的图片
[imageview sd_setImageWithURL:[NSURL URLWithString:@"pic.jpg"]];

在最开始先简单介绍这个框架

这个框架的核心类是SDWebImageManger,在外部有UIImageView+WebCacheUIButton+WebCache为下载图片的操作提供接口。内部有SDWebImageManger负责处理和协调 SDWebImageDownloaderSDWebImageCacheSDWebImageDownloader负责具体的下载任务,SDWebImageCache负责关于缓存的工作:添加,删除,查询缓存。

首先我们大致看一下这个框架的调用流程图:

SDWebImage框架结构图.png

从这个流程图里可以大致看出,该框架分为两个层:UIKit层(负责接收下载参数)和工具层(负责下载操作和缓存)。

OK~基本流程大概清楚了,我们看一下每个层具体实现吧~


UIKit层

该框架最外层的类是UIImageView +WebCache,我们将图片的URL,占位图片直接给这个类。下面是这个类的公共接口:

 // ==============  UIImageView + WebCache.h ============== //
- (void)sd_setImageWithURL:(NSURL *)url;

- (void)sd_setImageWithURL:(NSURL *)url
          placeholderImage:(UIImage *)placeholder;

- (void)sd_setImageWithURL:(NSURL *)url
          placeholderImage:(UIImage *)placeholder
                   options:(SDWebImageOptions)options;

- (void)sd_setImageWithURL:(NSURL *)url
                 completed:(SDWebImageCompletionBlock)completedBlock;

- (void)sd_setImageWithURL:(NSURL *)url
          placeholderImage:(UIImage *)placeholder
                 completed:(SDWebImageCompletionBlock)completedBlock;

- (void)sd_setImageWithURL:(NSURL *)url
          placeholderImage:(UIImage *)placeholder
                   options:(SDWebImageOptions)options
                 completed:(SDWebImageCompletionBlock)completedBlock;

- (void)sd_setImageWithURL:(NSURL *)url
          placeholderImage:(UIImage *)placeholder
                   options:(SDWebImageOptions)options
                  progress:(SDWebImageDownloaderProgressBlock)progressBlock
                 completed:(SDWebImageCompletionBlock)completedBlock;

- (void)sd_setImageWithPreviousCachedImageWithURL:(NSURL *)url
                                 placeholderImage:(UIImage *)placeholder
                                          options:(SDWebImageOptions)options
                                         progress:(SDWebImageDownloaderProgressBlock)progressBlock
                                        completed:(SDWebImageCompletionBlock)completedBlock;


可以看出,这个类提供的接口非常灵活,可以根据我们自己的需求来调用其中某一个方法,而这些方法到最后都会走到:

// ==============  UIImageView + WebCache.m ============== //
- (void)sd_setImageWithURL:(NSURL *)url
          placeholderImage:(UIImage *)placeholder
                   options:(SDWebImageOptions)options
                  progress:(SDWebImageDownloaderProgressBlock)progressBlock
                 completed:(SDWebImageCompletionBlock)completedBlock;

而这个方法里面,调用的是UIView+WebCache分类的:

// ==============  UIView+ WebCache.m ============== //
- (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+WebCache而要上一层到UIView的分类里呢?
因为SDWebImage框架也支持UIButton的下载图片等方法,所以需要在它们的父类:UIView里面统一一个下载方法。

简单看一下这个方法的实现(省略的代码用...代替):

 // ==============  UIView+ WebCache.m ============== //

    //valid key:UIImageView || UIButton
    NSString *validOperationKey = operationKey ?: NSStringFromClass([self class]);
    //UIView+WebCacheOperation 的 operationDictionary
    //下面这行代码是保证没有当前正在进行的异步下载操作, 使它不会与即将进行的操作发生冲突
    [self sd_cancelImageLoadOperationWithKey:validOperationKey];


    /* options & SDWebImageDelayPlaceholder这是一个位运算的与操作,
       !(options & SDWebImageDelayPlaceholder)的意思就是options参数
       不是SDWebImageDelayPlaceholder,就执行以下操作 
    */
    if (!(options & SDWebImageDelayPlaceholder)) {
        dispatch_main_async_safe(^{
            [self sd_setImage:placeholder imageData:nil basedOnClassOrViaCustomSetImageBlock:setImageBlock];
        });
    }

    //如果url存在
    if (url) {

       ...
        __weak __typeof(self)wself = self;

       //SDWebImageManager下载图片
        id <SDWebImageOperation> operation = [SDWebImageManager.sharedManager loadImageWithURL:url options:options progress:progressBlock completed:^(UIImage *image, NSData *data, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {

            ...
            //dispatch_main_sync_safe : 保证block能在主线程进行
            dispatch_main_async_safe(^{

                if (!sself) {
                    return;
                }

            /* SDWebImageAvoidAutoSetImage,默认情况下图片会在下载完毕后自动添加
               给imageView,但是有些时候我们想在设置图片之前加一些图片的处理,就要下
               载成功后去手动设置图片了,不会执行` wself.image = image; `,而是直接执行
               完成回调,有用户自己决定如何处理。
            */
                if (image && (options & SDWebImageAvoidAutoSetImage) && completedBlock) {
                     //image,而且不自动替换 placeholder image
                    completedBlock(image, error, cacheType, url);
                    return;

                } else if (image) {
                    //存在image,需要马上替换 placeholder image
                    [sself sd_setImage:image imageData:data basedOnClassOrViaCustomSetImageBlock:setImageBlock];
                    [sself sd_setNeedsLayout];

                } else {                    
                    //没有image,在图片下载完之后显示 placeholder image
                    if ((options & SDWebImageDelayPlaceholder)) {
                        [sself sd_setImage:placeholder imageData:nil basedOnClassOrViaCustomSetImageBlock:setImageBlock];
                        [sself sd_setNeedsLayout];
                    }
                }

                if (completedBlock && finished) {
                    completedBlock(image, error, cacheType, url);
                }
            });
        }];

        //在操作缓存字典(operationDictionary)里添加operation,表示当前的操作正在进行
        [self sd_setImageLoadOperation:operation forKey:validOperationKey];

    } else {
        //如果url不存在,就在completedBlock里传入error(url为空)
        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);
            }
        });
    }

值得一提的是,在这一层,使用一个字典operationDictionary专门用作存储操作的缓存,随时添加,删除操作任务。
而这个字典是UIView+WebCacheOperation分类的关联对象,它的存取方法使用运行时来操作:

 // ==============  UIView+WebCacheOperation.m ============== //
- (SDOperationsDictionary *)operationDictionary {
    SDOperationsDictionary *operations = objc_getAssociatedObject(self, &loadOperationKey);
    if (operations) {
        return operations;
    }
    operations = [NSMutableDictionary dictionary];
    objc_setAssociatedObject(self, &loadOperationKey, operations, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    return operations;
}

为什么不直接在UIImageView+WebCache里直接关联这个对象呢?我觉得这里作者应该是遵从面向对象的单一职责原则(SRP:Single responsibility principle),就连类都要履行这个职责,何况分类呢?这里作者专门创造一个分类UIView+WebCacheOperation来管理操作缓存(字典)。

到这里,UIKit层上面的东西都讲完了,现在开始正式讲解工具层。

工具层

上文提到过,SDWebImageManager同时管理SDImageCacheSDWebImageDownloader两个类,它是这一层的老大哥。在下载任务开始的时候,SDWebImageManager首先访问SDImageCache来查询是否存在缓存,如果有缓存,直接返回缓存的图片。如果没有缓存,就命令SDWebImageDownloader来下载图片,下载成功后,存入缓存,显示图片。以上是SDWebImageManager大致的工作流程。

在详细讲解SDWebImageManager是如何下载图片之前,我们先看一下这个类的几个重要的属性:

 // ==============  SDWebImageManager.m ============== //
/*
*初始化方法
*1.获得一个SDImageCache的实例
*2.获得一个SDWebImageDownloader的实例
*3.新建一个MutableSet来存储下载失败的url
*4.新建一个用来存储下载operation的可变数组
*/
@property (strong, nonatomic, readwrite, nonnull) SDImageCache *imageCache;
@property (strong, nonatomic, readwrite, nonnull) SDWebImageDownloader *imageDownloader;
@property (strong, nonatomic, nonnull) NSMutableSet<NSURL *> *failedURLs;
@property (strong, nonatomic, nonnull) NSMutableArray<SDWebImageCombinedOperation *> *runningOperations;

SDWebImageManager下载图片的方法只有一个:

[SDWebImageManager.sharedManager loadImageWithURL:options:progress:completed:]

看一下这个方法的具体实现:

 // ==============  SDWebImageManager.m ============== //
- (id <SDWebImageOperation>)loadImageWithURL:(nullable NSURL *)url
                                     options:(SDWebImageOptions)options
                                    progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                                   completed:(nullable SDInternalCompletionBlock)completedBlock {
     ...                             
    //在SDImageCache里查询是否存在缓存的图片
    operation.cacheOperation = [self.imageCache queryCacheOperationForKey:key done:^(UIImage *cachedImage, NSData *cachedData, SDImageCacheType cacheType) {

        ...
        //(没有缓存图片) || (即使有缓存图片,也需要更新缓存图片) || (代理没有响应imageManager:shouldDownloadImageForURL:消息,默认返回yes,需要下载图片)|| (imageManager:shouldDownloadImageForURL:返回yes,需要下载图片)
        if ((!cachedImage || options & SDWebImageRefreshCached) && (![self.delegate respondsToSelector:@selector(imageManager:shouldDownloadImageForURL:)] || [self.delegate imageManager:self shouldDownloadImageForURL:url])) {

            //1. 存在缓存图片 && 即使有缓存图片也要下载更新图片
            if (cachedImage && options & SDWebImageRefreshCached) {
                [self callCompletionBlockForOperation:weakOperation completion:completedBlock image:cachedImage data:cachedData error:nil cacheType:cacheType finished:YES url:url];
            }


            // 2. 如果不存在缓存图片
            ...

            //开启下载器下载
            //subOperationToken 用来标记当前的下载任务,便于被取消
            SDWebImageDownloadToken *subOperationToken = [self.imageDownloader downloadImageWithURL:url options:downloaderOptions progress:progressBlock completed:^(UIImage *downloadedImage, NSData *downloadedData, NSError *error, BOOL finished) {
                __strong __typeof(weakOperation) strongOperation = weakOperation;
                if (!strongOperation || strongOperation.isCancelled) {
                    // 1. 如果任务被取消,则什么都不做,避免和其他的completedBlock重复

                } else if (error) {

                    //2. 如果有错误
                    //2.1 在completedBlock里传入error
                    [self callCompletionBlockForOperation:strongOperation completion:completedBlock error:error url:url];

                            //2.2 在错误url名单中添加当前的url
                    if (   error.code != NSURLErrorNotConnectedToInternet
                        && error.code != NSURLErrorCancelled
                        && error.code != NSURLErrorTimedOut
                        && error.code != NSURLErrorInternationalRoamingOff
                        && error.code != NSURLErrorDataNotAllowed
                        && error.code != NSURLErrorCannotFindHost
                        && error.code != NSURLErrorCannotConnectToHost) {

                       @synchronized (self.failedURLs) {
                            [self.failedURLs addObject:url];
                        }
                    }
                }
                else {

                    //3. 下载成功
                    //3.1 如果需要下载失败后重新下载,则将当前url从失败url名单里移除
                    if ((options & SDWebImageRetryFailed)) {
                        @synchronized (self.failedURLs) {
                            [self.failedURLs removeObject:url];
                        }
                    }

                    //3.2 进行缓存
                    BOOL cacheOnDisk = !(options & SDWebImageCacheMemoryOnly);


                    if (options & SDWebImageRefreshCached && cachedImage && !downloadedImage) {

                        //(即使缓存存在,也要刷新图片) && 缓存图片 && 不存在下载后的图片:不做操作

                    } else if (downloadedImage && (!downloadedImage.images || (options & SDWebImageTransformAnimatedImage)) && [self.delegate respondsToSelector:@selector(imageManager:transformDownloadedImage:withURL:)]) {

          //(下载图片成功 && (没有动图||处理动图) && (下载之后,缓存之前处理图片)               dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
                            UIImage *transformedImage = [self.delegate imageManager:self transformDownloadedImage:downloadedImage withURL:url];

                            if (transformedImage && finished) {
                                BOOL imageWasTransformed = ![transformedImage isEqual:downloadedImage];
                                // pass nil if the image was transformed, so we can recalculate the data from the image
                                //缓存图片
                                [self.imageCache storeImage:transformedImage imageData:(imageWasTransformed ? nil : downloadedData) forKey:key toDisk:cacheOnDisk completion:nil];
                            }
                            //将图片传入completedBlock
                            [self callCompletionBlockForOperation:strongOperation completion:completedBlock image:transformedImage data:downloadedData error:nil cacheType:SDImageCacheTypeNone finished:finished url:url];
                        });
                    } else {

                        //(图片下载成功并结束)
                        if (downloadedImage && finished) {
                            [self.imageCache storeImage:downloadedImage imageData:downloadedData forKey:key toDisk:cacheOnDisk completion:nil];
                        }

                        [self callCompletionBlockForOperation:strongOperation completion:completedBlock image:downloadedImage data:downloadedData error:nil cacheType:SDImageCacheTypeNone finished:finished url:url];
                    }
                }

                     //如果完成,从当前运行的操作列表里移除当前操作
                if (finished) {
                    [self safelyRemoveOperationFromRunning:strongOperation];
                }
            }];

            //取消的block
            operation.cancelBlock = ^{

                //取消当前的token
                [self.imageDownloader cancel:subOperationToken];
                __strong __typeof(weakOperation) strongOperation = weakOperation;
                //从当前运行的操作列表里移除当前操作
                [self safelyRemoveOperationFromRunning:strongOperation];
            };

        } else if (cachedImage) {

            //存在缓存图片
            __strong __typeof(weakOperation) strongOperation = weakOperation;

            //调用完成的block
            [self callCompletionBlockForOperation:strongOperation completion:completedBlock image:cachedImage data:cachedData error:nil cacheType:cacheType finished:YES url:url];

            //删去当前的的下载操作(线程安全)
            [self safelyRemoveOperationFromRunning:operation];

        } else {

            //没有缓存的图片,而且下载被代理终止了
            __strong __typeof(weakOperation) strongOperation = weakOperation;

            // 调用完成的block
            [self callCompletionBlockForOperation:strongOperation completion:completedBlock image:nil data:nil error:nil cacheType:SDImageCacheTypeNone finished:YES url:url];

            //删去当前的下载操作
            [self safelyRemoveOperationFromRunning:operation];
        }
    }];

    return operation;                                                             
}

看完了SDWebImageManager的回调处理,我们分别看一下
SDImageCacheSDWebImageDownloader内部具体是如何工作的。首先看一下SDImageCache

SDImageCache

属性

 // ==============  SDImageCache.m ============== //
@property (strong, nonatomic, nonnull) NSCache *memCache;//内存缓存
@property (strong, nonatomic, nonnull) NSString *diskCachePath;//磁盘缓存路径
@property (strong, nonatomic, nullable) NSMutableArray<NSString *> *customPaths;//
@property (SDDispatchQueueSetterSementics, nonatomic, nullable) dispatch_queue_t //ioQueue唯一子线程;

核心方法:查询缓存

 // ==============  SDImageCache.m ============== //
- (nullable NSOperation *)queryCacheOperationForKey:(nullable NSString *)key done:(nullable SDCacheQueryCompletedBlock)doneBlock {
    if (!key) {
        if (doneBlock) {
            doneBlock(nil, nil, SDImageCacheTypeNone);
        }
        return nil;
    }

     //================查看内存的缓存=================//
    UIImage *image = [self imageFromMemoryCacheForKey:key];
    // 如果存在,直接调用block,将image,data,CaheType传进去
    if (image) {
        NSData *diskData = nil;

        // 如果是gif,就拿到data,后面要传到doneBlock里。不是gif就传nil
        if ([image isGIF]) {
            diskData = [self diskImageDataBySearchingAllPathsForKey:key];
        }
        if (doneBlock) {
            doneBlock(image, diskData, SDImageCacheTypeMemory);
        }
       // 因为图片有缓存可供使用,所以不用实例化NSOperation,直接范围nil
        return nil;
    }

    //================查看磁盘的缓存=================//
    NSOperation *operation = [NSOperation new];

  //唯一的子线程:self.ioQueue
    dispatch_async(self.ioQueue, ^{
        if (operation.isCancelled) {
            // 在用之前就判断operation是否被取消了,作者考虑的非常严谨
            return;
        }

        @autoreleasepool {
            NSData *diskData = [self diskImageDataBySearchingAllPathsForKey:key];
            UIImage *diskImage = [self diskImageForKey:key];c
            if (diskImage && self.config.shouldCacheImagesInMemory) {

                //  cost 被用来计算缓存中所有对象的代价。当内存受限或者所有缓存对象的总代价超过了最大允许的值时,缓存会移除其中的一些对象。
                NSUInteger cost = SDCacheCostForImage(diskImage);

                // 存入内存缓存中
                [self.memCache setObject:diskImage forKey:key cost:cost];
            }

            if (doneBlock) {
                dispatch_async(dispatch_get_main_queue(), ^{
                    doneBlock(diskImage, diskData, SDImageCacheTypeDisk);
                });
            }
        }
    });

    return operation;
}

SDWebImageDownloader

属性

 // ==============  SDWebImageDownloader.m ============== //
@property (strong, nonatomic, nonnull) NSOperationQueue *downloadQueue;//下载队列
@property (weak, nonatomic, nullable) NSOperation *lastAddedOperation;//最后添加的下载操作
@property (assign, nonatomic, nullable) Class operationClass;//操作类
@property (strong, nonatomic, nonnull) NSMutableDictionary<NSURL *, SDWebImageDownloaderOperation *> *URLOperations;//操作数组
@property (strong, nonatomic, nullable) SDHTTPHeadersMutableDictionary *HTTPHeaders;//HTTP请求头
@property (SDDispatchQueueSetterSementics, nonatomic, nullable) dispatch_queue_t barrierQueue;//用来阻塞前面的下载线程(串行化)

核心方法:下载图片

- (nullable SDWebImageDownloadToken *)downloadImageWithURL:(nullable NSURL *)url
                                                   options:(SDWebImageDownloaderOptions)options
                                                  progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                                                 completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock {
    __weak SDWebImageDownloader *wself = self;

    return [self addProgressCallback:progressBlock completedBlock:completedBlock forURL:url createCallback:^SDWebImageDownloaderOperation *{
        __strong __typeof (wself) sself = wself;
        NSTimeInterval timeoutInterval = sself.downloadTimeout;
        if (timeoutInterval == 0.0) {
            timeoutInterval = 15.0;
        }

        // In order to prevent from potential duplicate caching (NSURLCache + SDImageCache) we disable the cache for image requests if told otherwise
        // 为防止重复缓存,默认网络请求不进行缓存操作
        // 创建下载请求,配置相关参数
        NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url cachePolicy:(options & SDWebImageDownloaderUseNSURLCache ? NSURLRequestUseProtocolCachePolicy : NSURLRequestReloadIgnoringLocalCacheData) timeoutInterval:timeoutInterval];
        request.HTTPShouldHandleCookies = (options & SDWebImageDownloaderHandleCookies);
        request.HTTPShouldUsePipelining = YES;
        if (sself.headersFilter) {
            request.allHTTPHeaderFields = sself.headersFilter(url, [sself.HTTPHeaders copy]);
        }
        else {
            request.allHTTPHeaderFields = sself.HTTPHeaders;
        }
        // 针对每次下载操作,SD自创一个操作类,众多操作放在一个操作队列中,便于管理众多下载操作
        // 创建下载操作:SDWebImageDownloaderOperation用于请求网络资源的操作,它是一个 NSOperation 的子类
        SDWebImageDownloaderOperation *operation = [[sself.operationClass alloc] initWithRequest:request inSession:sself.session options:options];
        operation.shouldDecompressImages = sself.shouldDecompressImages;

        //url证书
        if (sself.urlCredential) {
            operation.credential = sself.urlCredential;
        } else if (sself.username && sself.password) {
            operation.credential = [NSURLCredential credentialWithUser:sself.username password:sself.password persistence:NSURLCredentialPersistenceForSession];
        }
        
        // 优先级
        if (options & SDWebImageDownloaderHighPriority) {
            operation.queuePriority = NSOperationQueuePriorityHigh;
        } else if (options & SDWebImageDownloaderLowPriority) {
            operation.queuePriority = NSOperationQueuePriorityLow;
        }

         // 在下载队列里添加下载操作,执行下载操作
        [sself.downloadQueue addOperation:operation];

        // 如果后进先出
        if (sself.executionOrder == SDWebImageDownloaderLIFOExecutionOrder) {
            //  Emulate LIFO execution order by systematically adding new operations as last operation's dependency
           //  addDependency:参数opertaion倍添加到NSOperationQueue后,只有等该opertion结束后才能执行其他的operation,实现了后进先出
            [sself.lastAddedOperation addDependency:operation];
            sself.lastAddedOperation = operation;
        }

        return operation;
    }];
}

这里面还有一个addProgressCallback: progressBlock: completedBlock: forURL: createCallback:方法,用来保存progressBlockcompletedBlock。我们看一下这个方法的实现:

- (nullable SDWebImageDownloadToken *)addProgressCallback:(SDWebImageDownloaderProgressBlock)progressBlock
                                           completedBlock:(SDWebImageDownloaderCompletedBlock)completedBlock
                                                   forURL:(nullable NSURL *)url
                                           createCallback:(SDWebImageDownloaderOperation *(^)())createCallback {
    // The URL will be used as the key to the callbacks dictionary so it cannot be nil. 
If it is nil immediately call the completed block with no image or data.
    // URL 将会做回调字典的key,不能为nil
    if (url == nil) {
        if (completedBlock != nil) {
            completedBlock(nil, nil, nil, NO);
        }
        return nil;
    }

    __block SDWebImageDownloadToken *token = nil;

     // 串行化前面所有的操作
    dispatch_barrier_sync(self.barrierQueue, ^{

        // 当前下载操作中取出SDWebImageDownloaderOperation实例
        SDWebImageDownloaderOperation *operation = self.URLOperations[url];

        // 如果没有,就初始化它
        if (!operation) {
            operation = createCallback();
            self.URLOperations[url] = operation;

            __weak SDWebImageDownloaderOperation *woperation = operation;
            operation.completionBlock = ^{
              SDWebImageDownloaderOperation *soperation = woperation;
              if (!soperation) return;
              if (self.URLOperations[url] == soperation) {
                  [self.URLOperations removeObjectForKey:url];
              };
            };
        }

       // 这里 downloadOperationCancelToken 默认是一个字典,存放 progressBlock 和 completedBlock
        id downloadOperationCancelToken = [operation addHandlersForProgress:progressBlock completed:completedBlock];

        token = [SDWebImageDownloadToken new];
        token.url = url;
        token.downloadOperationCancelToken = downloadOperationCancelToken;
    });

    return token;
}

这里真正保存两个block的方法是addHandlersForProgress: completed:

- (nullable id)addHandlersForProgress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                            completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock {

   // 实例化一个SDCallbacksDictionary,存放一个progressBlock 和 completedBlock
    SDCallbacksDictionary *callbacks = [NSMutableDictionary new];
    if (progressBlock) callbacks[kProgressCallbackKey] = [progressBlock copy];
    if (completedBlock) callbacks[kCompletedCallbackKey] = [completedBlock copy];
    dispatch_barrier_async(self.barrierQueue, ^{
        // 添加到缓存中 self.callbackBlocks
        [self.callbackBlocks addObject:callbacks];
    });
    return callbacks;
}

到这里SDWebImage的核心方法都讲解完毕了,其他没有讲到的部分以后会慢慢添加上去。

最后看一下一些比较零散的知识点:


1. 运行时存取关联对象:

存:

// 将operations对象关联给self,地址为&loadOperationKey,语义是OBJC_ASSOCIATION_RETAIN_NONATOMIC。
objc_setAssociatedObject(self, &loadOperationKey, operations, OBJC_ASSOCIATION_RETAIN_NONATOMIC);

取:

// 将operations对象通过地址&loadOperationKey从self里取出来
SDOperationsDictionary *operations = objc_getAssociatedObject(self, &loadOperationKey);

2. 数组的写操作需要加锁(多线程访问,避免覆写)

//给self.runningOperations加锁
//self.runningOperations数组的添加操作
    @synchronized (self.runningOperations) {
        [self.runningOperations addObject:operation];
    }

//self.runningOperations数组的删除操作
- (void)safelyRemoveOperationFromRunning:(nullable SDWebImageCombinedOperation*)operation {
    @synchronized (self.runningOperations) {
        if (operation) {
            [self.runningOperations removeObject:operation];
        }
    }
}

3. 确保在主线程的宏:

dispatch_main_async_safe(^{
                  //将下面这段代码放在主线程中
            [self sd_setImage:placeholder imageData:nil basedOnClassOrViaCustomSetImageBlock:setImageBlock];
        });

//宏定义:
#define dispatch_main_async_safe(block)\
    if (strcmp(dispatch_queue_get_label(DISPATCH_CURRENT_QUEUE_LABEL), dispatch_queue_get_label(dispatch_get_main_queue())) == 0) {\
        block();\
    } else {\
        dispatch_async(dispatch_get_main_queue(), block);\
    }
#endif

4. 设置不能为nil的参数

- (nonnull instancetype)initWithCache:(nonnull SDImageCache *)cache downloader:(nonnull SDWebImageDownloader *)downloader {
    if ((self = [super init])) {
        _imageCache = cache;
        _imageDownloader = downloader;
        _failedURLs = [NSMutableSet new];
        _runningOperations = [NSMutableArray new];
    }
    return self;
}

如果在参数里添加了nonnull关键字,那么编译器就可以检查传入的参数是否为nil,如果是,则编译器会有警告

5. 容错,强制转换类型

if ([url isKindOfClass:NSString.class]) {
        url = [NSURL URLWithString:(NSString *)url];
}

在传入的参数为NSString时(但是方法参数要求是NSURL),自动转换为NSURL

推荐阅读更多精彩内容