每個應(yīng)用或多或少都由一些需要相互傳遞消息的對象結(jié)合起來以完成任務(wù)。在這篇文章里,我們將介紹所有可用的消息傳遞機制,并通過例子來介紹怎樣在蘋果的框架里使用。我們還會選擇一些最佳范例來介紹什么時候該用什么機制。
雖然這一期的主題是關(guān)于 Foundation 框架的,但是我們會超出 Foundation 的消息傳遞機制 (KVO 和 通知) 來講一講 delegation,block 和 target-action 幾種機制。
當(dāng)然,有些情況下該使用什么機制沒有唯一的答案,所以應(yīng)該按照自己的喜好去試試。另外大多數(shù)情況下該使用什么機制應(yīng)該是很清楚的。
本文中,我們會常常提及“接收者”和“發(fā)送者”。它們在消息傳遞中的意思可以通過以下的例子解釋:一個 table view 是發(fā)送者,它的 delegate 就是接收者。Core Data managed object context 是它所發(fā)出的 notification 的發(fā)送者,獲取 notification 的就是接收者。一個滑塊 (slider) 是 action 消息的發(fā)送者,而實現(xiàn)這個 action (方法)的是它的接收者。任何修改一個支持 KVO 的對象的對象是發(fā)送者,這個 KVO 對象的觀察者就是接收者。明白精髓了嗎?
首先我們來看看每種機制的具體特點。在這個基礎(chǔ)上,下一節(jié)我們會畫一個流程圖來幫我們在具體情況下正確選擇應(yīng)該使用的機制。最后,我們會介紹一些蘋果框架里的例子并且解釋為什么在那些用例中會選擇這樣的機制。
KVO 是提供對象屬性被改變時的通知的機制。KVO 的實現(xiàn)在 Foundation 中,很多基于 Foundation 的框架都依賴它。想要了解更多有關(guān) KVO 的最佳實踐,請閱讀本期 Daniel 寫的 KVO 和 KVC 文章。
如果只對某個對象的值的改變感興趣的話,就可以使用 KVO 消息傳遞。不過有一些前提:第一,接收者(接收對象改變的通知的對象)需要知道發(fā)送者 (值會改變的對象);第二,接收者需要知道發(fā)送者的生命周期,因為它需要在發(fā)送者被銷毀前注銷觀察者身份。如果這兩個要去符合的話,這個消息傳遞機制可以一對多(多個觀察者可以注冊觀察同一個對象的變化)
如果要在 Core Data 上使用 KVO 的話,方法會有些許差別。這和 Core Data 的惰性加載 (faulting) 機制有關(guān)。一旦一個 managed object 被惰性加載處理的話,即使它的屬性沒有被改變,它還是會觸發(fā)相應(yīng)的觀察者。
編者注 把屬性值先取入緩存中,在對象需要的時候再進行一次訪問,這在 Core Data 中是默認(rèn)行為,這種技術(shù)稱為 Faulting。這么做可以避免降低內(nèi)存開銷,但是如果你確定將訪問結(jié)果對象的具體屬性值時,可以禁用 Faults 以提高獲取性能。關(guān)于這個技術(shù)更多的情況,請移步[官方文檔](https://developer.apple.com/library/ios/documentation/Cocoa/Conceptual/CoreData/Articles/cdFaultingUniquing.html)
要在代碼中的兩個不相關(guān)的模塊中傳遞消息時,通知機制是非常好的工具。通知機制廣播消息,當(dāng)消息內(nèi)容豐富而且無需指望接收者一定要關(guān)注的話這一招特別有用。
通知可以用來發(fā)送任意消息,甚至可以包含一個 userInfo
字典。你也可以繼承 NSNotification
寫一個自己的通知類來自定義行為。通知的獨特之處在于,發(fā)送者和接收者不需要相互知道對方,所以通知可以被用來在不同的相隔很遠的模塊之間傳遞消息。這就意味著這種消息傳遞是單向的,我們不能回復(fù)一個通知。
Delegation 在蘋果的框架中廣泛存在。它讓我們能自定義對象的行為,并收到一些觸發(fā)的事件。要使用 delegation 模式的話,發(fā)送者需要知道接收者,但是反過來沒有要求。因為發(fā)送者只需要知道接收者符合一定的協(xié)議,所以它們兩者結(jié)合的很松。
因為 delegate 協(xié)議可以定義任何的方法,我們可以照著自己的需求來傳遞消息??梢杂梅椒▍?shù)來傳遞消息內(nèi)容,delegate 可以通過返回值的形式來給發(fā)送者作出回應(yīng)。如果只要在相對接近的兩個模塊間傳遞消息,delgation 是很靈活很直接的消息傳遞機制。
過度使用 delegation 也會帶來風(fēng)險。如果兩個對象結(jié)合得很緊密,任何其中一個對象都不能單獨運轉(zhuǎn),那么就不需要用 delegate 協(xié)議了。這些情況下,對象已經(jīng)知道各自的類型,可以直接交流。兩個比較新的例子是 UICollectionViewLayout
和 NSURLSessionConfiguration
。
Block 是最近才加入 Objective-C 的,首次出現(xiàn)在 OS X 10.6 和 iOS 4 平臺上。Block 通常可以完全替代 delegation 消息傳遞機制的角色。不過這兩種機制都有它們自己的獨特需求和優(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è)我們要實現(xiàn)一個用 block 回調(diào)而不是 delegate 機制的 table view 里的選擇方法,如下所示:
self.myTableView.selectionHandler = ^void(NSIndexPath *selectedIndexPath) {
// 處理選擇
};
這兒的問題是,self
會 retain table view,table view 為了讓 block 之后可以使用而又需要 retain 這個 block。然而 table view 不能把這個引用設(shè)為 nil,因為它不知道什么時候不需要這個 block 了。如果我們不能保證打破 retain 環(huán)并且我們需要 retain 發(fā)送者,那么 block 就不是一個的好選擇。
NSOperation
是使用 block 的一個好范例。因為它在一定的地方打破了 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:
的方法。為了不出問題,我們需要保證編碼器對象在某個時間點會釋放對 block 的引用。其代碼如下所示:
@interface Encoder ()
@property (nonatomic, copy) void (^completionHandler)();
@end
@implementation Encoder
- (void)encodeWithCompletionHandler:(void (^)())handler
{
self.completionHandler = handler;
// 進行異步處理...
}
// 這個方法會在完成后被調(diào)用一次
- (void)finishedEncoding
{
self.completionHandler();
self.completionHandler = nil; // <- 不要忘了這個!
}
@end
一旦任務(wù)完成,completion block 調(diào)用過了以后,我們就應(yīng)該把它設(shè)為 nil
。
如果一個被調(diào)用的方法需要發(fā)送一個一次性的消息作為回復(fù),那么使用 block 是很好的選擇, 因為這樣做我們可以打破潛在的 retain 環(huán)。另外,如果將處理的消息和對消息的調(diào)用放在一起可以增強可讀性的話,我們也很難拒絕使用 block 來進行處理。在用例之中,使用 block 來做完成的回調(diào),錯誤的回調(diào),或者類似的事情,是很常見的情況。
Target-Action 是回應(yīng) UI 事件時典型的消息傳遞方式。iOS 上的 UIControl
和 Mac 上的 NSControl
/NSCell
都支持這個機制。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 傳遞機制的一個局限是,發(fā)送的消息不能攜帶自定義的信息。在 Mac 平臺上 action 方法的第一個參數(shù)永遠接收者。iOS 中,可以選擇性的把發(fā)送者和觸發(fā) action 的事件作為參數(shù)。除此之外就沒有別的控制 action 消息內(nèi)容的方法了。
基于上述對不同消息傳遞機制的特點,我們畫了一個流程圖來幫助我們在不同情境下做出不同的選擇。一句忠告:流程圖的建議不代表最終答案。有些時候別的選擇依然能達到應(yīng)有的效果。只不過大多數(shù)情況下這張圖能引導(dǎo)你做出正確的決定。
http://wiki.jikexueyuan.com/project/objc/images/7-3.png" alt="" />
圖中有些細節(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ā)送者不能保證在某個時間點會釋放對 block 的引用,那么你會惹上 retain 環(huán)的麻煩。
本節(jié)我們通過一些蘋果框架里的例子來驗證流程圖的選擇是否有道理,同時解釋為什么蘋果會選擇用這些機制。
NSOperationQueue
用了 KVO 觀察隊列中的 operation 狀態(tài)屬性的改變情況 (isFinished
,isExecuting
,isCancelled
)。當(dāng)狀態(tài)改變的時候,隊列會收到 KVO 通知。為什么 operation 隊列要用 KVO 呢?
消息的接收者(operation 隊列)知道消息的發(fā)送者(operation),并 retain 它并控制后者的生命周期。另外,在這種情況下只需要單向的消息傳遞機制。當(dāng)然如果考慮到 oepration 隊列只關(guān)心那些改變 operation 的值的改變情況的話,就還不足以說服大家使用 KVO 了。但我們可以這么理解:被傳遞的消息可以被當(dāng)成值的改變來處理。因為 state 屬性在 operation 隊列以外也是有用的,所以這里適合用 KVO。
http://wiki.jikexueyuan.com/project/objc/images/7-4.png" alt="" />
當(dāng)然 KVO 不是唯一的選擇。我們也可以將 operation 隊列作為 operation 的 delegate 來使用,operation 會調(diào)用類似 operationDidFinish:
或者 operationDidBeginExecuting:
等方法把它的 state 傳遞給 queue。這樣就不太方便了,因為 operation 要保存 state 屬性,以便于調(diào)用這些 delegate 方法。另外,由于 queue 不能主動獲取 state 信息,所以 queue 也必須保存所有 operation 的 state。
Core Data 使用 notification 傳遞事件(例如一個 managed object context 中的改變————NSManagedObjectContextObjectsDidChangeNotification
)
發(fā)生改變時觸發(fā)的 notification 是由 managed object contexts 發(fā)出的,所以我們不能假定消息的接收者知道消息的發(fā)送者。因為消息的源頭不是一個 UI 事件,很多接收者可能在關(guān)注著此消息,并且消息傳遞是單向的,所以 notification 是唯一可行的選擇。
http://wiki.jikexueyuan.com/project/objc/images/7-5.png" alt="" />
Table view 的 delegate 有多重功能,它可以從管理 accessory view,直到追蹤在屏幕上顯示的 cell。例如我們可以看看 tableView:didSelectRowAtIndexPath:
方法。為什么用 delegate 實現(xiàn)而不是 target-action 機制?
正如我們在上述流程圖中看到的,用 target-action 時,不能傳遞自定義的數(shù)據(jù)。而選中 table view 的某個 cell 時,collection view 不僅需要告訴我們一個 cell 被選中了,也要通過 index path 告訴我們哪個 cell 被選中了。如果我們照著這個思路,流程圖會引導(dǎo)我們使用 delegation 機制。
http://wiki.jikexueyuan.com/project/objc/images/7-6.png" alt="" />
如果不在消息傳遞中包含選中 cell 的 index path,而是讓選中項改變時我們像 table view 主動詢問并獲取選中 cell 的相關(guān)信息,會怎樣呢?這會非常不方便,因為我們必須記住當(dāng)前選中項的數(shù)據(jù),這樣才能在多選擇中知道哪些 cell 是被新選中的。
同理,我們可以想象通過觀察 table view 選中項的 index path 屬性,當(dāng)該值發(fā)生改變的時候,獲得一個選中項改變的通知。不過我們會遇到上述相似問題:不做記錄的話我們就不能分辨哪一個 cell 被選擇或取消選擇了。
我們用 -[NSURLSession dataTaskWithURL:completionHandler:]
來作為一個 block API 的介紹。那么從 URL 加載部分返回給調(diào)用者是怎么傳遞消息的呢?首先,作為 API 的調(diào)用者,我們知道消息的發(fā)送者,但是我們并沒有 retain 它。另外,這是個單向的消息傳遞————它直接調(diào)用 dataTaskWithURL:
的方法。如果我們對照流程圖,會發(fā)現(xiàn)這屬于 block 消息傳遞機制。
http://wiki.jikexueyuan.com/project/objc/images/7-7.png" alt="" />
有其他的選項嗎?當(dāng)然,蘋果自己的 NSURLConnection
就是最好的例子。NSURLConnection
在 block 問世之前就存在了,所以它并沒有用 block 來實現(xiàn)消息傳遞,而是使用 delegation 來完成。當(dāng) block 出現(xiàn)以后,蘋果就在 OS X 10.7 和 iOS 5 平臺上的 NSURLConnection
中加了 sendAsynchronousRequest:queue:completionHandler:
,所以我們不再在簡單的任務(wù)中使用 delegate 了。
因為 NSURLSession
是個最近在 OS X 10.9 和 iOS 7 才出現(xiàn)的 API,所以它們使用 block 來實現(xiàn)消息傳遞機制(NSURLSession
有一個 delegate,但是是用于其他目的)。
一個明顯的 target-action 用例是按鈕。按鈕在不被按下的時候不需要發(fā)送任何的信息。為了這個目的,target-action 是 UI 中消息傳遞的最佳選擇。
http://wiki.jikexueyuan.com/project/objc/images/7-8.png" alt="" />
如果 target 是明確指定的,那么 action 消息會發(fā)送給指定的對象。如果 target 是 nil
, action 消息會一直在響應(yīng)鏈中被傳遞下去,直到找到一個能處理它的對象。在這種情況下,我們有一個完全解耦的消息傳遞機制:發(fā)送者不需要知道接收者,反之亦然。
Target-action 機制非常適合響應(yīng) UI 的事件。沒有其他的消息傳遞機制能夠提供相同的功能。雖然 notification 在發(fā)送者和接收者的松散關(guān)系上最接近它,但是 target-action 可以用于響應(yīng)鏈——只有一個對象獲得 action 并響應(yīng),action 在響應(yīng)鏈中傳遞,直到能遇到響應(yīng)這個 action 的對象。
一開始接觸這么多的消息傳遞機制的時候,我們可能有些無所適從,覺得所有的機制都可以被選用。不過一旦我們仔細分析每個機制的時候,它們各自都有特殊的要求和能力。
文中的選擇流程圖是幫助你清楚認(rèn)識這些機制的好的開始,當(dāng)然它不是所有問題的答案。如果你覺得這和你自己選擇機制的方式相似或是有任何缺漏,歡迎來信指正。