轉載原地址:http://beyondvincent.com/blog/2013/12/14/124-communication-patterns/ html
注1:本文由破船譯自Communication Patterns。 ios
本文目錄以下所示: app
每一個應用程序或多或少,都由一些鬆耦合的對象構成,這些對象彼此之間要想很好的完成任務,就須要進行消息傳遞。本文將介紹全部可用的消息傳遞機制,並經過示例來介紹這些機制在蘋果的Framework中如何使用,同時,還介紹了一些最佳實踐建議,告訴你什麼時機該選擇使用什麼機制。 async
雖然這一期的主題是關於Foundation Framework的,不過本文中還介紹了一些超出Foundation Framework(KVO和Notification)範圍的一些消息傳遞機制,另外還介紹了delegation,block和target-action。 ide
大多數狀況下,消息傳遞該使用什麼機制,是很明確的了,固然了,在某些狀況下該使用什麼機制並無明確的答案,須要你親自去嘗試一下。 函數
本文中,會常常說起接收者[recipient]和發送者[sender]。在消息傳遞機制中具體是什麼意思,咱們能夠經過一個示例來解釋:一個table view是發送者,而它的delegate就是接收者。Core Data managed object context是notification的發送者,而獲取這些notification的主體則是接收者。一個滑塊(slider)是action消息的發送者,而在代碼裏面對應着實現這個action的responder就是接收者。對象中的某個屬性支持KVO,那麼誰修改這個值,誰就是發送者,對應的觀察者(observer)則是接收者。 ui
首先咱們來看看每種機制的具體特色。在下一節中,我會結合一個流程圖來介紹如何在具體狀況下,選擇正確的消息傳遞機制。最後,將介紹一些來自蘋果Framework中的示例,並會解釋在某種肯定狀況下爲何要選擇固定的機制。 this
KVO提供了這樣一種機制:當對象中的某個屬性值發生了改變,能夠對這些值的觀察者作出通知。KVO的實現包含在Foundation裏面,基於Foundation構建的許多Framework對KVO都有所依賴。要想了解更多關於如何使用KVO,能夠閱讀本期由Daniel寫的的KVO和KVC文章。 編碼
若是對某個對象中值的改變狀況感興趣,那麼可使用KVO消息傳遞機制。這裏有兩個要求,首先,接收者(會接收到值發生改變的消息)必須知道發送者(值將發生改變的那個對象)。另外,接收者一樣還須要知道發送者的生命週期,由於在銷燬發送者對象以前,須要取消觀察者的註冊。若是這兩個要求都知足了,消息傳遞過程當中能夠是1對多(多個觀察者能夠註冊某個對象中的值)。 atom
若是計劃在Core Data對象上使用KVO,須要知道這跟通常的KVO使用方法有點不一樣。那就是必須結合Core Data的故障機制(faulting mechanism),一旦core data出現了故障,它將會觸發其屬性對應的觀察者(即便這些屬性值沒有發生改變)。
在不相關的兩部分代碼中要想進行消息傳遞,通知(notifacation)是很是好的一種機制,它能夠對消息進行廣播。特別是想要傳遞豐富的信息,而且不必定期望有誰對此消息關心。
通知能夠用來發送任意的消息,甚至包含一個userInfo字典,或者是NSNotifacation的一個子類。通知的獨特之處就在於發送者和接收者雙方並不須要相互知道。這樣就能夠在很是鬆耦合的模塊間進行消息的傳遞。記住,這種消息傳遞機制是單向的,做爲接收者是不能夠回覆消息的。
在蘋果的Framework中,delegation模式被普遍的只用着。delegation容許咱們定製某個對象的行爲,而且能夠收到某些肯定的事件。爲了使用delegation模式,消息的發送者須要知道消息的接收者(delegate),反過來就不用了。這裏的發送者和接收者是比較鬆耦合的,由於發送者只知道它的delegate是遵循某個特定的協議。
delegate協議能夠定義任意的方法,所以你能夠準確的定義出你所須要的類型。你能夠用函數參數的形式來處理消息內容,delegate還能夠經過返回值的形式給發送者作出迴應。若是隻須要在相對接近的兩個模塊之間進行消息傳遞,那麼Delegation是一種很是靈活和直接方式。
不過,過渡使用delegation也有必定的風險,若是兩個對象的耦合程度比較緊密,相互之間不能獨立存在,那麼此時就沒有必要使用delegate協議了,針對這種狀況,對象之間能夠知道相互間的類型,進而直接進行消息傳遞。例如UICollectionViewLayout和NSURLSessionConfiguration。
Block相對來講,是一種比較新的技術,它首次出現是在OS X 10.6和iOS 4中。通常狀況下,block能夠知足用delegation實現的消息傳遞機制。不過這兩種機制都有各自的需求和優點。
當不考慮使用block時,通常主要是考慮到block極易引發retain環。若是發送者須要reatain block,而又不能確保這個引用何時被nil,這樣就會發生潛在的retain環。
假設咱們想要實現一個table view,使用block替代delegate,來當作selection的回調,以下:
1 2 3 |
self.myTableView.selectionHandler = ^void(NSIndexPath *selectedIndexPath) { // handle selection ... }; |
上面代碼的問題在於self retain了table view,而table view爲了以後可以使用block,進而 retain了block。而table view又不能把這個引用nil掉,由於它不知道何時不在須要這個block了。若是咱們保證不了能夠打破這個retain環,而咱們又須要retain發送者,此時block不是好的選擇。
NSOperation就能夠很好的使用block,由於它能再某個時機打破retain環:
1 2 3 4 5 6 |
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了completion block,而completion blockretain了self。不過,在這裏,將operation添加到queue時,會使operation在某個時機被執行,而後從queue中remove掉(若是沒有被執行,就會有大問題了)。一單queue移除了operation以後,retain環就被打破了。
再來一個示例:這裏實現了一個視頻編碼器的類,裏面有一個名爲encodeWithCompletionHandler:的方法。爲了不出現retain環,咱們須要確保編碼器這個對象可以在某個時機nil掉其對block的引用。其內部代碼以下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
@interface Encoder () @property (nonatomic, copy) void (^completionHandler)(); @end @implementation Encoder - (void)encodeWithCompletionHandler:(void (^)())handler { self.completionHandler = handler; // do the asynchronous processing... } // This one will be called once the job is done - (void)finishedEncoding { self.completionHandler(); self.completionHandler = nil; // <- Don't forget this! } @end |
在上面的代碼中,一旦編碼任務完成,就會調用complietion block,進而把引用nil掉。
若是咱們發送的消息屬於一次性的(具體到某個方法的調用),因爲這樣能夠打破潛在的retain環,那麼使用block是很是不錯的選擇。另外,若是爲了讓代碼可讀性更強,更有連貫性,那最好是使用block了。根據這個思路,block常常能夠用於completion handler、error handler等。
Target-Action主要被用於響應用戶界面事件時所須要傳遞的消息中。iOS中的UIControl和Mac中的NSControl/NSCell都支持這種機制。Target-Action在消息的發送者和接收者之間創建了一個很是鬆散耦合。消息的接收者不知道發送者,甚至消息的發送者不須要預先知道消息的接收者。若是target是nil,action會在響應鏈(responder chain)中被傳遞,知道找到某個可以響應該aciton的對象。在iOS中,每一個控件都能關聯多個target-action。
基於target-action消息傳遞的機制有一個侷限就是發送的消息不能攜帶自定義的payload。在Mac的action方法中,接收者老是被放在第一個參數中。而在iOS中,能夠選擇性的將發送者和和觸發action的事件做爲參數。除此以外,沒有別的辦法能夠對發送action消息內容作控制。
根據上面討論的結果,這裏我畫了一個流程圖,來幫助咱們什麼時候使用什麼消息傳遞機制作出更好的決定。忠告:流程圖中的建議並不是最終的答案;可能還有別的選項依然能實現目的。只不過大多數狀況下此圖能夠引導你作出正確的決定。
上圖中,還有一些細節須要作更近一步的解釋:
上圖中的有個盒子這樣說到:sender is KVO compliant(發送者支持compliant)。這不只以意味着當值發生改變時,發送者會發送KVO通知,而且觀察者還須要知道發送者的生命週期。若是發送者被存儲在一個weak屬性中,那麼發送者有可能被nil掉,進而引發觀察者發生leak。
另外底部的一個盒子說到:message is direct response to method call(消息直接在方法的調用代碼中響應)。也就是說處理消息的代碼跟方法的調用代碼處於相同的地方。
最後,在左下角,處於一個決策問題的判斷狀態:sender can guarantee to nil out reference to block?(發送者可以確保nil掉到block的引用嗎?),這實際上涉及到以前咱們討論到基於block 的APIs已經潛在的retain環。使用block時,若是發送者不能保證在某個實際可以把對block的引用nil掉,那麼將會遇到retain環的問題。
本節咱們經過一些來自蘋果Framework的示例,來看看在實際使用某種機制以前,蘋果是處於何種緣由作出選擇的。
NSOperationQueue就是lion給了KVO來觀察隊列中operation狀態屬性的改變狀況(isFinished, isExecuting, isCancelled)。當狀態發生了改變,隊列會受到一個KVO通知。爲何operationqueue要是用KVO呢?
消息的接收者(operation queue)明確的知道發送者(opertation),以及經過retain來控制operation的生命週期。另外,在這種狀況下,只須要單向的消息傳遞機制。固然,若是這樣考慮:若是operation queue只關心operation值的改變狀況,可能還不足以說服你們使用KVO。可是咱們至少能夠這樣理解:什麼機制能夠對值的改變進行消息傳遞呢。
固然KVO也不是惟一的選擇。咱們能夠這樣設計:operation queue做爲operation的delegate,operation會調用相似operationDidFinish: 或 operationDidBeginExecuting: 這樣的方法,來將它的state傳遞給queue。這樣一來,就不太方便了,由於operation須要將其state屬性保存下來,一遍調用這些delegate方法。另外,因爲queue不能主動獲取state信息,因此queue也必須保存着全部operation的state。
Core Data使用notification來傳遞事件(例如一個managed object context內部的改變——NSManagedObjectContextDidChangeNotification)。
change notification是由managed object context發出的,因此咱們不能肯定消息的接收者必定知道發送者。若是消息並非一個UI事件,而有可能多個接收者對該消息感興趣,而且消息的傳遞屬於單向(one-way communication channel),那麼notification是最佳選擇。
Table view的delegate有多種功能,從accessory view的管理,到屏幕中cell顯示的跟蹤,都與delegate的功勞。例如,咱們來看看 tableView:didSelectRowAtIndexPath: 方法。爲何要以delegate調用的方式來實現?而又爲啥不用target-action方式?
正如咱們在流程圖中看到的同樣,使用target-action時,不能傳遞自定義的數據。而在選中table view的某個cell時,collection view不只僅須要告訴咱們有一個cell被選中了,還須要告訴咱們是哪一個cell被選中了(index path)。按照這樣的一種思路,那麼從流程圖中能夠看到應該使用delegation機制。
若是消息傳遞中,不包含選中cell的index path,而是每當選中項改變時,咱們主動去table view中獲取到選中cell的相關信息,會怎樣呢?其實這會很是的麻煩,由於這樣一來,咱們就必須記住當前選中項相關數據,以便獲知被選中的cell。
同理,雖然咱們也能夠經過觀察table view中選中項的index paths屬性值,當該值發生改變時,得到一個選中項改變的通知。不過,咱們會遇到與上面一樣的問題:不作任何記錄的話,咱們如何獲知被選中項的相關信息。
關於block的介紹,咱們來看看[NSURLSession dataTaskWithURL:completionHandler:]吧。從URL loading system返回到調用者,這個過程具體是如何傳遞消息的呢?首先,做爲這個API的調用者,咱們知道消息的發送者,可是咱們並無retain這個發送者。另外,這屬於單向消息傳遞——直接調用dataTaskWithURL:方法。若是按照這樣的思路對照着流程圖,咱們會發現應該使用基於block消息傳遞的機制。
還有其它可選的機制嗎?固然有了,蘋果本身的NSURLConnection就是最好的例子。NSURLConnection在block問世以前就已經存在了,因此它並無利用block進行消息傳遞,而是使用delegation機制。當block出現以後,蘋果在NSURLConnection中添加了sendAsynchronousRequest:queue:completionHandler:方法(OSX 10.7 iOS 5),所以若是是簡單的task,就沒必要在使用delegate了。
在OS X 10.9 和 iOS 7中,蘋果引入了一個很是modern的API:NSURLSession,其中使用block當作消息傳遞機制(NSURLSession仍然有一個delegate,不過是用於別的目的)。
Target-Action用的最明顯的一個地方就是button(按鈕)。button除了須要發送一個click事件之外,並不須要再發送別的信息了。因此Target-Action在用戶界面事件傳遞過程當中,是最佳的選擇。
若是taget已經明確指定了,那麼action消息回直接發送給指定的對象。若是taget是nil,action消息會以冒泡的方式在響應鏈中查找一個可以處理該消息的對象。此時,咱們擁有一種徹底解耦的消息傳遞機制——發送者不須要知道接收者,以及其它一些信息。
Target-Action很是適用於用戶界面中的事件。目前也沒有其它合適的消息傳遞機制可以提供一樣的功能。雖然notification最接近這種在發送者和接收者解耦關係,可是target-action能夠用於響應鏈(responder chain)——只有一個對象得到action並做出響應,而且action能夠在響應鏈中傳遞,直到遇到可以響應該action的對象。
首次接觸這些機制,感受它們都能用於兩個對象間的消息傳遞。可是仔細琢磨一番,會發現它們各自有其需求和功能。
文中給出的決策流程圖能夠爲咱們選擇使用何種機制提供參考,不過圖中給出的方案並非最終答案,好多地方還須要親自去實踐。