NSOperation

<div style="text-align:center; margin-bottom:10px;"> <img src="/assets/nsoperation.png"
height="400" width="600"> </div>

几乎每个开发者都知道,让 App 快速响应的秘诀是把耗时的计算丢到后台线异步去做。于是,Modern Objective-C 开发者有两个选择:GCDNSOperation.

由于 GCD 已经发展的比较主流了,我们稍后再说它,先说说面向对象的 NSOperation.

NSOperation 表示一个单独的计算单元,它是一个抽象类(很类似 Java 里的 Runnable 接口),给子类提供了一些非常有用且线程安全的特性,比如 状态 (state), 优先级 (priority), 依赖 (dependencies) 以及 取消(cancellation). 如果你不想子类化 NSOperation,可以选择使用 NSBlockOperation 这个 NSOperation 的子类,它可以把一个 block 包装成为一个 NSOperation.

非常适合使用 NSOperation 的任务例子包括network requests, 图片的缩放,语言处理或者其他一些重复的、结构化的以及需要运行较长时间来处理数据的任务。

但是,仅仅把计算包装成一个对象,没有一些监管也不会非常的有用,这时 NSOperationQueue 就出现了。

NSOperationQueue 控制各个 operation 的并发执行. 它像是一个优先级队列,operation 大致的会按 FIFO 的方式被执行,不过带有高优先级的会跳到低优先级前面被执行(用 NSOperation 的 queuePriority 方法来设置优先级)。 NSOperationQueue 支持并发的执行 operations,通过 maxConcurrentOperationCount 来指定最大并发数,就是同时有最多有多少个 operation 同时被运行。

可以通过调用 -start 方法来启动一个 NSOperation,或者把它放到 NSOperationQueue 里,当到达队列最前端时也会被自动的执行。

现在来看看 NSOperation 的几个不同的特性,以及如何如果使用和子类化它:

状态 State

NSOperation 构建了一个非常优雅的状态机来描述一个 operation 的执行过程:

isReady -> isExecuting -> isFinished

State 是通过这些 keypath 的 KVO 通知来隐式的得到,而不是显式的通过一个 state 的属性。就是说,当一个 operation 已经准备就绪,将要被执行时,它会为isReadykeyPath 发送一个 KVO 的通知,对应的属性值也会变为 YES.

为了构造一致的状态,下面每个属性都与其他属性相互排斥:

  • isReady: 如果 operation 已经做好了执行的准备返回 YES,如果它所依赖的操作存在一些未完成的初始化步骤则返回 NO。
  • isExecuting: 如果 operation 正在执行它的任务返回 YES,否则返回 NO。
  • isFinished: 任务成功的完成了执行,或者中途被 Cancel,返回 YES。NSOperationQueue 只会把 isFinished 为 YES 的 operation 踢出队列,isFinished 为 NO 的永远不会被移除,所以实现时一定要保证其正确性,避免死锁的情况发生。

取消 Cancellation

如果正在进行的 operation 所做的工作不再有意义,尽早的取消掉是非常有必要的。取消一个 operation 可以是显式的调用 cancel 方法,也可以是 operation 依赖的其他 operation 执行失败。

和 state 类似,当 NSOperation 的被取消,是通过isCancelledkeypath 的 KVO 来获得。当 NSOperation 的子类覆写 cancel 方法时,注意清理掉内部分配的资源。特别注意的是,这时 isCancelled 和 isFinished 的值都变为了 YES,isExecuting 为值变为 NO。

一个需要格外注意的地方是和单词“cancel”有关的两个词:

  • cancel : 带一个 "l" 表示方法 (动词)
  • isCancelled : 带两个 "l" 表示属性(形容词)

优先级 Priority

所有的 operation 在 NSOperationQueue 中未必都是一样的重要,设置 queuePriority 属性就可以提升和降低 operation 的优先级,queuePriority属性可选的值如下:

  • NSOperationQueuePriorityVeryHigh
  • NSOperationQueuePriorityHigh
  • NSOperationQueuePriorityNormal
  • NSOperationQueuePriorityLow
  • NSOperationQueuePriorityVeryLow

另外,operation 可以指定一个 threadPriority 值,它的取值范围是 0.0 到 1.0,1.0 代表最高的优先级。queuePriority决定执行顺序的优先级,threadPriority决定当 operation 开始执行之后分配的计算资源的多少。

依赖 Dependencies

取决于你的 App 的复杂性,可能会需要把一个大的任务分成多个子任务,这时 NSOperation 依赖就排上用场了。

比如从服务器上下载和缩放图片的过程,你可能会想把下载图片作为一个 operation,缩放作为另外一个(这样也可以复用下载图片和缩放图片的代码)。然后,一个图片在从服务器上下载下来之前是没有办法缩放的,于是我们说缩放图片的 operation 依赖从服务器上下载图片的 operation,后者必须先完成,前者才能开始执行。用代码表示是这样的:

1
2
3
[resizingOperation addDependency:networkingOperation];
[operationQueue addOperation:networkingOperation];
[operationQueue addOperation:resizingOperation];
一个 operation 只有在它依赖的所有的 operation 的 isFinished 都为 YES 的时候才会开始执行。要记住添加到 queue 里的所有的 operation 的依赖关系,并避免循环依赖,比如 A 依赖 B,B 依赖 A,这样会产生死锁。

completionBlock

completionBlock是在 iOS4 和 Snow Leopard 中添加的一个非常有用的特性。当一个 NSOperation 完成之后,就会精确地只执行一次 completionBlock。我们需要在 operation 完成之后想做点什么的时候这个属性就会非常有用。比如当一个网络请求结束之后,可以在completionBlock 里处理返回的数据。

总结

NSOperation 依然是 Modern Objective-C 程序员杀手锏里的重要工具。相对于 GCD 非常适用于 in-line 的异步处理,NSOperation 提供了更综合的、面向对象的计算模型,非常适用于封装结构化的数据,重复性的任务。把它加到你的下个项目中,给你的用户和你自己都带来乐趣吧!

译者注

本文编译自 NSHipster 里的 NSOperation 一文,感谢作者Mattt Thompson, 来头很大,这是他的简介:

Mattt Thompson is the Mobile Lead at Heroku, and the creator & maintainer of AFNetworking and other popular open-source projects, including Postgres.app & Induction. He also writes about obscure & overlooked parts of Cocoa on NSHipster.

最上面的图片是来自于 WWDC2013 中的“Hidden Gems in Cocoa and Cocoa Touch”(228)中 Mattt 讲 NSOperation 时的截图,这个视频一共有 30 个 tips,这是第 8 个 tip,大部分的内容我是第一次知道,非常值得看,而且如果有条件的话,建议下载 HD 版本的视频来看,效果比 SD 好太多。字幕文件在我的这个 repo 里, :)

如有文中有不准确的地方,欢迎留言指正 :)

Enjoy!