? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?幾種消息傳遞機制
? ? ? ?首先我們來看看每種機制的具體特點。在這個基礎上,下一節我們會畫一個流程圖來幫我們在具體情況下正確選擇應該使用的機制。最后,我們會介紹一些蘋果框架里的例子并且解釋為什么在那些用例中會選擇這樣的機制。
#KVO
KVO 是提供對象屬性被改變時的通知的機制。KVO 的實現在 Foundation 中,很多基于 Foundation 的框架都依賴它。想要了解更多有關 KVO 的最佳實踐,請閱讀本期 Daniel 寫的 KVO 和 KVC 文章。
如果只對某個對象的值的改變感興趣的話,就可以使用 KVO 消息傳遞。不過有一些前提:第一,接收者(接收對象改變的通知的對象)需要知道發送者 (值會改變的對象);第二,接收者需要知道發送者的生命周期,因為它需要在發送者被銷毀前注銷觀察者身份。如果這兩個要去符合的話,這個消息傳遞機制可以一對多(多個觀察者可以注冊觀察同一個對象的變化)
如果要在 Core Data 上使用 KVO 的話,方法會有些許差別。這和 Core Data 的惰性加載 (faulting) 機制有關。一旦一個 managed object 被惰性加載處理的話,即使它的屬性沒有被改變,它還是會觸發相應的觀察者。
通知
要在代碼中的兩個不相關的模塊中傳遞消息時,通知機制是非常好的工具。通知機制廣播消息,當消息內容豐富而且無需指望接收者一定要關注的話這一招特別有用。
通知可以用來發送任意消息,甚至可以包含一個 userInfo 字典。你也可以繼承 NSNotification 寫一個自己的通知類來自定義行為。通知的獨特之處在于,發送者和接收者不需要相互知道對方,所以通知可以被用來在不同的相隔很遠的模塊之間傳遞消息。這就意味著這種消息傳遞是單向的,我們不能回復一個通知。
委托 (Delegation)
Delegation 在蘋果的框架中廣泛存在。它讓我們能自定義對象的行為,并收到一些觸發的事件。要使用 delegation 模式的話,發送者需要知道接收者,但是反過來沒有要求。因為發送者只需要知道接收者符合一定的協議,所以它們兩者結合的很松。
因為 delegate 協議可以定義任何的方法,我們可以照著自己的需求來傳遞消息。可以用方法參數來傳遞消息內容,delegate 可以通過返回值的形式來給發送者作出回應。如果只要在相對接近的兩個模塊間傳遞消息,delgation 是很靈活很直接的消息傳遞機制。
過度使用 delegation 也會帶來風險。如果兩個對象結合得很緊密,任何其中一個對象都不能單獨運轉,那么就不需要用 delegate 協議了。這些情況下,對象已經知道各自的類型,可以直接交流。兩個比較新的例子是 UICollectionViewLayout 和 NSURLSessionConfiguration。
Block
Block 是最近才加入 Objective-C 的,首次出現在 OS X 10.6 和 iOS 4 平臺上。Block 通??梢酝耆娲?delegation 消息傳遞機制的角色。不過這兩種機制都有它們自己的獨特需求和優勢。
一個不使用 block 的理由通常是 block 會存在導致 retain 環 (retain cycles) 的風險。如果發送者需要 retain block 但又不能確保引用在什么時候被賦值為 nil, 那么所有在 block 內對 self 的引用就會發生潛在的 retain 環。
假設我們要實現一個用 block 回調而不是 delegate 機制的 table view 里的選擇方法,如下所示:
self.myTableView.selectionHandler = ^void(NSIndexPath *selectedIndexPath) {
// 處理選擇
};
這兒的問題是,self 會 retain table view,table view 為了讓 block 之后可以使用而又需要 retain 這個 block。然而 table view 不能把這個引用設為 nil,因為它不知道什么時候不需要這個 block 了。如果我們不能保證打破 retain 環并且我們需要 retain 發送者,那么 block 就不是一個的好選擇。
NSOperation 是使用 block 的一個好范例。因為它在一定的地方打破了 retain 環,解決了上述的問題。
self.queue = [[NSOperationQueue alloc] init];
MyOperation *operation = [[MyOperation alloc] init];
operation.completionBlock = ^{
[self finishedOperation];
};
[self.queue addOperation:operation];
一眼看來好像上面的代碼有一個 retain 環:self retain 了 queue,queue retain 了 operation, operation retain 了 completionBlock, 而 completionBlock retain 了 self。然而,把 operation 加入 queue 中會使 operation 在某個時間被執行,然后被從 queue 中移除。(如果沒被執行,問題就大了。)一旦 queue 把 operation 移除,retain 環就被打破了。
另一個例子是:我們在寫一個視頻編碼器的類,在類里面我們會調用一個 encodeWithCompletionHandler: 的方法。為了不出問題,我們需要保證編碼器對象在某個時間點會釋放對 block 的引用。其代碼如下所示
@interface Encoder ()
@property (nonatomic, copy) void (^completionHandler)();
@end
@implementation Encoder
- (void)encodeWithCompletionHandler:(void (^)())handler
{
self.completionHandler = handler;
// 進行異步處理...
}
// 這個方法會在完成后被調用一次
- (void)finishedEncoding
{
self.completionHandler();
self.completionHandler = nil; // <- 不要忘了這個!
}
@end
一旦任務完成,completion block 調用過了以后,我們就應該把它設為 nil。
如果一個被調用的方法需要發送一個一次性的消息作為回復,那么使用 block 是很好的選擇, 因為這樣做我們可以打破潛在的 retain 環。另外,如果將處理的消息和對消息的調用放在一起可以增強可讀性的話,我們也很難拒絕使用 block 來進行處理。在用例之中,使用 block 來做完成的回調,錯誤的回調,或者類似的事情,是很常見的情況。
? ? ? ? ? ? ? ? ? ? ? ? ? ?Target-Action
Target-Action 是回應 UI 事件時典型的消息傳遞方式。iOS 上的 UIControl 和 Mac 上的 NSControl/NSCell 都支持這個機制。Target-Action 在消息的發送者和接收者之間建立了一個松散的關系。消息的接收者不知道發送者,甚至消息的發送者也不知道消息的接收者會是什么。如果 target 是 nil,action 會在響應鏈 (responder chain) 中被傳遞下去,直到找到一個響應它的對象。在 iOS 中,每個控件甚至可以和多個 target-action 關聯。
基于 target-action 傳遞機制的一個局限是,發送的消息不能攜帶自定義的信息。在 Mac 平臺上 action 方法的第一個參數永遠接收者。iOS 中,可以選擇性的把發送者和觸發 action 的事件作為參數。除此之外就沒有別的控制 action 消息內容的方法了。
? ? ? ?
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? 做出正確的選擇
基于上述對不同消息傳遞機制的特點,我們畫了一個流程圖來幫助我們在不同情境下做出不同的選擇。一句忠告:流程圖的建議不代表最終答案。有些時候別的選擇依然能達到應有的效果。只不過大多數情況下這張圖能引導你做出正確的決定。
Decision flow chart for communication patterns in Cocoa
圖中有些細節值得深究:
有個框中說到: 發送者支持 KVO。這不僅僅是說發送者會在值改變的時候發送 KVO 通知,而且說明觀察者需要知道發送者的生命周期。如果發送者被存在一個 weak 屬性中,那么發送者有可能會自己變成 nil,那時觀察者會導致內存泄露。
一個在最后一行的框里說,消息直接響應方法調用。也就是說方法調用的接收者需要給調用者一個消息作為方法調用的直接反饋。這也就是說處理消息的代碼和調用方法的代碼必須在同一個地方。
最后在右下角的地方,一個選擇分支這樣說:發送者能確保釋放對 block 的引用嗎?這涉及到了我們之前討論 block 的 API 存在潛在的 retain 環的問題。如果發送者不能保證在某個時間點會釋放對 block 的引用,那么你會惹上 retain 環的麻煩。
Framework 示例
本節我們通過一些蘋果框架里的例子來驗證流程圖的選擇是否有道理,同時解釋為什么蘋果會選擇用這些機制。
? ? ? ? ? ? ? ? ? ? ? ? ? KVO
NSOperationQueue 用了 KVO 觀察隊列中的 operation 狀態屬性的改變情況 (isFinished,isExecuting,isCancelled)。當狀態改變的時候,隊列會收到 KVO 通知。為什么 operation 隊列要用 KVO 呢?
消息的接收者(operation 隊列)知道消息的發送者(operation),并 retain 它并控制后者的生命周期。另外,在這種情況下只需要單向的消息傳遞機制。當然如果考慮到 oepration 隊列只關心那些改變 operation 的值的改變情況的話,就還不足以說服大家使用 KVO 了。但我們可以這么理解:被傳遞的消息可以被當成值的改變來處理。因為 state 屬性在 operation 隊列以外也是有用的,所以這里適合用 KVO。
當然 KVO 不是唯一的選擇。我們也可以將 operation 隊列作為 operation 的 delegate 來使用,operation 會調用類似 operationDidFinish: 或者 operationDidBeginExecuting: 等方法把它的 state 傳遞給 queue。這樣就不太方便了,因為 operation 要保存 state 屬性,以便于調用這些 delegate 方法。另外,由于 queue 不能主動獲取 state 信息,所以 queue 也必須保存所有 operation 的 state。
Notifications
Core Data 使用 notification 傳遞事件(例如一個 managed object context 中的改變————NSManagedObjectContextObjectsDidChangeNotification)
發生改變時觸發的 notification 是由 managed object contexts 發出的,所以我們不能假定消息的接收者知道消息的發送者。因為消息的源頭不是一個 UI 事件,很多接收者可能在關注著此消息,并且消息傳遞是單向的,所以 notification 是唯一可行的選擇。
Delegation
Table view 的 delegate 有多重功能,它可以從管理 accessory view,直到追蹤在屏幕上顯示的 cell。例如我們可以看看 tableView:didSelectRowAtIndexPath: 方法。為什么用 delegate 實現而不是 target-action 機制?
正如我們在上述流程圖中看到的,用 target-action 時,不能傳遞自定義的數據。而選中 table view 的某個 cell 時,collection view 不僅需要告訴我們一個 cell 被選中了,也要通過 index path 告訴我們哪個 cell 被選中了。如果我們照著這個思路,流程圖會引導我們使用 delegation 機制。
如果不在消息傳遞中包含選中 cell 的 index path,而是讓選中項改變時我們像 table view 主動詢問并獲取選中 cell 的相關信息,會怎樣呢?這會非常不方便,因為我們必須記住當前選中項的數據,這樣才能在多選擇中知道哪些 cell 是被新選中的。
同理,我們可以想象通過觀察 table view 選中項的 index path 屬性,當該值發生改變的時候,獲得一個選中項改變的通知。不過我們會遇到上述相似問題:不做記錄的話我們就不能分辨哪一個 cell 被選擇或取消選擇了。
Block
我們用 -[NSURLSession dataTaskWithURL:completionHandler:] 來作為一個 block API 的介紹。那么從 URL 加載部分返回給調用者是怎么傳遞消息的呢?首先,作為 API 的調用者,我們知道消息的發送者,但是我們并沒有 retain 它。另外,這是個單向的消息傳遞————它直接調用 dataTaskWithURL: 的方法。如果我們對照流程圖,會發現這屬于 block 消息傳遞機制。
有其他的選項嗎?當然,蘋果自己的 NSURLConnection 就是最好的例子。NSURLConnection在 block 問世之前就存在了,所以它并沒有用 block 來實現消息傳遞,而是使用 delegation 來完成。當 block 出現以后,蘋果就在 OS X 10.7 和 iOS 5 平臺上的 NSURLConnection 中加了 sendAsynchronousRequest:queue:completionHandler:,所以我們不再在簡單的任務中使用 delegate 了。
因為 NSURLSession 是個最近在 OS X 10.9 和 iOS 7 才出現的 API,所以它們使用 block 來實現消息傳遞機制(NSURLSession 有一個 delegate,但是是用于其他目的)。
Target-Action
一個明顯的 target-action 用例是按鈕。按鈕在不被按下的時候不需要發送任何的信息。為了這個目的,target-action 是 UI 中消息傳遞的最佳選擇。
如果 target 是明確指定的,那么 action 消息會發送給指定的對象。如果 target 是 nil, action 消息會一直在響應鏈中被傳遞下去,直到找到一個能處理它的對象。在這種情況下,我們有一個完全解耦的消息傳遞機制:發送者不需要知道接收者,反之亦然。
Target-action 機制非常適合響應 UI 的事件。沒有其他的消息傳遞機制能夠提供相同的功能。雖然 notification 在發送者和接收者的松散關系上最接近它,但是 target-action 可以用于響應鏈——只有一個對象獲得 action 并響應,action 在響應鏈中傳遞,直到能遇到響應這個 action 的對象。
? ? ? ? ? ? ? ? 總結
一開始接觸這么多的消息傳遞機制的時候,我們可能有些無所適從,覺得所有的機制都可以被選用。不過一旦我們仔細分析每個機制的時候,它們各自都有特殊的要求和能力。