iOS開發--Advanced NSOperations

前言

這篇文章是對 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)
  1. 這個屬性用來表明這個 Condtion 是否是排他的,如果是的話,同一時間只能出現一個該類型的實例,類型的指定是通過設置 name 來實現的。
  2. 爲傳入的 operation 返回一個依賴的 operation,比如「喜歡」這個 Operation 需要用戶已處於登錄狀態,那麼「登錄」這個 Condition 的這個方法就可以返回一個「登錄」的 Operation。
  3. 這個方法是查看這個 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))
            }
        }
    }
}
  1. Condtion 初始化時可以傳參數進來。
  2. 這個 Condition 沒有生成一個 dependencyForOperation,因爲生成依賴 Operation 的目的是當這個 Operation 運行完後,可以在 evaluateForOperation 時獲取之前的運行結果,而這裏直接調用 ReachabilityController 的 requestReachability 方法就可以了,所以就免去了這一步。
  3. 當結果符合預期時,調用 completion(.Satisfied)
  4. 當出現異常時,調用 completion(.Failed(error))

Operation

Operation 繼承自 NSOperation,同時添加了一些方法,主要可以分爲 4 部分

  • 設置狀態變量,同時手動設置 KVO
  • 執行 conditions 的 evaluateForOperation 方法
  • 添加 Observer
  • 添加 Condtion
設置狀態變量,同時手動設置 KVO

在系統提供的狀態的基礎上,又添加了一些新的狀態,如 EvaluatingConditionsPending 等,這些狀態的改變都需要觸發內置狀態的 KVO,如 isExecutingisFinishedisReady 等。通常的做法會是這樣:

[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
    }
}
  1. 可以看到,當系統在問某個 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
    }
}
  1. 遍歷當前 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()
    }
}
  1. 這裏出現了 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)
  1. 我們前面說過,一個 Operation 可以生成一個新的 Operation,這個 Operation 生成後也需要被放到 Queue 裏,這個放置的過程就是在這個 delegate 裏實現的。
  2. 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()
        }
    }
}
  1. 因爲這是一個 ViewController 相關的 Operation,所以其他同類型的 Operation,需要等我完成後才能被執行。
  2. 當這個 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)
}
  1. 類似 NSBlockOperation, BlockOperation 也可以快速生成一個 Operation。
  2. BlockObserver 也是一個快速生成 observer 的方法,這裏描述了當 Operation 完成後的處理。
  3. 調用方需要新建一個 queue,然後把 Operation 放到這個 queue 裏。

相比起正常的調用,還是會多了些步驟。

小結

基於 Operation 來架構的思想還是蠻新穎的,可以將複雜的任務拆分成粒度更細的 Operation,然後再組裝。但實際使用起來也會有不少問題,比如之前提到的寫起來會複雜些,調試時看 backtrace 會很累,不確定是否會帶來更好的可維護性等等。不過既然蘋果都已經把它用到了線上的 App,至少說明是可行的,至於與已有的架構相比會帶來怎樣的提升,可能需要實際寫起來才知道。


--EOF--

本文轉載自: http://limboy.me/ios/2015/08/08/advanced-nsoperations.html

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章