? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?幾種消息傳遞機(jī)制
? ? ? ?首先我們來看看每種機(jī)制的具體特點(diǎn)。在這個基礎(chǔ)上,下一節(jié)我們會畫一個流程圖來幫我們在具體情況下正確選擇應(yīng)該使用的機(jī)制。最后,我們會介紹一些蘋果框架里的例子并且解釋為什么在那些用例中會選擇這樣的機(jī)制。
#KVO
KVO 是提供對象屬性被改變時的通知的機(jī)制。KVO 的實(shí)現(xiàn)在 Foundation 中,很多基于 Foundation 的框架都依賴它。想要了解更多有關(guān) KVO 的最佳實(shí)踐,請閱讀本期 Daniel 寫的 KVO 和 KVC 文章。
如果只對某個對象的值的改變感興趣的話,就可以使用 KVO 消息傳遞。不過有一些前提:第一,接收者(接收對象改變的通知的對象)需要知道發(fā)送者 (值會改變的對象);第二,接收者需要知道發(fā)送者的生命周期,因?yàn)樗枰诎l(fā)送者被銷毀前注銷觀察者身份。如果這兩個要去符合的話,這個消息傳遞機(jī)制可以一對多(多個觀察者可以注冊觀察同一個對象的變化)
如果要在 Core Data 上使用 KVO 的話,方法會有些許差別。這和 Core Data 的惰性加載 (faulting) 機(jī)制有關(guān)。一旦一個 managed object 被惰性加載處理的話,即使它的屬性沒有被改變,它還是會觸發(fā)相應(yīng)的觀察者。
通知
要在代碼中的兩個不相關(guān)的模塊中傳遞消息時,通知機(jī)制是非常好的工具。通知機(jī)制廣播消息,當(dāng)消息內(nèi)容豐富而且無需指望接收者一定要關(guān)注的話這一招特別有用。
通知可以用來發(fā)送任意消息,甚至可以包含一個 userInfo 字典。你也可以繼承 NSNotification 寫一個自己的通知類來自定義行為。通知的獨(dú)特之處在于,發(fā)送者和接收者不需要相互知道對方,所以通知可以被用來在不同的相隔很遠(yuǎn)的模塊之間傳遞消息。這就意味著這種消息傳遞是單向的,我們不能回復(fù)一個通知。
委托 (Delegation)
Delegation 在蘋果的框架中廣泛存在。它讓我們能自定義對象的行為,并收到一些觸發(fā)的事件。要使用 delegation 模式的話,發(fā)送者需要知道接收者,但是反過來沒有要求。因?yàn)榘l(fā)送者只需要知道接收者符合一定的協(xié)議,所以它們兩者結(jié)合的很松。
因?yàn)?delegate 協(xié)議可以定義任何的方法,我們可以照著自己的需求來傳遞消息??梢杂梅椒▍?shù)來傳遞消息內(nèi)容,delegate 可以通過返回值的形式來給發(fā)送者作出回應(yīng)。如果只要在相對接近的兩個模塊間傳遞消息,delgation 是很靈活很直接的消息傳遞機(jī)制。
過度使用 delegation 也會帶來風(fēng)險。如果兩個對象結(jié)合得很緊密,任何其中一個對象都不能單獨(dú)運(yùn)轉(zhuǎn),那么就不需要用 delegate 協(xié)議了。這些情況下,對象已經(jīng)知道各自的類型,可以直接交流。兩個比較新的例子是 UICollectionViewLayout 和 NSURLSessionConfiguration。
Block
Block 是最近才加入 Objective-C 的,首次出現(xiàn)在 OS X 10.6 和 iOS 4 平臺上。Block 通??梢酝耆娲?delegation 消息傳遞機(jī)制的角色。不過這兩種機(jī)制都有它們自己的獨(dú)特需求和優(yōu)勢。
一個不使用 block 的理由通常是 block 會存在導(dǎo)致 retain 環(huán) (retain cycles) 的風(fēng)險。如果發(fā)送者需要 retain block 但又不能確保引用在什么時候被賦值為 nil, 那么所有在 block 內(nèi)對 self 的引用就會發(fā)生潛在的 retain 環(huán)。
假設(shè)我們要實(shí)現(xiàn)一個用 block 回調(diào)而不是 delegate 機(jī)制的 table view 里的選擇方法,如下所示:
self.myTableView.selectionHandler = ^void(NSIndexPath *selectedIndexPath) {
// 處理選擇
};
這兒的問題是,self 會 retain table view,table view 為了讓 block 之后可以使用而又需要 retain 這個 block。然而 table view 不能把這個引用設(shè)為 nil,因?yàn)樗恢朗裁磿r候不需要這個 block 了。如果我們不能保證打破 retain 環(huán)并且我們需要 retain 發(fā)送者,那么 block 就不是一個的好選擇。
NSOperation 是使用 block 的一個好范例。因?yàn)樗谝欢ǖ牡胤酱蚱屏?retain 環(huán),解決了上述的問題。
self.queue = [[NSOperationQueue alloc] init];
MyOperation *operation = [[MyOperation alloc] init];
operation.completionBlock = ^{
[self finishedOperation];
};
[self.queue addOperation:operation];
一眼看來好像上面的代碼有一個 retain 環(huán):self retain 了 queue,queue retain 了 operation, operation retain 了 completionBlock, 而 completionBlock retain 了 self。然而,把 operation 加入 queue 中會使 operation 在某個時間被執(zhí)行,然后被從 queue 中移除。(如果沒被執(zhí)行,問題就大了。)一旦 queue 把 operation 移除,retain 環(huán)就被打破了。
另一個例子是:我們在寫一個視頻編碼器的類,在類里面我們會調(diào)用一個 encodeWithCompletionHandler: 的方法。為了不出問題,我們需要保證編碼器對象在某個時間點(diǎn)會釋放對 block 的引用。其代碼如下所示
@interface Encoder ()
@property (nonatomic, copy) void (^completionHandler)();
@end
@implementation Encoder
- (void)encodeWithCompletionHandler:(void (^)())handler
{
self.completionHandler = handler;
// 進(jìn)行異步處理...
}
// 這個方法會在完成后被調(diào)用一次
- (void)finishedEncoding
{
self.completionHandler();
self.completionHandler = nil; // <- 不要忘了這個!
}
@end
一旦任務(wù)完成,completion block 調(diào)用過了以后,我們就應(yīng)該把它設(shè)為 nil。
如果一個被調(diào)用的方法需要發(fā)送一個一次性的消息作為回復(fù),那么使用 block 是很好的選擇, 因?yàn)檫@樣做我們可以打破潛在的 retain 環(huán)。另外,如果將處理的消息和對消息的調(diào)用放在一起可以增強(qiáng)可讀性的話,我們也很難拒絕使用 block 來進(jìn)行處理。在用例之中,使用 block 來做完成的回調(diào),錯誤的回調(diào),或者類似的事情,是很常見的情況。
? ? ? ? ? ? ? ? ? ? ? ? ? ?Target-Action
Target-Action 是回應(yīng) UI 事件時典型的消息傳遞方式。iOS 上的 UIControl 和 Mac 上的 NSControl/NSCell 都支持這個機(jī)制。Target-Action 在消息的發(fā)送者和接收者之間建立了一個松散的關(guān)系。消息的接收者不知道發(fā)送者,甚至消息的發(fā)送者也不知道消息的接收者會是什么。如果 target 是 nil,action 會在響應(yīng)鏈 (responder chain) 中被傳遞下去,直到找到一個響應(yīng)它的對象。在 iOS 中,每個控件甚至可以和多個 target-action 關(guān)聯(lián)。
基于 target-action 傳遞機(jī)制的一個局限是,發(fā)送的消息不能攜帶自定義的信息。在 Mac 平臺上 action 方法的第一個參數(shù)永遠(yuǎn)接收者。iOS 中,可以選擇性的把發(fā)送者和觸發(fā) action 的事件作為參數(shù)。除此之外就沒有別的控制 action 消息內(nèi)容的方法了。
? ? ? ?
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? 做出正確的選擇
基于上述對不同消息傳遞機(jī)制的特點(diǎn),我們畫了一個流程圖來幫助我們在不同情境下做出不同的選擇。一句忠告:流程圖的建議不代表最終答案。有些時候別的選擇依然能達(dá)到應(yīng)有的效果。只不過大多數(shù)情況下這張圖能引導(dǎo)你做出正確的決定。

Decision flow chart for communication patterns in Cocoa
圖中有些細(xì)節(jié)值得深究:
有個框中說到: 發(fā)送者支持 KVO。這不僅僅是說發(fā)送者會在值改變的時候發(fā)送 KVO 通知,而且說明觀察者需要知道發(fā)送者的生命周期。如果發(fā)送者被存在一個 weak 屬性中,那么發(fā)送者有可能會自己變成 nil,那時觀察者會導(dǎo)致內(nèi)存泄露。
一個在最后一行的框里說,消息直接響應(yīng)方法調(diào)用。也就是說方法調(diào)用的接收者需要給調(diào)用者一個消息作為方法調(diào)用的直接反饋。這也就是說處理消息的代碼和調(diào)用方法的代碼必須在同一個地方。
最后在右下角的地方,一個選擇分支這樣說:發(fā)送者能確保釋放對 block 的引用嗎?這涉及到了我們之前討論 block 的 API 存在潛在的 retain 環(huán)的問題。如果發(fā)送者不能保證在某個時間點(diǎn)會釋放對 block 的引用,那么你會惹上 retain 環(huán)的麻煩。
Framework 示例
本節(jié)我們通過一些蘋果框架里的例子來驗(yàn)證流程圖的選擇是否有道理,同時解釋為什么蘋果會選擇用這些機(jī)制。
? ? ? ? ? ? ? ? ? ? ? ? ? KVO
NSOperationQueue 用了 KVO 觀察隊(duì)列中的 operation 狀態(tài)屬性的改變情況 (isFinished,isExecuting,isCancelled)。當(dāng)狀態(tài)改變的時候,隊(duì)列會收到 KVO 通知。為什么 operation 隊(duì)列要用 KVO 呢?
消息的接收者(operation 隊(duì)列)知道消息的發(fā)送者(operation),并 retain 它并控制后者的生命周期。另外,在這種情況下只需要單向的消息傳遞機(jī)制。當(dāng)然如果考慮到 oepration 隊(duì)列只關(guān)心那些改變 operation 的值的改變情況的話,就還不足以說服大家使用 KVO 了。但我們可以這么理解:被傳遞的消息可以被當(dāng)成值的改變來處理。因?yàn)?state 屬性在 operation 隊(duì)列以外也是有用的,所以這里適合用 KVO。

當(dāng)然 KVO 不是唯一的選擇。我們也可以將 operation 隊(duì)列作為 operation 的 delegate 來使用,operation 會調(diào)用類似 operationDidFinish: 或者 operationDidBeginExecuting: 等方法把它的 state 傳遞給 queue。這樣就不太方便了,因?yàn)?operation 要保存 state 屬性,以便于調(diào)用這些 delegate 方法。另外,由于 queue 不能主動獲取 state 信息,所以 queue 也必須保存所有 operation 的 state。
Notifications
Core Data 使用 notification 傳遞事件(例如一個 managed object context 中的改變————NSManagedObjectContextObjectsDidChangeNotification)
發(fā)生改變時觸發(fā)的 notification 是由 managed object contexts 發(fā)出的,所以我們不能假定消息的接收者知道消息的發(fā)送者。因?yàn)橄⒌脑搭^不是一個 UI 事件,很多接收者可能在關(guān)注著此消息,并且消息傳遞是單向的,所以 notification 是唯一可行的選擇。

Delegation
Table view 的 delegate 有多重功能,它可以從管理 accessory view,直到追蹤在屏幕上顯示的 cell。例如我們可以看看 tableView:didSelectRowAtIndexPath: 方法。為什么用 delegate 實(shí)現(xiàn)而不是 target-action 機(jī)制?
正如我們在上述流程圖中看到的,用 target-action 時,不能傳遞自定義的數(shù)據(jù)。而選中 table view 的某個 cell 時,collection view 不僅需要告訴我們一個 cell 被選中了,也要通過 index path 告訴我們哪個 cell 被選中了。如果我們照著這個思路,流程圖會引導(dǎo)我們使用 delegation 機(jī)制。

如果不在消息傳遞中包含選中 cell 的 index path,而是讓選中項(xiàng)改變時我們像 table view 主動詢問并獲取選中 cell 的相關(guān)信息,會怎樣呢?這會非常不方便,因?yàn)槲覀儽仨氂涀‘?dāng)前選中項(xiàng)的數(shù)據(jù),這樣才能在多選擇中知道哪些 cell 是被新選中的。
同理,我們可以想象通過觀察 table view 選中項(xiàng)的 index path 屬性,當(dāng)該值發(fā)生改變的時候,獲得一個選中項(xiàng)改變的通知。不過我們會遇到上述相似問題:不做記錄的話我們就不能分辨哪一個 cell 被選擇或取消選擇了。
Block
我們用 -[NSURLSession dataTaskWithURL:completionHandler:] 來作為一個 block API 的介紹。那么從 URL 加載部分返回給調(diào)用者是怎么傳遞消息的呢?首先,作為 API 的調(diào)用者,我們知道消息的發(fā)送者,但是我們并沒有 retain 它。另外,這是個單向的消息傳遞————它直接調(diào)用 dataTaskWithURL: 的方法。如果我們對照流程圖,會發(fā)現(xiàn)這屬于 block 消息傳遞機(jī)制。
有其他的選項(xiàng)嗎?當(dāng)然,蘋果自己的 NSURLConnection 就是最好的例子。NSURLConnection在 block 問世之前就存在了,所以它并沒有用 block 來實(shí)現(xiàn)消息傳遞,而是使用 delegation 來完成。當(dāng) block 出現(xiàn)以后,蘋果就在 OS X 10.7 和 iOS 5 平臺上的 NSURLConnection 中加了 sendAsynchronousRequest:queue:completionHandler:,所以我們不再在簡單的任務(wù)中使用 delegate 了。
因?yàn)?NSURLSession 是個最近在 OS X 10.9 和 iOS 7 才出現(xiàn)的 API,所以它們使用 block 來實(shí)現(xiàn)消息傳遞機(jī)制(NSURLSession 有一個 delegate,但是是用于其他目的)。
Target-Action
一個明顯的 target-action 用例是按鈕。按鈕在不被按下的時候不需要發(fā)送任何的信息。為了這個目的,target-action 是 UI 中消息傳遞的最佳選擇。

如果 target 是明確指定的,那么 action 消息會發(fā)送給指定的對象。如果 target 是 nil, action 消息會一直在響應(yīng)鏈中被傳遞下去,直到找到一個能處理它的對象。在這種情況下,我們有一個完全解耦的消息傳遞機(jī)制:發(fā)送者不需要知道接收者,反之亦然。
Target-action 機(jī)制非常適合響應(yīng) UI 的事件。沒有其他的消息傳遞機(jī)制能夠提供相同的功能。雖然 notification 在發(fā)送者和接收者的松散關(guān)系上最接近它,但是 target-action 可以用于響應(yīng)鏈——只有一個對象獲得 action 并響應(yīng),action 在響應(yīng)鏈中傳遞,直到能遇到響應(yīng)這個 action 的對象。
? ? ? ? ? ? ? ? 總結(jié)
一開始接觸這么多的消息傳遞機(jī)制的時候,我們可能有些無所適從,覺得所有的機(jī)制都可以被選用。不過一旦我們仔細(xì)分析每個機(jī)制的時候,它們各自都有特殊的要求和能力。