? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?幾種消息傳遞機(jī)制
? ? ? ?首先我們來(lái)看看每種機(jī)制的具體特點(diǎn)羊精。在這個(gè)基礎(chǔ)上裆悄,下一節(jié)我們會(huì)畫一個(gè)流程圖來(lái)幫我們?cè)诰唧w情況下正確選擇應(yīng)該使用的機(jī)制嘉抓。最后始绍,我們會(huì)介紹一些蘋果框架里的例子并且解釋為什么在那些用例中會(huì)選擇這樣的機(jī)制访锻。
#KVO
KVO 是提供對(duì)象屬性被改變時(shí)的通知的機(jī)制。KVO 的實(shí)現(xiàn)在 Foundation 中屁置,很多基于 Foundation 的框架都依賴它焊夸。想要了解更多有關(guān) KVO 的最佳實(shí)踐仁连,請(qǐng)閱讀本期 Daniel 寫的 KVO 和 KVC 文章蓝角。
如果只對(duì)某個(gè)對(duì)象的值的改變感興趣的話阱穗,就可以使用 KVO 消息傳遞。不過(guò)有一些前提:第一使鹅,接收者(接收對(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 被惰性加載處理的話执虹,即使它的屬性沒(méi)有被改變拓挥,它還是會(huì)觸發(fā)相應(yīng)的觀察者。
通知
要在代碼中的兩個(gè)不相關(guān)的模塊中傳遞消息時(shí)袋励,通知機(jī)制是非常好的工具侥啤。通知機(jī)制廣播消息,當(dāng)消息內(nèi)容豐富而且無(wú)需指望接收者一定要關(guān)注的話這一招特別有用茬故。
通知可以用來(lái)發(fā)送任意消息盖灸,甚至可以包含一個(gè) userInfo 字典。你也可以繼承 NSNotification 寫一個(gè)自己的通知類來(lái)自定義行為磺芭。通知的獨(dú)特之處在于赁炎,發(fā)送者和接收者不需要相互知道對(duì)方,所以通知可以被用來(lái)在不同的相隔很遠(yuǎn)的模塊之間傳遞消息钾腺。這就意味著這種消息傳遞是單向的甘邀,我們不能回復(fù)一個(gè)通知。
委托 (Delegation)
Delegation 在蘋果的框架中廣泛存在垮庐。它讓我們能自定義對(duì)象的行為松邪,并收到一些觸發(fā)的事件。要使用 delegation 模式的話哨查,發(fā)送者需要知道接收者逗抑,但是反過(guò)來(lái)沒(méi)有要求。因?yàn)榘l(fā)送者只需要知道接收者符合一定的協(xié)議寒亥,所以它們兩者結(jié)合的很松邮府。
因?yàn)?delegate 協(xié)議可以定義任何的方法,我們可以照著自己的需求來(lái)傳遞消息溉奕」涌可以用方法參數(shù)來(lái)傳遞消息內(nèi)容,delegate 可以通過(guò)返回值的形式來(lái)給發(fā)送者作出回應(yīng)加勤。如果只要在相對(duì)接近的兩個(gè)模塊間傳遞消息仙辟,delgation 是很靈活很直接的消息傳遞機(jī)制同波。
過(guò)度使用 delegation 也會(huì)帶來(lái)風(fēng)險(xiǎn)。如果兩個(gè)對(duì)象結(jié)合得很緊密叠国,任何其中一個(gè)對(duì)象都不能單獨(dú)運(yùn)轉(zhuǎn)未檩,那么就不需要用 delegate 協(xié)議了。這些情況下粟焊,對(duì)象已經(jīng)知道各自的類型冤狡,可以直接交流。兩個(gè)比較新的例子是 UICollectionViewLayout 和 NSURLSessionConfiguration项棠。
Block
Block 是最近才加入 Objective-C 的悲雳,首次出現(xiàn)在 OS X 10.6 和 iOS 4 平臺(tái)上。Block 通诚阕罚可以完全替代 delegation 消息傳遞機(jī)制的角色怜奖。不過(guò)這兩種機(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) {
// 處理選擇
};
這兒的問(wèn)題是滥崩,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)才菠,解決了上述的問(wèn)題茸时。
self.queue = [[NSOperationQueue alloc] init];
MyOperation *operation = [[MyOperation alloc] init];
operation.completionBlock = ^{
[self finishedOperation];
};
[self.queue addOperation:operation];
一眼看來(lái)好像上面的代碼有一個(gè) retain 環(huán):self retain 了 queue,queue retain 了 operation赋访, operation retain 了 completionBlock可都, 而 completionBlock retain 了 self。然而蚓耽,把 operation 加入 queue 中會(huì)使 operation 在某個(gè)時(shí)間被執(zhí)行渠牲,然后被從 queue 中移除。(如果沒(méi)被執(zhí)行步悠,問(wèn)題就大了签杈。)一旦 queue 把 operation 移除,retain 環(huán)就被打破了鼎兽。
另一個(gè)例子是:我們?cè)趯懸粋€(gè)視頻編碼器的類答姥,在類里面我們會(huì)調(diào)用一個(gè) encodeWithCompletionHandler: 的方法铣除。為了不出問(wèn)題,我們需要保證編碼器對(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)用過(guò)了以后序宦,我們就應(yīng)該把它設(shè)為 nil睁壁。
如果一個(gè)被調(diào)用的方法需要發(fā)送一個(gè)一次性的消息作為回復(fù),那么使用 block 是很好的選擇互捌, 因?yàn)檫@樣做我們可以打破潛在的 retain 環(huán)潘明。另外,如果將處理的消息和對(duì)消息的調(diào)用放在一起可以增強(qiáng)可讀性的話秕噪,我們也很難拒絕使用 block 來(lái)進(jìn)行處理钳降。在用例之中,使用 block 來(lái)做完成的回調(diào)腌巾,錯(cuò)誤的回調(diào)遂填,或者類似的事情,是很常見(jiàn)的情況澈蝙。
? ? ? ? ? ? ? ? ? ? ? ? ? ?Target-Action
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ù)号涯。除此之外就沒(méi)有別的控制 action 消息內(nèi)容的方法了目胡。
? ? ? ?
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? 做出正確的選擇
基于上述對(duì)不同消息傳遞機(jī)制的特點(diǎn),我們畫了一個(gè)流程圖來(lái)幫助我們?cè)诓煌榫诚伦龀霾煌倪x擇链快。一句忠告:流程圖的建議不代表最終答案誉己。有些時(shí)候別的選擇依然能達(dá)到應(yīng)有的效果。只不過(guò)大多數(shù)情況下這張圖能引導(dǎo)你做出正確的決定域蜗。
Decision flow chart for communication patterns in Cocoa
圖中有些細(xì)節(jié)值得深究:
有個(gè)框中說(shuō)到: 發(fā)送者支持 KVO巨双。這不僅僅是說(shuō)發(fā)送者會(huì)在值改變的時(shí)候發(fā)送 KVO 通知噪猾,而且說(shuō)明觀察者需要知道發(fā)送者的生命周期。如果發(fā)送者被存在一個(gè) weak 屬性中筑累,那么發(fā)送者有可能會(huì)自己變成 nil袱蜡,那時(shí)觀察者會(huì)導(dǎo)致內(nèi)存泄露。
一個(gè)在最后一行的框里說(shuō)慢宗,消息直接響應(yīng)方法調(diào)用坪蚁。也就是說(shuō)方法調(diào)用的接收者需要給調(diào)用者一個(gè)消息作為方法調(diào)用的直接反饋。這也就是說(shuō)處理消息的代碼和調(diào)用方法的代碼必須在同一個(gè)地方镜沽。
最后在右下角的地方敏晤,一個(gè)選擇分支這樣說(shuō):發(fā)送者能確保釋放對(duì) block 的引用嗎?這涉及到了我們之前討論 block 的 API 存在潛在的 retain 環(huán)的問(wèn)題缅茉。如果發(fā)送者不能保證在某個(gè)時(shí)間點(diǎn)會(huì)釋放對(duì) block 的引用嘴脾,那么你會(huì)惹上 retain 環(huán)的麻煩。
Framework 示例
本節(jié)我們通過(guò)一些蘋果框架里的例子來(lái)驗(yàn)證流程圖的選擇是否有道理蔬墩,同時(shí)解釋為什么蘋果會(huì)選擇用這些機(jī)制译打。
? ? ? ? ? ? ? ? ? ? ? ? ? KVO
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 的值的改變情況的話猜扮,就還不足以說(shuō)服大家使用 KVO 了勉吻。但我們可以這么理解:被傳遞的消息可以被當(dāng)成值的改變來(lái)處理。因?yàn)?state 屬性在 operation 隊(duì)列以外也是有用的旅赢,所以這里適合用 KVO齿桃。
當(dāng)然 KVO 不是唯一的選擇。我們也可以將 operation 隊(duì)列作為 operation 的 delegate 來(lái)使用煮盼,operation 會(huì)調(diào)用類似 operationDidFinish: 或者 operationDidBeginExecuting: 等方法把它的 state 傳遞給 queue短纵。這樣就不太方便了,因?yàn)?operation 要保存 state 屬性僵控,以便于調(diào)用這些 delegate 方法香到。另外,由于 queue 不能主動(dòng)獲取 state 信息,所以 queue 也必須保存所有 operation 的 state悠就。
Notifications
Core Data 使用 notification 傳遞事件(例如一個(gè) managed object context 中的改變————NSManagedObjectContextObjectsDidChangeNotification)
發(fā)生改變時(shí)觸發(fā)的 notification 是由 managed object contexts 發(fā)出的千绪,所以我們不能假定消息的接收者知道消息的發(fā)送者。因?yàn)橄⒌脑搭^不是一個(gè) UI 事件梗脾,很多接收者可能在關(guān)注著此消息荸型,并且消息傳遞是單向的,所以 notification 是唯一可行的選擇炸茧。
Delegation
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 被選中了,也要通過(guò) index path 告訴我們哪個(gè) cell 被選中了他去。如果我們照著這個(gè)思路毙驯,流程圖會(huì)引導(dǎo)我們使用 delegation 機(jī)制。
如果不在消息傳遞中包含選中 cell 的 index path灾测,而是讓選中項(xiàng)改變時(shí)我們像 table view 主動(dòng)詢問(wèn)并獲取選中 cell 的相關(guān)信息爆价,會(huì)怎樣呢?這會(huì)非常不方便媳搪,因?yàn)槲覀儽仨氂涀‘?dāng)前選中項(xiàng)的數(shù)據(jù)铭段,這樣才能在多選擇中知道哪些 cell 是被新選中的。
同理秦爆,我們可以想象通過(guò)觀察 table view 選中項(xiàng)的 index path 屬性序愚,當(dāng)該值發(fā)生改變的時(shí)候,獲得一個(gè)選中項(xiàng)改變的通知等限。不過(guò)我們會(huì)遇到上述相似問(wèn)題:不做記錄的話我們就不能分辨哪一個(gè) cell 被選擇或取消選擇了爸吮。
Block
我們用 -[NSURLSession dataTaskWithURL:completionHandler:] 來(lái)作為一個(gè) block API 的介紹。那么從 URL 加載部分返回給調(diào)用者是怎么傳遞消息的呢望门?首先形娇,作為 API 的調(diào)用者,我們知道消息的發(fā)送者筹误,但是我們并沒(méi)有 retain 它埂软。另外,這是個(gè)單向的消息傳遞————它直接調(diào)用 dataTaskWithURL: 的方法。如果我們對(duì)照流程圖勘畔,會(huì)發(fā)現(xiàn)這屬于 block 消息傳遞機(jī)制所灸。
有其他的選項(xiàng)嗎?當(dāng)然炫七,蘋果自己的 NSURLConnection 就是最好的例子爬立。NSURLConnection在 block 問(wèn)世之前就存在了,所以它并沒(méi)有用 block 來(lái)實(shí)現(xiàn)消息傳遞万哪,而是使用 delegation 來(lái)完成侠驯。當(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 來(lái)實(shí)現(xiàn)消息傳遞機(jī)制(NSURLSession 有一個(gè) delegate的止,但是是用于其他目的)檩坚。
Target-Action
一個(gè)明顯的 target-action 用例是按鈕。按鈕在不被按下的時(shí)候不需要發(fā)送任何的信息诅福。為了這個(gè)目的匾委,target-action 是 UI 中消息傳遞的最佳選擇。
如果 target 是明確指定的氓润,那么 action 消息會(huì)發(fā)送給指定的對(duì)象赂乐。如果 target 是 nil, action 消息會(huì)一直在響應(yīng)鏈中被傳遞下去咖气,直到找到一個(gè)能處理它的對(duì)象挨措。在這種情況下,我們有一個(gè)完全解耦的消息傳遞機(jī)制:發(fā)送者不需要知道接收者崩溪,反之亦然浅役。
Target-action 機(jī)制非常適合響應(yīng) UI 的事件。沒(méi)有其他的消息傳遞機(jī)制能夠提供相同的功能悯舟。雖然 notification 在發(fā)送者和接收者的松散關(guān)系上最接近它担租,但是 target-action 可以用于響應(yīng)鏈——只有一個(gè)對(duì)象獲得 action 并響應(yīng),action 在響應(yīng)鏈中傳遞抵怎,直到能遇到響應(yīng)這個(gè) action 的對(duì)象奋救。
? ? ? ? ? ? ? ? 總結(jié)
一開(kāi)始接觸這么多的消息傳遞機(jī)制的時(shí)候,我們可能有些無(wú)所適從反惕,覺(jué)得所有的機(jī)制都可以被選用尝艘。不過(guò)一旦我們仔細(xì)分析每個(gè)機(jī)制的時(shí)候,它們各自都有特殊的要求和能力姿染。