前言
這篇文章是對 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,至少說明是可行的,至於與已有的架構相比會帶來怎樣的提升,可能需要實際寫起來才知道。
--EOF--
本文轉載自: http://limboy.me/ios/2015/08/08/advanced-nsoperations.html