前言
这篇文章是对 WWDC 2015 Session 226: Advanced NSOperations 的一个小结,在那个视频中,Dave DeLong 分享了 NSOperation 的高级玩法,WWDC App 就是基于这套玩法做的,还是挺开阔思路的。
NSOperation 和 NSOperationQueue 简介
我们知道 NSOperation 可以执行一些后台操作,如 HTTP 请求,在 iOS 4.0 之前是基于 NSThread 来实现的,iOS 4.0 带了 GCD,NSOperation 底层也基于 GCD 重写了底层实现。
所以 NSOperation 是 GCD 的高层封装,同时也带来了一些更加便利的功能,比如取消任务,设置依赖等。在进入高级玩法前,先简单的介绍下 NSOperation 和 NSOperationQueue。
NSOperationQueue maxConcurrentOperationCount
这个属性表示的是 NSOperationQueue 最多可以同时处理几个任务,假如我们希望它一次只处理一个,也就是线性 Queue,可以设置 maxConcurrentOperationCount = 1
中间的点表示任务的状态,在上一个任务完成前,下一个任务不会被执行,因为只有一个 worker。
如果希望一次能处理多个,将这个值设置为大于 1 即可,或者直接使用默认值,系统会自动设置一个合理的最大值。
NSOperation cancel
从上面的图可以看到,正在被执行的任务的状态跟在后面排队的状态是不一样的,有这么几种状态:pending, ready, executing, finished, cancelled。
之前提到过 NSOperation 一个很重要的特性是可以被取消,但不同状态的取消处理也不一样。比如当 Operation 处于 pending, ready 状态时,系统可以去看一下这个 Operation 是否已经被取消了(判断 self.cancelled),如果是的话,就不执行任务了。但是当 Operation 处于 executing 状态时,取消的操作就只能自己处理了,比如
@implementation MyOperation: NSOperation
- (void)main
{
// ...
while (!self.cancelled) {
// executing
}
}
@end
NSOperation dependency
NSOperation 还有一个很重要的特性是可以设置依赖
任务 A 需要等待 任务 B 和 任务 C 完成,才能被执行,而任务 B 需要等到 任务 D 完成才能被执行。
当然前提是这些 Operation 都需要被放到某个 Queue 里,这样它们的状态才会发生改变。
高级玩法
开发 App 的过程中,有一些逻辑是可以共用的,比如登录、网络状况等,最好可以组装起来,就像超能陆战队里的 megabot 一样
基于前面提到的 NSOperation / NSOperationQueue 的一些特点,苹果的工程师们想到了他们的解决方法。
Condition
Condition,也就是条件,它可以被附加到 Operation 上,只有当 Condition 被满足时,Operation 才能被执行。比如只有在有网络的情况下才能进行交易,这时「网络状况」就是附加给「交易」的 Condition。
一个 Condition 主要包含了 3 个方法:
// 1
static var isMutuallyExclusive: Bool { get }
// 2
func dependencyForOperation(operation: Operation) -> NSOperation?
// 3
func evaluateForOperation(operation: Operation, completion: OperationConditionResult -> Void)
- 这个属性用来表明这个 Condtion 是否是排他的,如果是的话,同一时间只能出现一个该类型的实例,类型的指定是通过设置
name
来实现的。 - 为传入的 operation 返回一个依赖的 operation,比如「喜欢」这个 Operation 需要用户已处于登录状态,那么「登录」这个 Condition 的这个方法就可以返回一个「登录」的 Operation。
- 这个方法是查看这个 Condition 的执行结果,比如前面的「登录」Operation 结束后,系统将要执行「喜欢」这个 Operation,然后这个方法就会被触发,如果没有错误发生的话,就执行「喜欢」,如果有错误发生「喜欢」就会自动结束。
所以总结起来 Condition 主要干了这么三件事
来看一个简单的 Condition (来自 WWDC Sample)
struct ReachabilityCondition: OperationCondition {
static let hostKey = "Host"
static let name = "Reachability"
static let isMutuallyExclusive = false
let host: NSURL
// 1
init(host: NSURL) {
self.host = host
}
// 2
func dependencyForOperation(operation: Operation) -> NSOperation? {
return nil
}
func evaluateForOperation(operation: Operation, completion: OperationConditionResult -> Void) {
ReachabilityController.requestReachability(host) { reachable in
if reachable {
// 3
completion(.Satisfied)
}
else {
let error = NSError(code: .ConditionFailed, userInfo: [
OperationConditionKey: self.dynamicType.name,
self.dynamicType.hostKey: self.host
])
// 4
completion(.Failed(error))
}
}
}
}
- Condtion 初始化时可以传参数进来。
- 这个 Condition 没有生成一个
dependencyForOperation
,因为生成依赖 Operation 的目的是当这个 Operation 运行完后,可以在 evaluateForOperation 时获取之前的运行结果,而这里直接调用 ReachabilityController 的 requestReachability 方法就可以了,所以就免去了这一步。 - 当结果符合预期时,调用
completion(.Satisfied)
- 当出现异常时,调用
completion(.Failed(error))
Operation
Operation
继承自 NSOperation
,同时添加了一些方法,主要可以分为 4 部分
- 设置状态变量,同时手动设置 KVO
- 执行 conditions 的
evaluateForOperation
方法 - 添加 Observer
- 添加 Condtion
设置状态变量,同时手动设置 KVO
在系统提供的状态的基础上,又添加了一些新的状态,如 EvaluatingConditions
, Pending
等,这些状态的改变都需要触发内置状态的 KVO,如 isExecuting
, isFinished
, isReady
等。通常的做法会是这样:
[self willChangeValueForKey:@"isExecuting"];
_state = Executing;
[self didChangeValueForKey:@"isExecuting"];
当只有少量的状态改变时,在前后包一层还可以接受,但如果多了的话,就不美观了,这时可以使用 KVO 的一个方法 + keyPathsForValuesAffectingValueForKey:
,它的意思是,哪些 keyPaths 的改变会导致 Key
发生变化。所以可以定义这几个方法,然后正常设置 state
就可以了。
class func keyPathsForValuesAffectingIsReady() -> Set<NSObject> {
return ["state"]
}
class func keyPathsForValuesAffectingIsExecuting() -> Set<NSObject> {
return ["state"]
}
class func keyPathsForValuesAffectingIsFinished() -> Set<NSObject> {
return ["state"]
}
当然,这只是完成了一半,系统知道 state 变了后, isReady
会变,然后就会调用 ready
方法,所以这三个方法我们也要一并覆盖掉。
override var executing: Bool {
return state == .Executing
}
override var finished: Bool {
return state == .Finished
}
override var ready: Bool {
switch state {
case .Pending:
// 省去不相关的代码
if super.ready {
// 1
evaluateConditions()
}
// Until conditions have been evaluated, "isReady" returns false
return false
case .Ready:
return super.ready || cancelled
default:
return false
}
}
- 可以看到,当系统在问某个 Operation 是否 ready 时,
evaluateConditions
方法会被触发,这里包含了该 Operation 的所有 Conditions 的evaluateForOperation
的执行结果。
执行 conditions 的 evaluateForOperation
方法
private func evaluateConditions() {
assert(state == .Pending && !cancelled, "evaluateConditions() was called out-of-order")
state = .EvaluatingConditions
// 1
OperationConditionEvaluator.evaluate(conditions, operation: self) { failures in
self._internalErrors.extend(failures)
self.state = .Ready
}
}
- 遍历当前 Operation 的 conditions,执行它们的
evaluateForOperation
方法,然后将错误保存在_internalErrors
里,同时将当前的状态设置为.Ready
。
或许你会问,如果出现错误,是不是表示条件不满足,如果条件不满足,为什么还要将状态设置为 .Ready
? 这是因为当状态设置为 .Ready
后,就会执行 main
方法,在那里会对 _internalErrors
做统一判断。
override final func main() {
assert(state == .Ready, "This operation must be performed on an operation queue.")
if _internalErrors.isEmpty && !cancelled {
state = .Executing
// 1
for observer in observers {
observer.operationDidStart(self)
}
execute()
}
else {
finish()
}
}
- 这里出现了 observer,当 Operation 处于不同状态时,会调用 observers 的不同方法
添加 Observers
observer 的实现还是比较简单的,首先定义一个 Protocol,所有的 observer 都需要实现这个 Protocol 里的方法,然后 Operation 内置一个数组作为容器,addObserver
时,将 observer 添加到容器,当处于不同状态时,遍历容器里的 observer,调用相应的方法。
这不免让我们想起了 delegate,跟 delegate 相比,observer 的好处就在于可以指定多个观察者,而 delegate 只能指定一个。
添加 Condtions
跟 observer 的实现思路基本一致。你或许会问,添加的这些 Conditions 什么时候会被触发呢?没错,就是在将 Operation 添加到 OperationQueue 时。
OperationQueue
OperationQueue
也是继承自系统的 NSOperationQueue
,同时重写了 addOperation
方法,这个方法主要做了 3 件事
- 给 Operation 添加 observer
- 处理 Operation 的 dependencies 的
dependencyForOperation
- 处理 Operation 的 dependencies 的排他性
给 Operation 添加 observer
let delegate = BlockObserver(
startHandler: nil,
produceHandler: { [weak self] in
// 1
self?.addOperation($1)
},
finishHandler: { [weak self] in
if let q = self {
// 2
q.delegate?.operationQueue?(q, operationDidFinish: $0, withErrors: $1)
}
}
)
op.addObserver(delegate)
- 我们前面说过,一个 Operation 可以生成一个新的 Operation,这个 Operation 生成后也需要被放到 Queue 里,这个放置的过程就是在这个 delegate 里实现的。
- operationQueue 自己有一个 delegate,当 queue 里的一个 operation 执行完时,会向 delegate 报告。
处理 Operation 的 dependencies 的 dependencyForOperation
// Extract any dependencies needed by this operation.
let dependencies = op.conditions.flatMap {
$0.dependencyForOperation(op)
}
for dependency in dependencies {
op.addDependency(dependency)
self.addOperation(dependency)
}
这个就很简单了,调用 dependencyForOperation
方法,拿到 operation,然后将当前的 op 依赖该 operation,同时将这个 operation 放到 queue 里,所以在 conditions 的 operations 执行完之前,op 是不会执行的。
处理 Operation 的 dependencies 的排他性
let concurrencyCategories: [String] = op.conditions.flatMap { condition in
if !condition.dynamicType.isMutuallyExclusive { return nil }
return "\(condition.dynamicType)"
}
if !concurrencyCategories.isEmpty {
// Set up the mutual exclusivity dependencies.
let exclusivityController = ExclusivityController.sharedExclusivityController
exclusivityController.addOperation(op, categories: concurrencyCategories)
op.addObserver(BlockObserver { operation, _ in
exclusivityController.removeOperation(operation, categories: concurrencyCategories)
})
}
在这里可能看不出「排他」的实现,因为是在 exclusivityController
里面实现的,调用了它的 addOperation
方法后,它会去查看这个类型的数组是否为空,如果不为空,就让这个 operation 依赖数组的最后一个。这样在之前的 operation 执行完之前,这个 operation 是不会被执行的。
使用
有了 Operation 和 OperationQueue 之后,就可以开始生产 megabot 了,来看一个「查看原网页」的 Operation,这个 Operation 的作用就是展示传入的 URL。
import Foundation
import SafariServices
/// An `Operation` to display an `NSURL` in an app-modal `SFSafariViewController`.
class MoreInformationOperation: Operation {
let URL: NSURL
init(URL: NSURL) {
self.URL = URL
super.init()
// 1
addCondition(MutuallyExclusive<UIViewController>())
}
override func execute() {
dispatch_async(dispatch_get_main_queue()) {
self.showSafariViewController()
}
}
private func showSafariViewController() {
if let context = UIApplication.sharedApplication().keyWindow?.rootViewController {
let safari = SFSafariViewController(URL: URL, entersReaderIfAvailable: false)
safari.delegate = self
context.presentViewController(safari, animated: true, completion: nil)
}
else {
finish()
}
}
}
extension MoreInformationOperation: SFSafariViewControllerDelegate {
func safariViewControllerDidFinish(controller: SFSafariViewController) {
controller.dismissViewControllerAnimated(true) {
// 2
self.finish()
}
}
}
- 因为这是一个
ViewController
相关的 Operation,所以其他同类型的 Operation,需要等我完成后才能被执行。 - 当这个 controller 被关闭时,表示这个 Operation 结束,调用一下
finish
方法。
如果需要的话,可以给这个 Operation 再加一个 ReachabilityCondition
,当没有网络时就不打开了。
再来看看在 VC 层面的使用。
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
// 1
let operation = BlockOperation {
self.performSegueWithIdentifier("showEarthquake", sender: nil)
}
operation.addCondition(MutuallyExclusive<UIViewController>())
// 2
let blockObserver = BlockObserver { _, errors in
/*
If the operation errored (ex: a condition failed) then the segue
isn't going to happen. We shouldn't leave the row selected.
*/
if !errors.isEmpty {
dispatch_async(dispatch_get_main_queue()) {
tableView.deselectRowAtIndexPath(indexPath, animated: true)
}
}
}
operation.addObserver(blockObserver)
// 3
operationQueue.addOperation(operation)
}
- 类似
NSBlockOperation
,BlockOperation
也可以快速生成一个 Operation。 BlockObserver
也是一个快速生成 observer 的方法,这里描述了当 Operation 完成后的处理。- 调用方需要新建一个 queue,然后把 Operation 放到这个 queue 里。
相比起正常的调用,还是会多了些步骤。
小结
基于 Operation 来架构的思想还是蛮新颖的,可以将复杂的任务拆分成粒度更细的 Operation,然后再组装。但实际使用起来也会有不少问题,比如之前提到的写起来会复杂些,调试时看 backtrace 会很累,不确定是否会带来更好的可维护性等等。不过既然苹果都已经把它用到了线上的 App,至少说明是可行的,至于与已有的架构相比会带来怎样的提升,可能需要实际写起来才知道。