每個Application或多或少都有一些松耦合的對象(模塊)組成,他們必須彼此通訊來完成工作。這篇文章將會通過可用的通訊機制,並以Apple的Framework來舉例,並給出最佳的實踐建議關於使用哪種通訊機制。
雖然這個問題是關於Foundation框架的,但是我們可以通過Foundation的通訊機制,差不多有這幾個通訊方法 - KVO,Notification,delegate,block,target-action。
當然,沒有人定義了在哪種情況就必須用哪種模式,選擇往往都是看個人口味。但是也有很多情況可以很明確的知道使用哪種通訊機制。
在這篇文章中,我們經常使用‘recipient’(接收者)和‘sender’(發送者),舉個例子:tableView是一個sender,它的代理是recipient。Core data managed對象是sender,他通過發通知然後接收到的對象是recipient。Slider是sender,它的action Implement是recipient,一個對象如果用KVO監聽了一個屬性,那麼屬性的改變是一個sender,observe是recipient。明白了嗎?
首先我們看一下每個通訊模式他們的不同點。在此基礎上,我們通過流程圖的方式幫助你選擇使用哪種模式。最後,我們通過蘋果的幾個framework來舉幾個例子。
KVO是通知對象屬性改變(property change)的一種機制,Foundation和其他很多蘋果的框架的實現都依靠它。如果你想知道更多關於KVO的使用例子,請點此鏈接。
如果你對某個對象的屬性改變感興趣,那麼適合使用KVO。它有一些要求,首先,你的recipient(接收者)需要監聽sender某個屬性改變的消息,並且sender會把屬性改變後的值告訴你。此外,recipient同樣需要知道sender的生命周期,因為在sender調用dealloc之前需要注銷監聽。如果上面條件滿足了,那麼適合使用KVO。KVO甚至可以一對多,可以同時設置多個監聽者。
如果你想在CoreData對象使用KVO,會有一點點的不同,這和它的faulting機制相關。當一個CoreData manager對象進入了faulting,他也會通知觀察者盡管他們的屬性沒改變。
Notification是一種非常好的工具,在兩個不相關的對象之間廣播消息,尤其是有附帶信息的時候,你也不需要做其他任何事。
Notifications被用來發送任何消息,在使用NSNotification這個子類的時候,還能附帶自定義的參數在userInfo這個Dictionary裡面。notifications是唯一一種通訊機制,它們使sender和recipient不需要知道彼此。也更加的松耦合。因此,這種機制是單向的-你不能去回復一個通知(對sender做某些事情)。
delegate模式在蘋果的框架中使用廣泛,它允許定制一個對象的行為並且通知它某個時候去執行。在delegate中,sender需要知道recipient,但和常規的方式不同,它更加的松耦合,因為sender僅僅知道他的delegate遵守了某個協議。
由於在protocol中能定義任何方法,所以你能精確的建立你想要的通訊模式,你能在參數裡面傳遞自定義的數據,並且能對返回值做出響應。delegate是一種靈活和簡單的方式,如果你僅僅需要在兩個對象之間做出通訊,這是一種用在兩個對象之間的關系相對比較親近的時候。
過度使用delegate模式也是很危險的,如果兩個對象是緊耦合的關系,一個不能沒有另一個,那就不適合使用delegate。這種情況下,一個對象應該直接設置為另一個對象的屬性。比如:UICollectionViewLayout和NSURLSessionConfiguration。
block是在OS X 10.6和iOS 4的時候才加進去的。Blocks替代了很多之前用delegate模式實現的地方。但是,這兩種模式都有它自己的優缺點。
如果你使用了block。如果你的sender需要引用block,然而不能保證這個引用置為nil的話,並且在bloc裡面使用了self這個變量的話,這裡將會產生循環引用。假設我們不用delegate,而用block實現一個tableVeiw,像這樣:
self.myTableView.selectionHandler = ^void(NSIndexPath *selectedIndexPath) {
// handle selection ...
};
這裡出現的問題是self引用了tableview,tableView應用了block為了在將來之後某個時刻使用它。tableView不知道在什麼時候把他設為nil。如果我們不能保證循環引用在某個時候會打破,block就不適合在這種情況使用。
使用NSOperation就沒有這個問題。因為他將在某個時候打破循環引用。
self.queue = [[NSOperationQueue alloc] init];
MyOperation *operation = [[MyOperation alloc] init];
operation.completionBlock = ^{
[self finishedOperation];
};
[self.queue addOperation:operation];
咋一看,這裡似乎會產生循環引用,self引用了queue,queue引用了operation,operation引用了complete block,complete block引用了self。但是,在operation被添加到queue的時候,operation就會執行,在執行完畢的時候,queue會把這個operation移除,這就打破了這個循環引用。
另一個例子:假設我們去實現一個視頻編碼類(video encoder class),我們調用了encodeWithCompletionHandler:方法。為了使調用無問題,我們必須保證編碼對象在某個時刻把block引用置為nil。像這樣:
@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
一旦我們調用了complete block之後,我們立即把他置為nil。
block非常適合去做只有單一回調的事情,因為我們知道在這種情況下,什麼時候該去打破這個引用循環。此外,block的可讀性比較好,因為方法調用和處理是在一起的。沿著這條線,在completeHandler和errorHandler這種情況下適合使用block。
Target-Action是用於響應用戶UI操作的一種模式。支持這種模式在iOS上是UIControl,Mac上是NSControl、NSCell。Target-Action在sender和recipient之間建立了一種松耦合的關系。recipient不需要知道sender,sender也不需要知道recipient。在target為nil的情況下,action就會進入事件響應鏈,知道找到能響應它的對象。在iOS中,一個control能關聯多個target-action。
使用target-action模式的限制是你不能傳遞自定義的參數。在Mac上總是以sender作為他的第一個參數。iOS中你可以選擇sender和event模式作為action的參數。但是,沒有辦法傳遞其他對象。
基於上面提到的每個模式不同的特點,我們畫了一個流程圖,助於你選擇在什麼時候使用哪種模式。但也僅僅是個建議,並不是最終的答案。你可能也其他的選擇一樣能實現的非常好。但是在工作中,大多數情況下能引導你做出正確的選擇。
圖片中有一些細節需要解釋一下:
圖片中有一句‘sender is KVO compliant’,不僅僅意味著他能在屬性改變的時候發送消息,而且觀察者也必須知道sender的生命周期。如果sender是以弱引用形式被保存的,那麼它將在任何時候都可能被釋放,這將會導致內存洩露。
還有一句話是‘message is direct response to method call’,這句話的意思是方法調用的接收者必須回調調用者的方法作為它的一個直接響應。你也可以理解為你的調用代碼和處理代碼出現在同一個地方是有道理的。
最後,你也要考慮sender是否能保證在某個時刻把block設為nil,如果不能,將可能會產生循環引用。
接下來,我們通過一些蘋果框架的例子來說明上述的流程圖是有一定道理的,蘋果為什麼選擇使用這種模式。
NSOperationQueue內部使用KVO來監聽各個operation的狀態改變(isFinished, isExecuting, isCancelled)。當狀態發生改變的時候,queue會收到一個KVO通知。為什麼NSOperationQueue使用KVO呢?
因為recipient(NSOperationQueue)清楚的知道sender(NSOperation)並且控制著它們的生命周期。進一步說,這種使用情況僅僅要求單向的通訊。如果NSOperationQueue僅僅是對NSOperation的屬性值改變感興趣,那其實也沒必要一定使用KVO。但是我們至少可以說,值改變被模型化成了狀態改變。由於NSOperationQueue無時無刻需要NSOperation的state屬性的最新的狀態。在這種情況下使用KVO是合乎邏輯的。
KVO並不是唯一的選擇。我們可以想象一下NSOperationQueue成為NSOperation的代理,然後NSOperation內部就會調用類似於operationDidFinish:或operationDidBeginExecuting:語句通知NSOperationQueue狀態的改變。這就很不方便了,為了保持NSOperation的狀態最新就必須加入這些代碼。
CoreData在managed對象的context發生改變的時候使用notification機制來通訊的(NSManagedObjectContextObjectsDidChangeNotification)。
這個通知是managed對象的contexts發送的,因此我們不能讓消息的接收者知道發送者是誰。由於消息的起源顯然不是一個UI事件,多個接收者可能對此感興趣,而且他們必須是一個單向的通道,那麼使用notification時唯一的選擇。
tableView的delegate能實現多種功能,從管理編輯狀態的views到追蹤屏幕上的cells。舉個例子,我們知道 tableView:didSelectRowAtIndexPath: 這個方法,為什麼我們用delegate去實現?為什麼不用target-action模式去實現?
在上面我們已經用圖表的方式提到過,target-action僅僅用在我們不需要傳參的情況下。如果一個選中事件發生了,那麼collectionView不僅僅告訴我們發生了一個cell選中事件,還要告訴我們被選中的cell的indexPath。如果我們要傳遞indexpath參數,那麼我們沿著圖片就能找到使用delegate模式。
如果我們不向選擇的方法裡面傳送indexPath參數,相反的通過訪問tableView的方式去獲取那個cell被選中了,這就很不方便,就多重選項的功能來說,到時候我們就必須去管理哪些cells被選中了為了告訴哪個cell是最近一次選中的。
類似的,我們可以想象一下使用通知和KVO的方式實現。
不管怎樣,都會遇到上面說的問題。除了我們自己去管理,我們是區分不了哪個cell是最近一次被選中/取消選中的。
基於block調用我們以-[NSURLSession dataTaskWithURL:completionHandler:]為例,當調用方加載一個url的時候它是以什麼方式回調的?首先,作為這個api的調用方,我們知道當發送一個消息的時候,但是我們不行引用它。或者說,這是一個單向的通信直接耦合dataTaskWithURL:方法調用。如果我們對照上面的圖片,就會找到使用block的方式。
有其他的選擇嗎?當然,蘋果自己的NSURLConnection就是一個很好的例子。NSURLConnection在Block出現前就已經存在了,所以他們使用delegate模式去實現。一旦block出現後,蘋果在OS X 10.7 and iOS 5上又為NSURLConnectionin增加了methodsendAsynchronousRequest:queue:completionHandler:方法來實現簡單的任務。
由於NSURLSession的API在OS X 10.9 and iOS 7之後才出現,block就被用來作為這種類型的通訊方式(NSURLSession仍然有delegate,但是用在其他方面)。
使用Target-Action模式最明顯的例子是buttons,按鈕除了他們被點擊之外不需要傳遞其他任何的信息。因此,Target-Action模式是最適合處理用戶界面事件的。
如果指定了target,那麼action消息會直接發送到Target這個對象上。但是,如果target為nil,action就會進入事件響應鏈來尋找哪個對象能夠處理它。當發送者不需要知道接受者的情況下,我們就有一種徹底解耦的通訊機制。
target-action這種模式用在用戶界面事件中比較完美。沒有其他的通訊模式可以提供相同的功能。Notifications也能對發送者和接收者完全解耦,但是它沒有 target-action的事件響應鏈機制。
在通訊機制中有這麼多可用的通訊模式值得我們高興一下。但是,在使用中具體選擇哪種模式往往很模糊。一旦我們仔細的研究每一種模式,會發現他們都有各自的特點。