objc系列譯文(7.4):訊息傳遞機制

發表於2014-05-30

每個應用或多或少都由一些需要相互傳遞訊息的物件結合起來以完成任務。在這篇文章裡,我們將介紹所有可用的訊息傳遞機制,並通過例子來介紹怎樣在蘋果的框架裡使用。我們還會選擇一些最佳範例來介紹什麼時候該用什麼機制。

雖然這一期的主題是關於 Foundation 框架的,但是我們會超出 Foundation 的訊息傳遞機制 (KVO 和 通知) 來講一講 delegation,block 和 target-action 幾種機制。

當然,有些情況下該使用什麼機制沒有唯一的答案,所以應該按照自己的喜好去試試。另外大多數情況下該使用什麼機制應該是很清楚的。

本文中,我們會常常提及“接收者”和“傳送者”。它們在訊息傳遞中的意思可以通過以下的例子解釋:一個 table view 是傳送者,它的 delegate 就是接收者。Core Data managed object context 是它所發出的 notification 的傳送者,獲取 notification 的就是接收者。一個滑塊 (slider) 是 action 訊息的傳送者,而實現這個 action (方法)的是它的接收者。任何修改一個支援 KVO 的物件的物件是傳送者,這個 KVO 物件的觀察者就是接收者。明白精髓了嗎?

幾種訊息傳遞機制

首先我們來看看每種機制的具體特點。在這個基礎上,下一節我們會畫一個流程圖來幫我們在具體情況下正確選擇應該使用的機制。最後,我們會介紹一些蘋果框架裡的例子並且解釋為什麼在那些用例中會選擇這樣的機制。

KVO

KVO 是提供物件屬性被改變時的通知的機制。KVO 的實現在 Foundation 中,很多基於 Foundation 的框架都依賴它。想要了解更多有關 KVO 的最佳實踐,請閱讀本期 Daniel 寫的 KVO 和 KVC 文章

如果只對某個物件的值的改變感興趣的話,就可以使用 KVO 訊息傳遞。不過有一些前提:第一,接收者(接收物件改變的通知的物件)需要知道傳送者 (值會改變的物件);第二,接收者需要知道傳送者的生命週期,因為它需要在傳送者被銷燬前登出觀察者身份。如果這兩個要去符合的話,這個訊息傳遞機制可以一對多(多個觀察者可以註冊觀察同一個物件的變化)

如果要在 Core Data 上使用 KVO 的話,方法會有些許差別。這和 Core Data 的惰性載入 (faulting) 機制有關。一旦一個 managed object 被惰性載入處理的話,即使它的屬性沒有被改變,它還是會觸發相應的觀察者。

 

編者注 把屬性值先取入快取中,在物件需要的時候再進行一次訪問,這在 Core Data 中是預設行為,這種技術稱為 Faulting。這麼做可以避免降低記憶體開銷,但是如果你確定將訪問結果物件的具體屬性值時,可以禁用 Faults 以提高獲取效能。關於這個技術更多的情況,請移步官方文件

通知

要在程式碼中的兩個不相關的模組中傳遞訊息時,通知機制是非常好的工具。通知機制廣播訊息,當訊息內容豐富而且無需指望接收者一定要關注的話這一招特別有用。

通知可以用來傳送任意訊息,甚至可以包含一個 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 會 retain table view,table view 為了讓 block 之後可以使用而又需要 retain 這個 block。然而 table view 不能把這個引用設為 nil,因為它不知道什麼時候不需要這個 block 了。如果我們不能保證打破 retain 環並且我們需要 retain 傳送者,那麼 block 就不是一個的好選擇。

NSOperation 是使用 block 的一個好範例。因為它在一定的地方打破了 retain 環,解決了上述的問題。

一眼看來好像上面的程式碼有一個 retain 環:self retain 了 queue,queue retain 了 operation, operation retain 了 completionBlock, 而 completionBlock retain 了 self。然而,把 operation 加入 queue 中會使 operation 在某個時間被執行,然後被從 queue 中移除。(如果沒被執行,問題就大了。)一旦 queue 把 operation 移除,retain 環就被打破了。

另一個例子是:我們在寫一個視訊編碼器的類,在類裡面我們會呼叫一個 encodeWithCompletionHandler: 的方法。為了不出問題,我們需要保證編碼器物件在某個時間點會釋放對 block 的引用。其程式碼如下所示:

一旦任務完成,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 訊息內容的方法了。

做出正確的選擇

基於上述對不同訊息傳遞機制的特點,我們畫了一個流程圖來幫助我們在不同情境下做出不同的選擇。一句忠告:流程圖的建議不代表最終答案。有些時候別的選擇依然能達到應有的效果。只不過大多數情況下這張圖能引導你做出正確的決定。

communication-patterns-flow-chart

 

圖中有些細節值得深究:

有個框中說到: 傳送者支援 KVO。這不僅僅是說傳送者會在值改變的時候傳送 KVO 通知,而且說明觀察者需要知道傳送者的生命週期。如果傳送者被存在一個 weak 屬性中,那麼傳送者有可能會自己變成 nil,那時觀察者會導致記憶體洩露。

一個在最後一行的框裡說,訊息直接響應方法呼叫。也就是說方法呼叫的接收者需要給呼叫者一個訊息作為方法呼叫的直接反饋。這也就是說處理訊息的程式碼和呼叫方法的程式碼必須在同一個地方。

最後在右下角的地方,一個選擇分支這樣說:傳送者能確保釋放對 block 的引用嗎?這涉及到了我們之前討論 block 的 API 存在潛在的 retain 環的問題。如果傳送者不能保證在某個時間點會釋放對 block 的引用,那麼你會惹上 retain 環的麻煩。

Framework 示例

本節我們通過一些蘋果框架裡的例子來驗證流程圖的選擇是否有道理,同時解釋為什麼蘋果會選擇用這些機制。

KVO

NSOperationQueue 用了 KVO 觀察佇列中的 operation 狀態屬性的改變情況 (isFinishedisExecutingisCancelled)。當狀態改變的時候,佇列會收到 KVO 通知。為什麼 operation 佇列要用 KVO 呢?

訊息的接收者(operation 佇列)知道訊息的傳送者(operation),並 retain 它並控制後者的生命週期。另外,在這種情況下只需要單向的訊息傳遞機制。當然如果考慮到 oepration 佇列只關心那些改變 operation 的值的改變情況的話,就還不足以說服大家使用 KVO 了。但我們可以這麼理解:被傳遞的訊息可以被當成值的改變來處理。因為 state 屬性在 operation 佇列以外也是有用的,所以這裡適合用 KVO。

kvo-flow-chart

當然 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 是唯一可行的選擇。 notification-flow-chart

 

Delegation

Table view 的 delegate 有多重功能,它可以從管理 accessory view,直到追蹤在螢幕上顯示的 cell。例如我們可以看看tableView:didSelectRowAtIndexPath: 方法。為什麼用 delegate 實現而不是 target-action 機制?

正如我們在上述流程圖中看到的,用 target-action 時,不能傳遞自定義的資料。而選中 table view 的某個 cell 時,collection view 不僅需要告訴我們一個 cell 被選中了,也要通過 index path 告訴我們哪個 cell 被選中了。如果我們照著這個思路,流程圖會引導我們使用 delegation 機制。

delegation-flow-chart

如果不在訊息傳遞中包含選中 cell 的 index path,而是讓選中項改變時我們像 table view 主動詢問並獲取選中 cell 的相關資訊,會怎樣呢?這會非常不方便,因為我們必須記住當前選中項的資料,這樣才能在多選擇中知道哪些 cell 是被新選中的。

同理,我們可以想象通過觀察 table view 選中項的 index path 屬性,當該值發生改變的時候,獲得一個選中項改變的通知。不過我們會遇到上述相似問題:不做記錄的話我們就不能分辨哪一個 cell 被選擇或取消選擇了。

Block

我們用 -[NSURLSession dataTaskWithURL:completionHandler:] 來作為一個 block API 的介紹。那麼從 URL 載入部分返回給呼叫者是怎麼傳遞訊息的呢?首先,作為 API 的呼叫者,我們知道訊息的傳送者,但是我們並沒有 retain 它。另外,這是個單向的訊息傳遞————它直接呼叫 dataTaskWithURL: 的方法。如果我們對照流程圖,會發現這屬於 block 訊息傳遞機制。

block-flow-chart

有其他的選項嗎?當然,蘋果自己的 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-flow-chart

如果 target 是明確指定的,那麼 action 訊息會傳送給指定的物件。如果 target 是 nil, action 訊息會一直在響應鏈中被傳遞下去,直到找到一個能處理它的物件。在這種情況下,我們有一個完全解耦的訊息傳遞機制:傳送者不需要知道接收者,反之亦然。

Target-action 機制非常適合響應 UI 的事件。沒有其他的訊息傳遞機制能夠提供相同的功能。雖然 notification 在傳送者和接收者的鬆散關係上最接近它,但是 target-action 可以用於響應鏈——只有一個物件獲得 action 並響應,action 在響應鏈中傳遞,直到能遇到響應這個 action 的物件。

總結

一開始接觸這麼多的訊息傳遞機制的時候,我們可能有些無所適從,覺得所有的機制都可以被選用。不過一旦我們仔細分析每個機制的時候,它們各自都有特殊的要求和能力。

文中的選擇流程圖是幫助你清楚認識這些機制的好的開始,當然它不是所有問題的答案。如果你覺得這和你自己選擇機制的方式相似或是有任何缺漏,歡迎來信指正。

相關文章