SDWebImage 源代码剖析-多线程策略

前一篇文章从缓存策略的角度分析了SDWebImage 的部分代码,下面从多线程的角度对它的其他模块进行分析。

苹果的多线程解决方案有三种,分别是NSThreadGCD以及NSOperation。在实际开发中,我们常用的方案是GCD 和NSOperation。关于如何在GCD 和NSOperation 中进行选择,Stackoverflow 上已经有很多详细和深入的讨论,这里不再展开。本篇开始时我们将首先介绍SDWebImage所使用的NSOperation,然后深入SDWebImage的源码来分析它是如何使用NSOperation的。

NSOperation

NSOperation 定义在Foundation 框架中,表示一个用来完成某项工作的操作。它是一个抽象类,因此在实际使用时,必须让一个类继承它,然后实现某些方法。Foundation 为我们事先定义好了两个子类,分别是NSInvocationOperationNSBlockOperation,这两个类可以直接使用。

实现自定义的NSOperation 子类

如果说上面提到的两个子类不能满足我们的需求,我们还可以自己去实现一个NSOperation 的子类。
如果你的这个子类是非并行的,那么只要实现下面两个方法,再加上对取消操作的响应即可;如果子类是并行的,那还需要重载NSOperation 中的某些方法。我们先看下并行和串行子类都必须实现的几个方法。

每个operation 对象都应该实现的方法

  • 一个自定义initialization 方法
  • main 方法

initialization 用于对operation 对象进行初始化设置,main用于执行具体的任务。

下面是一个示例:

对取消操作做出响应

注意,上面的那个例子没有实现对取消操作的响应。而事实上operation 对象应该对取消事件作出响应。当一个operation 对象被执行后,它会一直运行,直到任务完成或者是操作被取消,而取消操作甚至可能发生在任务开始执行之前。当取消操作发生的时候,operation 对象需要回收已经被分配的资源,之后优雅地退出。

为了能对取消操作作出反应,在代码中应该周期性地调用operation 对象的isCancelled 方法。具体来说,应该是在如下几个场景处调用此方法:

  • 当进行任何实际性的工作时。
  • 在每次循环中至少调用一次,如果每次循环的时间很久,则可以在一次循环中多次调用。这个方法本身的开销很小,因此不必担心多次调用会带来性能上的下降。
  • 当取消操作很可能发生的时候。

并行operation 对象的配置

Operation objects execute in a synchronous manner by default—that is, they perform their task in the thread that calls their start method.

下面这张表列出了并行operation 对象所需要重载的一些方法。

方法 类型 描述
start 必须实现 通常在这个方法中来设置并行任务的执行环境,如具体的线程等。这个方法是operation 对象的起始点。在这个方法中不可以调用super 的方法。
main 可选的 在这个方法中实现具体要完成的任务。虽然实现也可以放在start 方法中来做,但是放在main 方法中可以使得设置和执行分离,使得代码的逻辑更加清晰。
isExecuting/isFinished 必须实现 这两个方法用来向外部对象报告operation 对象的运行状态。 这两个方法的实现必须是线程安全的。
isConcurrent 必须实现 为了确认一个operation 对象是否是并行的,需要重载这个方法,让它返回YES。

有了以上知识作铺垫,我们来看下一个并行operation 对象应该怎么完整地实现:

注意:即使operation 对象是被取消的,也需要通知这个对象的观察者它已经完成了。因为如果一个operation 对象依赖于其他对象,那么它会观察其他对象的isFinished属性,只有当它所依赖的对象都已经发出了完成的信号后,这个对象才会开始运行。所以如果忘记发出完成通知,那么可能就有一个operation 对象永远都无法执行。

自定义operation 对象的执行行为

配置对象间的依赖关系

前面多次提到,一个operation 对象可能依赖于另外几个operation 对象。在它所依赖的一个operation 对象都运行完成后,这个operation 对象才可以开始运行。

为一个operation 对象配置依赖的操作很简单,只需要调用下面这个函数:

取消依赖也是类似的,只要调用

改变operation 对象的执行优先级

对于队列中的operation 对象,它们的执行顺序取决于它们各自的优先级。更准确的说,对于那些已经ready 的operation 对象来说,优先级高的先执行,优先级低的后执行。

operation 对象的优先级用这个函数来设置:

priority 的可能取值有这几个:

  • NSOperationQueuePriorityVeryLow
  • NSOperationQueuePriorityLow
  • NSOperationQueuePriorityNormal
  • NSOperationQueuePriorityHigh
  • NSOperationQueuePriorityVeryHigh

设置完成时的block

setCompletionBlock: 这个方法可以用来设置当operation执行完后的回调block。

执行operations

当完成了对operation 对象的配置后,就可以执行了。

使用OperationQueue

执行Operation 的最简单的方法是使用NSOperationQueue 。它也是我们分析SDWebImage 时所关注的一个核心类。

最大并行任务数量

macConcurrentOperationCount这个成员可以控制该队列中最多允许多少个任务同时进行。如果使用setMaxConcurrentOperationCount: 将该成员设置为1, 那么这个队列一次只能执行一个operation,也就相当于一个串行队列了。

取消operations

cancelAllOperations 可以用来取消一个operationQueue 内的所有operation。

等待operations 完成

在一个operation 实例上调用waitUntilFinished可以短暂地阻塞这个operation,直到此opeartion 执行完。但是通常我们应该避免这样做。尤其是不要在主线程上调用这个方法,否则可能阻塞主线程,导致程序失去响应。

阻塞operation

在operationQueue 上调用setSuspended 可以阻塞那些还没有执行的operation,但是不会影响那些正在执行的和已经执行的operation。

SDWebImage 中的实现

上面一节介绍了一些理论知识,现在我们来看看SDWebImage的代码是怎么实践这些理论的。SDWebImage 中,使用NSOperation 对象和GCD 的地方主要是在SDWebImageDownloader 中,使用SDWebImageDownloaderOperation 来完成图片的下载任务。

SDWebImageDownloader

在这个类的类扩展中,有几个属性是与Operation 和OperationQueue 对象有联系的:

从名字和注释中不难猜测到它们各自的作用:

  • downloadQueue:用来管理 download 操作的队列。
  • lastAddedOperation:上一个被添加进队列的 operation。
  • barrierQueue:这是一个dispatch_queue_t 类型的属性,作用是串行地处理所有 download 操作的网络响应。

downloadQueue

downloadImageWithURL:options:progress:completed:这个方法中,SDWebImageDownloader 为每一个下载图片的请求创建一个SDWebImageDownloaderOperation ,并将这个SDWebImageDownloaderOperation 加入downloadQueue 中。

在将operation 添加进downloadQueue 后,还可以设置operation 对象在这个队列中的执行顺序:

downloadQueue 的默认执行顺序是

  • SDWebImageDownloaderFIFOExecutionOrder,也就是先入先出,其行为类似于一个队列;

当执行顺序改为

  • SDWebImageDownloaderLIFOExecutionOrder 时,则是后入先出,其行为类似于一个栈。lastAddedOperation 就是用来记录当前栈顶的那个operation 的,当有新的operation 要进栈时,就把这个新的operation 设置为lastAddedOperation 的依赖,这样的话,只有新的operation 执行完毕,lastAddedOperation 才能开始执行,因此就实现了后入先出的效果。

SDWebImageDownloaderOperation

SDWebImageDownloaderOperationNSOperation 的子类,是SDWebImage 中完成下载任务的operation 对象。这个operation 对象的设计,符合我们在『实现自定义的NSOperation 子类』一节中介绍的要求。对于它的start 和其他方法,由于是对网络请求部分的具体实现,与本文主题无关,因此暂时不做介绍(日后若有时间,我将专门写一篇博客来介绍SDWebImage 中的网络部分)。

属性

SDWebImageDownloadOperation中使用了两个BOOL属性来表示当前operation所处在的状态,分别是:

barrierQueue

前面提到,barrierQueue 是用来串行地处理所有的download 操作的网络响应的。关于dispatch_queue_tNSOperation 的对比,可以参见这儿

这段代码中,SDWebImageDownloader 维护了一个NSDictionary 用以将url 和url 对应的operation 联系起来。值得注意的是,在前一个方法中,使用了dispatch_barrier_async,在后一个方法中,则使用了dispatch_barrier_sync,那么这两个函数究竟有什么区别呢?

我们知道dispatch_barrier_asyncdispatch_barrier_sync都是为了解决竞争问题而提出来的。举例来说,现在我们在项目中使用一个数据库,那么对数据库的操作就有读和写两种操作。假设我们需要先读两次数据库,然后再写入一个数据,然后再读出两个数据,那么显然读和写的操作是不能同时发生的,如果同时发生,就有可能造成数据错误。

使用dispatch_barrier_(a)sync,上述任务可以这样实现:

dispatch_barrier_(a)sync 就像是一个屏障一样,它确保blk_write 一定会等到blk1,blk2,blk3 全部执行完毕才开始执行,在blk_write 执行完毕后,blk4 和blk5 又正常地并行运行。

syncasync 的区别体现在:将block 追加到指定的queue 中是否是同步的。如果是async 的,那么dispatch_barrier_async 不会等待追加操作结束,当前进程会继续进行;如果是sync 的,那么dispatch_barrier_async 会等待一直追加操作结束,当前进程会因此阻塞。

用例子来解释:

在A 中,step1 先执行,但是step3 可能会在step2 之前执行完,因此执行顺序可能是step1->step2->step3,也可能是step1->step3->step2;在B 中,执行的顺序则一定是step4->step5->step6

addProgressCallback:方法中,因为需要返回一个token,而这个token 是在block 中计算的,所以我们一定要等到block 的追加操作完成才可以返回,否则就会得到nil,因此需要使用dispatch_barrier_sync;而在cancel: 方法中,不需要等待,因此使用dispatch_barrier_async 就可以了。

总结

多线程策略无疑是iOS 应用开发中一个十分重要的考虑因素。随着项目的进行,需求不断增多,应用的逻辑越加复杂,对性能的要求也随之提要,因此我们不得不借助于多线程来适应这些要求。本文简要介绍了SDWebImage 这个库中多线程策略的选择和实现。如果想要了解更多NSOperation的知识,可以参考WWDC 2015 Session 226-Advanced NSOperation的内容,视频版文字版

发表评论

电子邮件地址不会被公开。 必填项已用*标注

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据