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