之前做 iOS 的并發(fā)編程的時(shí)候一直用的 GCD纯赎,NSOperation 用得很少,這個(gè)類對(duì)我來(lái)說(shuō)一直比較神秘飘千,于是最近仔細(xì)研究了一下染坯。注意這篇文章并不會(huì)很詳細(xì)講到各個(gè)方面羹应,只是解答一些剛開始接觸 NSOperation 時(shí)會(huì)遇到的疑惑掀鹅。
我們?cè)谑褂?GCD 的時(shí)候散休,通常會(huì)將 block dispatch 到其他線程異步執(zhí)行,就像這樣:
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)) {
// some task you want to perform asynchronously on other thread
}
這里的 block 就是我們希望異步執(zhí)行的代碼乐尊,這段代碼被當(dāng)做參數(shù)傳入 dispatch_async 函數(shù)戚丸,然后它為我們開啟一個(gè)新的線程,用 dispatch queue 來(lái)管理 block 的執(zhí)行順序扔嵌。
其實(shí) NSOperation 就是對(duì)我們需要異步執(zhí)行的代碼的面向?qū)ο蟮姆庋b限府,上面的代碼換作 NSOperation 會(huì)這么寫:
let queue = NSOperationQueue()
queue.addOperationWithBlock {
// some task you want to perform asynchronously on other thread
}
你會(huì)發(fā)現(xiàn)上面的代碼根本沒(méi)有出現(xiàn) NSOperation,而是出現(xiàn)了 NSOperationQueue痢缎,這是為什么呢胁勺?
因?yàn)榍懊嬉呀?jīng)說(shuō)過(guò),NSOperation 只是對(duì)執(zhí)行代碼的封裝独旷,本身并不提供任何的異步功能署穗,NSOperationQueue 底層是基于 GCD 的封裝,所以一般來(lái)說(shuō)嵌洼,我們會(huì)用 NSOperationQueue 來(lái)控制 operation 的執(zhí)行案疲,queue 會(huì)根據(jù) operation 的優(yōu)先級(jí)、依賴等來(lái)決定如何執(zhí)行添加進(jìn) queue 的 operation麻养。
之前的代碼我們用了 NSOperationQueue 的方法 addOpertionWithBlock 來(lái)給 queue 方便的添加 operation褐啡,接下來(lái)我們來(lái)使用 NSOperation,但是 NSOperation 是一個(gè)“抽象”類(注意這里的抽象是指它功能上的抽象鳖昌,OC 和 Swift 中并沒(méi)有真正的抽象類的概念)备畦,所以你只能通過(guò)它的子類來(lái)使用它低飒,系統(tǒng)已經(jīng)提供了兩個(gè),NSBlockOperation 和 NSInvocationOperation(Swift 不支持后者)萍恕,使用起來(lái)就像下面這樣:
let queue = NSOperationQueue()
let operation = NSBlockOperation {
// ...
}
queue.addOperation(operation)
使用 NSOperation 的好處是逸嘀,你可以指定 operation 的優(yōu)先級(jí)和依賴车要,當(dāng)多個(gè) operaton 添加到 queue 時(shí)允粤,queue 可以根據(jù)這些來(lái)確定執(zhí)行順序。
當(dāng)然翼岁,你也可以自定義 NSOperation 的子類类垫,你可以選擇定義同步的子類或者異步的子類。系統(tǒng)提供的兩個(gè)子類都是同步的琅坡,你可以通過(guò)打印他們的 asynchronous 屬性來(lái)查看悉患。
為什么還需要定義異步的 NSOperation 子類呢?
之前沒(méi)有提到的是榆俺,NSOperation 不僅可以通過(guò)添加到 queue 里面執(zhí)行售躁,還可以通過(guò) start 方法執(zhí)行,就像這樣:
let operation = NSBlockOperation {
// ...
}
operation.start()
對(duì)于上面這段代碼茴晋,這樣寫基本是沒(méi)有意義的陪捷,因?yàn)檫@段代碼還是會(huì)在主線程執(zhí)行,跟直接執(zhí)行 block 里面的代碼沒(méi)有區(qū)別诺擅。
通過(guò)官方文檔可以知道市袖,如果你只想要一個(gè)同步的 NSOperation 的子類,你只需要重寫 main 方法就行了烁涌,但是苍碟,要想讓它異步,你需要干的事情更多撮执。
如何實(shí)現(xiàn)我們待會(huì)再說(shuō)微峰,先再多說(shuō)一點(diǎn)關(guān)于 NSOperation 和 NSOperationQueue 的關(guān)系。
之前提到 queue 能控制 operation 的執(zhí)行順序抒钱,它還能控制它開啟的線程最多能執(zhí)行的 operation 的個(gè)數(shù)蜓肆。那么 queue 是如何知道 operation 是什么時(shí)候開始執(zhí)行,什么執(zhí)行完成继效,什么時(shí)候被取消的呢症杏?
這與 operation 維護(hù)的狀態(tài)機(jī)有關(guān),operation 有四個(gè)布爾屬性瑞信,它們的狀態(tài)可以用下面這張圖表示:
當(dāng)一個(gè) operation 的所有依賴都完成執(zhí)行后厉颤,它就會(huì)進(jìn)入 ready 狀態(tài),準(zhǔn)備開始執(zhí)行了凡简,但是如果此時(shí)它已經(jīng)被 cancel 了逼友,那么它不會(huì)開始執(zhí)行而是直接進(jìn)入 finished 狀態(tài)精肃,反之那么它將進(jìn)入 executing 狀態(tài),開始執(zhí)行帜乞,執(zhí)行結(jié)束后進(jìn)入 finished 狀態(tài)司抱。
這里多說(shuō)一下 NSOperation 的 cancel 方法,這個(gè)方法只會(huì)將它的 isCanceled 屬性變?yōu)?true黎烈,其他的什么也不會(huì)做习柠。也就是說(shuō),如果這個(gè) operation 已經(jīng)開始執(zhí)行了的話照棋,那么只有等到 operation 的代碼執(zhí)行結(jié)束后它的 state 才會(huì)變?yōu)?finished资溃。
queue 就是根據(jù)這些狀態(tài)量以及優(yōu)先級(jí)和其他因素來(lái)確定 operation 的執(zhí)行順序的。operation 用到了 iOS 中一個(gè)比較重要的黑科技 KVO 來(lái)通知 queue 自身狀態(tài)的改變烈炭,可以猜想的是溶锭,在 NSOperation 的默認(rèn)實(shí)現(xiàn)中,比如 start 方法中符隙,肯定含有不少的 KVO 代碼趴捅。
注意能被 KVO 觀察的屬性一定是 KVC 的,因?yàn)檫@里的狀態(tài)屬性都是布爾類型霹疫,按照蘋果的風(fēng)格拱绑,這些屬性的 getter 方法的前面都有 is 前綴,所以更米,這些屬性的 keypath 都是 is 開頭的欺栗。
回到如何實(shí)現(xiàn)異步的 NSOperation 上來(lái),官方的文檔說(shuō)了三點(diǎn):重寫 start 方法征峦,在合適的地方更新 executing 和 finished 的值并且產(chǎn)生 KVO 通知迟几,重寫 asynchronous 返回 true。
這里說(shuō)一下 start 方法和 main 方法栏笆,start 方法是 operation 開始的入口类腮,main 方法一般放具體需要執(zhí)行的代碼,在 NSOperation 的默認(rèn)實(shí)現(xiàn)里蛉加,start 方法會(huì)調(diào)用 main 方法蚜枢。
因?yàn)橹貙懥?start 方法,所以 KVO 通知必須由你自己來(lái)管理针饥,官方文檔里是通過(guò)兩個(gè) ivar 來(lái)實(shí)現(xiàn)狀態(tài)值的更新厂抽。
@interface MyOperation : NSOperation {
BOOL executing;
BOOL finished;
}
既然要實(shí)現(xiàn)異步的 NSOperation,那么就必然要把需要執(zhí)行的代碼放到其他線程去執(zhí)行丁眼,官方文檔里直接把 main 方法 detach 到一個(gè)新的線程去執(zhí)行筷凤。
值得注意的是,main 方法并沒(méi)有特別的作用,你完全可以寫一個(gè)其他的方法來(lái)寫你需要移步執(zhí)行的代碼藐守。比如下面提到的 AFN 2.0 里面就沒(méi)有用到 main 方法挪丢,而是自己寫了operationDidStart 來(lái)做工作。
[NSThread detachNewThreadSelector:@selector(main) toTarget:self withObject:nil];
除了官方文檔的這種實(shí)現(xiàn)方法外卢厂,業(yè)內(nèi)用的最多的是用一個(gè)枚舉來(lái)標(biāo)示狀態(tài)乾蓬,這里用 Swift 來(lái)演示:
public class ConcurrentOperation: NSOperation {
public enum State: String {
case Ready, Executing, Finished
private var keyPath: String {
return "is" + rawValue
}
}
public var state = State.Ready {
willSet {
willChangeValueForKey(newValue.keyPath)
willChangeValueForKey(state.keyPath)
}
didSet {
didChangeValueForKey(oldValue.keyPath)
didChangeValueForKey(state.keyPath)
}
}
}
可以看到這樣帶來(lái)的效果很顯著,在任何地方更新 state 屬性都會(huì)發(fā)出相應(yīng)的 KVO 通知慎恒。
OC 的版本可以去看 AFNetworking 2.x 的 AFURLConnectionOperation 的實(shí)現(xiàn)任内。另外,蘋果在 wwdc 2015 session 226 的這個(gè) demo 有關(guān)于異步 NSOperation 更完善復(fù)雜的封裝巧号。
講到這里族奢,相信你對(duì) NSOperation 里面的一些疑惑點(diǎn)有了一些基本的認(rèn)識(shí),接下來(lái)更深入的使用丹鸿,可以去看看官方文檔或其他文章。