noki雑記

iOS、ときどきAndroid

OperationとOperationQueue

複数の非同期な処理を順に実行する方法を検討していて、OperationQueueを使うと便利そうだなと思い触ってみました。

OperationとOperationQueue

キュー(OperationQueue)の中に処理(Operation)を追加しておくと、先に追加した処理から順に実行され、実行中の処理が終了すると次の処理実行される仕組みです。終了した処理はQueueから取り除かれます。
並列で処理する数を制限したり、「処理Aが終わってから処理Bを実行したい」というような依存関係も設定できます。

非同期で実行する場合

まずはOperationを継承したクラスを用意し、実行したい処理はstart()メソッドをoverrideして中に記述します。また、isConcurrenttrueに設定することで並列に実行することが可能になります。

class Operation: Foundation.Operation {
    override var isConcurrent: Bool {
        return true
    }

    override func start() {
        // 処理
    }
}

非同期処理の終了を検知する

ある処理が終わった後に次の処理を実行するためには、処理が終了したことを検知する必要があります。Operationでは、isFinishedプロパティを監視(KVO)して検知する仕組みです。例えば、適当にStateで状態を管理しつつ、state = .finishedになったらisFinishedの値が変更されたことを通知するようにしています。isFinishedプロパティがread-onlyなため、willChangeValue(forKey:)didChangeValue(forKey:)を使って通知するしかなさそうです。

class Operation: Foundation.Operation {
    enum State {
        case ready, started, finished
    }

    private var state: State = .ready {
        willSet { willChangeValue(forKey: #keyPath(Operation.isFinished)) }
        didSet { didChangeValue(forKey: #keyPath(Operation.isFinished)) }
    }

    override var isFinished: Bool {
        return state == .finished
    }
}

KVOでキーを指定する場合は、#keyPathを使うと便利です。プロパティ名を文字列で指定してしまうと、コンパイルエラーにならなかったりするので面倒です。

キャンセル処理

OperationQueueにはcancelAllOperations()メソッドがあり、キュー内の全てのOperationのcancel()メソッドを呼ぶことが出来ます。ただし、isCanceledプロパティが変更されるだけで、実際にOperationが実行されなくなるわけでも、キューから取り除かれるわけでもありません。キャンセルの処理は自身で記述する必要があります。
また、まだ実行されていないOperationは、キャンセルされた後であればQueueから即座に取り除くことが可能だそうです。

class Operation: Foundation.Operation {
    override func cancel() {
        super.cancel()
        state = .finished
    }
}

Queueに詰める

OperationQueueに詰めると即座に実行されていきます。

class ViewController: UIViewController {
    private lazy var queue: OperationQueue = {
        let queue = OperationQueue()
        queue.maxConcurrentOperationCount = 1  // 同時実行処理を1つに制限
        return queue
    }()

    override func viewDidLoad() {
        super.viewDidLoad()

        (0...10).forEach { (num) in
            let operation = Operation()
            let name = "\(num)"
            operation.name = name
            operation.completionBlock = {
                print("operation finished:", name)
            }
            queue.addOperation(operation)
        }
    }
}

おまけ:KVO

#keyPathはプロパティ変更の通知を受け取った先でも使えます。便利!

    override public func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
        guard let keyPath = keyPath, let change = change else { return }

        if keyPath == #keyPath(Operation.isFinished) {
            guard let old = change[.oldKey] as? Bool,
                  let new = change[.newKey] as? Bool else { return }

            if !old && new {
                print("operation is finished.")
            }
        }
    }