前言
Hi Coder,我是 CoderStar!
我们之前已经讲过 iOS多线程-Thread 以及 iOS多线程-GCD,我们今天来聊一聊 iOS 多线程中最后一种比较常用的方式 –Operation
。
概览
对于 Operation 而言,其相关的类比 GCD 要少的多。Operation
本身是一个 抽象类
,不能直接进行使用,其定义了相关的方法及属性,需要靠子类进行相应的实现,系统已经实现了一个 –BlockOperation
。(在 OC 中,还有一个是 NSInvocationOperation
,但在 Swift 中,该子类已经在 Swift4 里去掉,想必去掉的原因大家也很容易理解,因为 Swift 语言本身就不推荐 selector
这种形式)。
Operation
底层建立在 GCD
之上,是更高一级的抽象,使我们可以面向对象(Cocoa 对象)的方式进行多线程编程。
其实
NSOpertion
是先于GCD
引进的,在当时,NSOperationQueue
接收NSOperation
对象并创建一个线程,然后在该线程上运行main
方法 ,运行完成之后再杀死该线程。这种方式相对于后面出现的GCD
底层的线程池而言,效率就很低,所以在 Mac OS 10.5 以及 iOS 2 开始便对NSOpertion
底层在基于GCD
的基础上进行完全重写,利用GCD
的相关特性提高性能并提供了一些新功能。如果想简单佐证下,可以看到OperationQueue
拥有一个unowned(unsafe) open var underlyingQueue: DispatchQueue?
属性。
如果大家对 Operation 底层实现比较有兴趣,可以在开源的 Foundtion 框架中查看 Operation.swift。
基本原理
先罗列一下 Operation
及 OperationQueue
主要的属性及方法。
注释会有相应说明。
Operation
|
|
对 Operation
几个属性、方法再进行详细的说明:
cancel
方法
如果这个操作正在执行,调用 cancel()
只会将状态 isCanceled
置为 true
,但不会影响操作的继续执行。
如果操作还没执行,调用 cancel()
会将状态 isCanceled
和 isReady
置为 true
, 如果执行取消后的操作,会直接将状态 isFinished
置为 true
而不会执行操作。也会触发 completionBlock
方法。
所以当我们子类化 Operation
的时候在处理耗时以及启动等相关操作时,应先检查 isCanceled
状态。
addDependency
方法
- 需要注意在设置时不要设置成循环依赖,比如 A 依赖 B、B 又依赖 A,这样会形成死锁,导致谁也不会执行。
- 可以跨操作队列设置依赖。
- 当给某个
Operation
添加依赖的Operation
后,只有其所依赖的所有Operation
都执行完毕,当前的Operation
才能开始执行。不管依赖的Operation
是执行成功了还是失败了,或者是取消了,都认为是执行完毕了。
OperationQueue
|
|
对 OperationQueue
几个属性、方法再进行详细的说明:
maxConcurrentOperationCount
属性
- maxConcurrentOperationCount 如果不设置值时,默认值会取
defaultMaxConcurrentOperationCount
,也就是 -1,此时默认最大操作数由OperationQueue
对象根据当前系统条件(系统内存与 CPU)动态确定。 maxConcurrentOperationCount
为 0 时,队列中的 Operation 不会执行。maxConcurrentOperationCount
为 1 时,队列串行执行。maxConcurrentOperationCount
大于 1 时,队列并发执行,当然这个值不应超过系统限制,即使自己设置一个很大的值,系统也会自动调整为 min(自己设定的值,系统设定的默认最大值),系统默认限制应该是 64。
需要注意,因为有
queuePriority
的存在,同一个Queue
的Operation
之间有优先级,所以先进入 Queue 的Operation
不一定先运行,所以当maxConcurrentOperationCount
设置为 1 时并不是一个真正意义上的串行队列,优先级较高后加入的Operation
有可能会先执行。
64 这个值在 GCD 下应该也是默认最大线程数,但是可以调整目标队列的优先级进行调整。这里涉及到一个线程爆炸的概念,后面可能还会出一篇文章写这些东西。
从上面 Operation
的几个状态属性我们可以知道 Operation
在程序运行过程中状态会进行相应的流转,其状态图如下所示。
使用
对于一般的任务,我们可以直接使用 BlockOperation
。使用示例如下:
|
|
但是很多时候,我们需要继承 Operation
进行一些自定义操作,如网络请求的依赖。这时,我们需要继承 Operation
重写对应的属性与方法来实现。
网络请求的依赖为何需要子类化 Operation:普通的
Operation
等待main
方法执行完毕之后就会自动将isFinished
置为true
,继而执行下一个,但是对于网络请求这种场景,我们需要手动控制,等待网络请求回调之后再将isFinished
置为true
。
这部分内容,Apple 的文档上有详细的介绍,Operation文档链接
除了将 Operation
放入 OperationQueue
运行这种方式之外,还可以直接调用 start
方法进行运行。
对于第一种方式,OperationQueue
会自动为 Operation
开辟线程,不需进行额外的处理,对于第二种方式,就需要我们手动进行控制,我们可以将操作设计为同步或者异步的,也就是所谓的非并发 Operation
以及并发 Operation
当然,其实直接调用
start
方法这种方式在日常开发过程中用的比较少的,主要是使用OperationQueue
这种方式。下列部分主要是给大家拓宽一下Operation
的使用方式以及了解当子类化Operation
时我们需要注意的地方。
Operation
内部本身是线程安全的,当我们子类化 Operation
时,不管是非并发 Operation
还是并发 Operation
,我们也需要保证其线程安全,所以需要在一些地方加上互斥锁,如后续操作中的状态切换时。
非并发 Operation
对于非并发 Operation
,因为 Operation
在默认情况直接调用 start
方法是一个同步操作,所以当我们继承 Operation
来实现一个非并发 Operation
时,我们只需要重写 main
方法。
|
|
并发 Operation
如果是并发 Operation
,则至少需要重写以下属性及方法,并且运行状态更新时需要生成 KVO 通知。
- isAsynchronous
- isExecuting
- isFinished
- start()
具体代码如下,请注意阅读注释:
|
|
代码中几个地方需要特别说明一下:
- 状态变量切换时,为保证线程安全,我们需要进行加锁;
- 虽然官方文档说
main
方法不需要强制进行重写,但为了逻辑性,start
方法主要负责任务的启动,main
方法中进行任务的处理,所以重写的main
方法。 - 关于
isAsynchronous
属性,刚开始我以为其可以控制Operation
是否自动开辟线程,但是根据实验以及查看源码之后,发现其应该只是一个标识当前Operation
是否是异步操作的一个标志而已,当设置为true
时,我们需要自己开辟线程进行任务的分发。当我们确定该Operation
后续都是以OperationQueue
的形式运行,我们也可以将isAsynchronous
返回值改为 false,去除内部的队列。
关于 Operation
的具体应用,大家可以去阅读 Alamofire
或者 SDWebImage
等开源库的源码,内部都有其应用。
GCD VS Operation
使用 GCD
还是使用 Operation
这个问题其实在社区已经争论了很久,从斯坦福大学的 CS193p 课程推荐使用 GCD
,到 WWDC 2012 时演讲者推荐使用 Operation
,也能看出开发者对该问题的看法不一致,该节我们主要来聊一聊两者各自优势以及差别。
目前网络上的很多文章都是基于没有
DispatchWorkItem
对象前提下对 GCD 和Operation
做的对比,大家阅读时需要注意一下。
1、从两者所在层次来讲:GCD 底层是 C 语言的 API,而 Operation 是 GCD 基础上更高层次的抽象,那 GCD 相对 Operation 来说肯定是又快又轻的。(Operation 在使用 GCD API 的基础上还会加上一些锁用来保证线程安全)
但是反过来说因为 Operation 是更高层次的抽象,按照一般的经验法则来看,我们应首先使用最高级别的 API,然后在根据需要完成内容进行降级。从这一角度来看,使用 Operation 抽象度更高,更符合面向对象的思想,也有利于底层的无痕变更。
2、从两者提供的 API 来讲:其实 GCD 和 Operation 两者之间是很相似的,特别是当 DispatchWorkItem
对象(@available(macOS 10.10, iOS 8.0, *))出来之后,从一定意义上讲,DispatchWorkItem
可以类比到 Operation
对象,DispatchQueue
可以类比到 OperationQueue
对象。比如 DispatchWorkItem
和 Operation
对象都可以进行 cancel
等操作,DispatchQueue
、OperationQueue
对象都可以添加任务或操作(对象以及闭包两种形式),栅栏函数,进行挂起以及恢复(前者是两个对应方法,后者是一个属性)等。但两者还是有一些区别的,比如:
OperationQueue
可以设置并发操作的最大数量maxConcurrentOperationCount
。在一定条件下可以类比到 GCD 的信号量
在不同的任务之间建立依赖关系
addOperation
;在一定条件下可以类比到 GCD 的
DispatchWorkItem
的public func notify(queue: DispatchQueue, execute: DispatchWorkItem)
方法OperationQueue
可以取消队列中的所有操作。…
3、相对 GCD
而言,Operation
可以通过子类化的方式将一些操作内聚,更方便管理。
最后
要更加努力呀!
Let’s be CoderStar!
推荐学习资料