iOS - Advanced NSOperations

2019-07-05

看完 WWDC 2015 - Advanced NSOperations 以及 Sample Code 了解了他的强大,并不仅仅用在多线程中,并总结一下收获。

正如 WWDC 演讲所说,他们在 WWDC 的应用中重度使用了 NSOperation 来构建他们的应用,将基本操作都封装成一个个 Operation,比如收藏操作、登录操作、展示提示框(没错,还可以用于 UI 展示)等,利用 NSOperation 、NSOperationQueue 的相关特性进行管理各个操作之间的关系。

比如下面就是 WWDC 应用启动的一个流程,后面的操作都依赖与前面的操作完成,通过 NSOperation 添加 Dependency 就可以很好的确保这些操作按顺序执行。提取了代码中的逻辑,将逻辑加到 NSOperation 中,简化逻辑变化。

1

我们知道 NSOpration 相比 GCD 在以下优势

  • 状态管理
  • 通过 Dependency 添加依赖
  • 控制并发量

在 Sample Code 中,很好的演示了一个地震信息查询的应用是如何通过 NSOperation 驱动的,看完代码确实学到不少东西,比如 NSOperation 的状态管理,合理的使用 Dependency。

1. 自定义 Operation

有时候我们想自定义一个 Operation,并且 管理它的状态,如果无需管理状态,那么直接重写 main 方法添加自己的操作即可。比如一个网络请求的 Operation,在 add 到 Queue 中执行请求,在请求完成后设置状态为 finished,并执行后面的 Operation,那么可以参考 Defining a Custom Operation Object,一个基本的结构如下。

这里有几点是必须的

  • 重写 isExcuting 属性
  • 重写 isFinished 属性
  • 重写 main 方法

同时看到有些地方说以下内容也需要重写,实际是无需重写的

  • start 方法,可以使用用默认的实现,start 方法的主要作用就是在 Operation ready 时作为入口函数,会做一些判断,确定是否要执行 main 方法。如果要重写可以参考下面实现。

  • isConcurrent 属性,iOS 7.0 之后用 isAsynchronous 替代,这个属性的作用是直接用 start 方法启动 Operation,而不是 add 到 Queue 中时起作用。为 false 时会在当前线程执行,为 true 会在一个新的线程中执行

还有一个如果想要控制 isReady 状态的话,不能像 isExcuting 和 isFinished 那样我们直接控制,我们需要合并 super.isReady 的值, super.isReady 的值是有当前 Operation 是否有 dependency 决定的。我们假设我们有 op1 依赖与 op2,我们不考虑 op1 的 super.isReady 而直接将 op1 的 isReady 按照自己的逻辑返回 true,那么此时即便 op2 未完成,因为 op1 的 isReady 为 true,op1 还是会执行,和预期是不一致的。

Operation 的状态管理是通过 KVO 实现的,比如要将设置 isFinished 为 true,会通过 willChangeValuedidChangeValue 触发一个 KVO,那么 OperationQueue 观察到 isFinished 属性改变了,会获取 isFinished 的当前值,发现为 true,那么就会结束当前 Operation,执行后面的 Operation。

class MyOperation: Operation {
    
    var myExcuting = false
    var myFinished = false

    override var isExecuting: Bool {
        return myExcuting
    }
    
    override var isFinished: Bool {
        return myFinished
    }
    
//    override var isConcurrent: Bool {
//        return true
//    }
    
    override func start() {
        // Always check for cancellation before launching the task.
        if(self.isCancelled) {
            // Must move the operation to the finished state if it is canceled.
            self.willChangeValue(forKey: "isFinished")
            myFinished = true
            self.didChangeValue(forKey: "isFinished")
            return
        }
        
        // If the operation is not canceled, begin executing the task.
        self.willChangeValue(forKey: "isExecuting")
        Thread.detachNewThreadSelector(#selector(main), toTarget: self, with: nil)
        myExcuting = true
        self.didChangeValue(forKey: "isExecuting")
    }
    
    override func main() {
        // TODO 添加网络请求等操作
        // 在任务执行完毕调用 completeOperation() 
        completeOperation()
    }
    
    func completeOperation() {
        self.willChangeValue(forKey: "isFinished")
        self.willChangeValue(forKey: "isExecuting")
        
        myExcuting = false
        myFinished = true
        
        self.didChangeValue(forKey: "isExecuting")
        self.didChangeValue(forKey: "isFinished")
    }
}

2. Operation 状态管理

在 NSOperation 中原有的有四种状态

  1. isReady
  2. isExcuting
  3. isFinished
  4. isCancelled

但是在 Sample Code 中却定义了以下状态,主要是为了方便为每个 Operation 进入 ready 状态前执行一些前置条件(Condition)

fileprivate enum State: Int {
    case initialized
    case pending
    case evaluatingConditions
    case ready
    case executing
    case finishing
    case finished
}

我们先不管上面的七个状态,主要看看原本自带的四种状态

isReady

isReady 状态用来标记 Operation 是否可以开始执行了。isReady 的值取决于其依赖的 Operations 是否执行完毕,如果有依赖的 Operation 未执行完毕,那么 isReady 为 false。只有 isReady 为 true,才能执行 Operation 的 start 方法。对于想要自己控制 isReady 状态的话,我们需要合并 super.isReady 的值,原因在上面也提到了

比如我们看 Sample Code 中的一段代码,对于 isReady 的值设计到 super.isReadyisCancelled,为啥和 isCancelled 有关呢,下面说

override var isReady: Bool {
    switch state {
    // ...
    case .ready:
        return super.isReady || isCancelled
    default:
        return false
    }
}

isExcuting

就是字面意思,用于判断当前 main 中的任务是否正在执行

isFinished

用于判断任务是否执行完毕,通过 KVO 设置 Operation 的 isFinished 为 true 可以使 OperationQueue 移除当前 Operation 并执行后面的 Operation

isCancelled

  • 当 Operation 还未执行时,比如说依赖其他 Operation 正在等待的时候,此时调用 cancel() 方法时,会将 isCancelled 设置为 true,isReady 设置为 true。因为 isReady 为 true 了,因此也无需等待其依赖的 Operation 执行完毕,会执行 start 方法,我们看1.自定义 Operation 中 start 的实现,会判断 isCancelled 是否为 true,为 true 就不执行 mian 方法了,也就将取消掉了当前 Operation
  • 对于已经执行的 Operation,调用 cancel() 方法并不会将当前 Operation 取消,它只会将 isCancelled 标记为 true,那么对于 main 中的任务的取消需要我们手动根据 isCancelled 去取消。同时取消了任务后,还是需要将 isFinished 标记为 true

3. Operation 条件控制

在 Sample Code 中,BaseOperation 有个属性 conditions,声明了一个 Operation 执行前的条件

fileprivate(set) var conditions = [OperationCondition]()
protocol OperationCondition {
    static var name: String { get }
    static var isMutuallyExclusive: Bool { get }
    
    func dependency(for operation: BaseOperation) -> Operation?
    func evaluate(for operation: BaseOperation, completion: @escaping (OperationConditionResult) -> ())
}
  • name

    对于每一个实现了该协议的类都返回一个 name

  • isMutuallyExclusive

    返回是否是否互斥。对于两个 Operation op1 和 op2,如果 op1 和 op2 都含有一个 name 相同的 condition,并且 condition 的 isMutuallyExclusive 为 true。那么在 OperationQueue 将 op1 和 op2 加入 Queue 时,会自动将 op2 设置为 op1 的依赖,那么 op1 和 op2 就形成一个互斥的操作了

  • func dependency(for operation: BaseOperation) -> Operation?

    这里返回所有需要依赖的 Operation,会在一个 Operation 加入 Operation 时自动为其添加依赖

  • func evaluate(for operation: BaseOperation, completion: @escaping (OperationConditionResult) -> ())

    这里定义的操作会在 Operation 变成 Ready 状态之前执行

对于一些需要在 Operation 完成后额外添加的的任务,比如对于一个网络请求 Operation 失败的时候执行一个 弹框警告的 Operation 的时候。

Sample Code 通过给 BaseOperation 添加 OperationObserver 来实现

protocol OperationObserver {
    func operation(didStart operation: BaseOperation)
    func operation(_ operation: BaseOperation, didProduce newOperation: Operation)
    func operation(didFinish operation: BaseOperation, errors: [NSError])
}

在 Operation 添加至 OperationQueue 的时候,会创建一个 OperationObserver 来观察 operation 的执行过程,当 Operation didProduce newOperation 时,会将其将如 Queue 中,保证任务的连贯性。

class BaseOperationQueue: OperationQueue {
    weak var delegate: BaseOperationQueueDelegate?
    
    override func addOperation(_ op: Operation) {
        if let op = op as? BaseOperation {
            let delegate = BlockObserver(startHandler: nil,
                                         produceHandler: { [weak self] in
                                            self?.addOperation($1)
                },
                                         finishHandler: { [weak self] in
                                            if let q = self {
                                                q.delegate?.operationQueue?(q, didFinish: $0, with: $1)
                                            }
            })
            
            op.add(delegate)
           // ......
    }
}

Sample Code 主要就是通过以上两种方式来管理各个 Operation 之间的逻辑的。

通过看 Sample Code 的实现,以及 WWDC 中的介绍,我们还是能学习到如何通过 Operation 去构建一个应用或者说去实现某些逻辑性复杂的功能。