每個應用程序或多或少,都由一些松耦合的對象構(gòu)成,這些對象彼此之間要想很好的完成任務,就需要進行消息傳遞。
本文將介紹所有可用的消息傳遞機制,并通過示例來介紹這些機制在蘋果的Framework中如何使用,同時,還介紹了一些最佳實踐建議,告訴你什么時機該選擇使用什么機制。
雖然這一期的主題是關(guān)于Foundation Framework的,不過本文中還介紹了一些超出Foundation Framework(KVO和Notification)范圍的一些消息傳遞機制,另外還介紹了delegation,block和target-action。
本文中,會經(jīng)常提及接收者[recipient]和發(fā)送者[sender]。
在消息傳遞機制中具體是什么意思,我們可以通過一個示例來解釋:
一個tableview是發(fā)送者,而它的delegate就是接收者。
CoreData managed object context是notification的發(fā)送者,而獲取這些notification的主體則是接收者。
一個滑塊(slider)是action消息的發(fā)送者,而在代碼里面對應著實現(xiàn)這個action的responder就是接收者。
對象中的某個屬性支持KVO,那么誰修改這個值,誰就是發(fā)送者,對應的觀察者(observer)則是接收者。
可用的機制
首先我們來看看每種機制的具體特點。在下一節(jié)中,我會結(jié)合一個流程圖來介紹如何在具體情況下,選擇正確的消息傳遞機制。最后,將介紹一些來自蘋果Framework中的示例,并會解釋在某種確定情況下為什么要選擇固定的機制。
KVO
KVO提供了這樣一種機制:當對象中的某個屬性值發(fā)生了改變,可以對這些值的觀察者做出通知。KVO的實現(xiàn)包含在Foundation里面,基于Foundation構(gòu)建的許多Framework對KVO都有所依賴。要想了解更多關(guān)于如何使用KVO,可以閱讀本期由Daniel寫的的KVO和KVC文章。
如果對某個對象中值的改變情況感興趣,那么可以使用KVO消息傳遞機制。這里有兩個要求,首先,接收者(會接收到值發(fā)生改變的消息)必須知道發(fā)送者(值將發(fā)生改變的那個對象)。另外,接收者同樣還需要知道發(fā)送者的生命周期,因為在銷毀發(fā)送者對象之前,需要取消觀察者的注冊。如果這兩個要求都滿足了,消息傳遞過程中可以是1對多(多個觀察者可以注冊某個對象中的值)。
如果計劃在Core Data對象上使用KVO,需要知道這跟一般的KVO使用方法有點不同。那就是必須結(jié)合Core Data的故障機制(faulting mechanism),一旦core data出現(xiàn)了故障,它將會觸發(fā)其屬性對應的觀察者(即使這些屬性值沒有發(fā)生改變)。
Notification
在不相關(guān)的兩部分代碼中要想進行消息傳遞,通知(notifacation)是非常好的一種機制,它可以對消息進行廣播。特別是想要傳遞豐富的信息,并且不一定指望有誰對此消息關(guān)心。
通知可以用來發(fā)送任意的消息,甚至包含一個userInfo字典,或者是NSNotifacation的一個子類。通知的獨特之處就在于發(fā)送者和接收者雙方并不需要相互知道。這樣就可以在非常松耦合的模塊間進行消息的傳遞。記住,這種消息傳遞機制是單向的,作為接收者是不可以回復消息的。
delegation
在蘋果的Framework中,delegation模式被廣泛的只用著。
delegation允許我們定制某個對象的行為,并且可以收到某些確定的事件。
為了使用delegation模式,消息的發(fā)送者需要知道消息的接收者(delegate),反過來就不用了。
這里的發(fā)送者和接收者是比較松耦合的,因為發(fā)送者只知道它的delegate是遵循某個特定的協(xié)議。
delegate協(xié)議可以定義任意的方法,因此你可以準確的定義出你所需要的類型。你可以用函數(shù)參數(shù)的形式來處理消息內(nèi)容,delegate還可以通過返回值的形式給發(fā)送者做出回應。如果只需要在相對接近的兩個模塊之間進行消息傳遞,那么Delegation是一種非常靈活和直接方式。
不過,過渡使用delegation也有一定的風險,如果兩個對象的耦合程度比較緊密,相互之間不能獨立存在,那么此時就沒有必要使用delegate協(xié)議了,針對這種情況,對象之間可以知道相互間的類型,進而直接進行消息傳遞。例如UICollectionViewLayout和NSURLSessionConfiguration。
block
Block相對來說,是一種比較新的技術(shù),它首次出現(xiàn)是在OS X 10.6和iOS 4中。一般情況下,block可以滿足用delegation實現(xiàn)的消息傳遞機制。不過這兩種機制都有各自的需求和優(yōu)勢。
當不考慮使用block時,一般主要是考慮到block極易引起retain環(huán)。如果發(fā)送者需要reatain block,而又不能確保這個引用什么時候被nil,這樣就會發(fā)生潛在的retain環(huán)。
假設我們想要實現(xiàn)一個tableview,使用block替代delegate,來當做selection的回調(diào),如下:
self.myTableView.selectionHandler = ^void(NSIndexPath *selectedIndexPath) {
// handle selection ...
};
上面代碼的問題在于self retain了tableview,而tableview為了之后能夠使用block,進而 retain了block。而tableview又不能把這個引用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了completion block,而completionblock retain了self。不過,在這里,將operation添加到queue時,會使operation在某個時機被執(zhí)行,然后從queue中remove掉(如果沒有被執(zhí)行,就會有大問題了)。一單queue移除了operation之后,retain環(huán)就被打破了。
再來一個示例:
這里實現(xiàn)了一個視頻編碼器的類,里面有一個名為encodeWithCompletionHandler:
的方法。為了避免出現(xiàn)retain環(huán),我們需要確保編碼器這個對象能夠在某個時機nil掉其對block的引用。其內(nèi)部代碼如下所示:
@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
在上面的代碼中,一旦編碼任務完成,就會調(diào)用complietion block,進而把引用nil掉。
如果我們發(fā)送的消息屬于一次性的(具體到某個方法的調(diào)用),由于這樣可以打破潛在的retain環(huán),那么使用block是非常不錯的選擇。另外,如果為了讓代碼可讀性更強,更有連貫性,那最好是使用block了。根據(jù)這個思路,block經(jīng)常可以用于completion handler、error handler等。
Target-Action
Target-Action主要被用于響應用戶界面事件時所需要傳遞的消息中。iOS中的UIControl和Mac中的NSControl/NSCell都支持這種機制。Target-Action在消息的發(fā)送者和接收者之間建立了一個非常松散耦合。消息的接收者不知道發(fā)送者,甚至消息的發(fā)送者不需要預先知道消息的接收者。如果target是nil,action會在響應鏈(responder chain)中被傳遞,知道找到某個能夠響應該aciton的對象。在iOS中,每個控件都能關(guān)聯(lián)多個target-action。
基于target-action消息傳遞的機制有一個局限就是發(fā)送的消息不能攜帶自定義的payload。在Mac的action方法中,接收者總是被放在第一個參數(shù)中。而在iOS中,可以選擇性的將發(fā)送者和和觸發(fā)action的事件作為參數(shù)。除此之外,沒有別的辦法可以對發(fā)送action消息內(nèi)容做控制。
做出正確的選擇
根據(jù)上面討論的結(jié)果,這里我畫了一個流程圖,來幫助我們何時使用什么消息傳遞機制做出更好的決定。忠告:流程圖中的建議并非最終的答案;可能還有別的選項依然能實現(xiàn)目的。只不過大多數(shù)情況下此圖可以引導你做出正確的決定。
上圖中的有個盒子這樣說到:sender is KVO compliant(發(fā)送者支持compliant)。這不僅以意味著當值發(fā)生改變時,發(fā)送者會發(fā)送KVO通知,并且觀察者還需要知道發(fā)送者的生命周期。如果發(fā)送者被存儲在一個weak屬性中,那么發(fā)送者有可能被nil掉,進而引起觀察者發(fā)生leak。
另外底部的一個盒子說到:message is direct response to method call(消息直接在方法的調(diào)用代碼中響應)。也就是說處理消息的代碼跟方法的調(diào)用代碼處于相同的地方。
最后,在左下角,處于一個決策問題的判斷狀態(tài):sender can guarantee to nil out reference to block?(發(fā)送者能夠確保nil掉到block的引用嗎?),這實際上涉及到之前我們討論到基于block 的APIs已經(jīng)潛在的retain環(huán)。使用block時,如果發(fā)送者不能保證在某個實際能夠把對block的引用nil掉,那么將會遇到retain環(huán)的問題。
Framework示例
本節(jié)我們通過一些來自蘋果Framework的示例,來看看在實際使用某種機制之前,蘋果是處于何種原因做出選擇的。
KVO
NSOperationQueue就是用KVO來觀察隊列中operation狀態(tài)屬性的改變情況(isFinished, isExecuting, isCancelled)。當狀態(tài)發(fā)生了改變,隊列會受到一個KVO通知。為什么operationqueue要是用KVO呢?
消息的接收者(operation queue)明確的知道發(fā)送者(opertation),以及通過retain來控制operation的生命周期。另外,在這種情況下,只需要單向的消息傳遞機制。當然,如果這樣考慮:如果operation queue只關(guān)心operation值的改變情況,可能還不足以說服大家使用KVO。但是我們至少可以這樣理解:什么機制可以對值的改變進行消息傳遞呢。
當然KVO也不是唯一的選擇。
我們可以這樣設計:operation queue作為operation的delegate,operation會調(diào)用類似operationDidFinish: 或 operationDidBeginExecuting: 這樣的方法,來將它的state傳遞給queue。這樣一來,就不太方便了,因為operation需要將其state屬性保存下來,一遍調(diào)用這些delegate方法。另外,由于queue不能主動獲取state信息,所以queue也必須保存著所有operation的state。
Notifications
Core Data使用notification來傳遞事件(例如一個managed object context內(nèi)部的改變——NSManagedObjectContextDidChangeNotification)。
change notification是由managed object context發(fā)出的,所以我們不能確定消息的接收者一定知道發(fā)送者。如果消息并不是一個UI事件,而有可能多個接收者對該消息感興趣,并且消息的傳遞屬于單向(one-way communication channel),那么notification是最佳選擇。
Delegation
Tableview的delegate有多種功能,從accessoryview的管理,到屏幕中cell顯示的跟蹤,都與delegate的功勞。
例如,我們來看看 tableView:didSelectRowAtIndexPath: 方法。為什么要以delegate調(diào)用的方式來實現(xiàn)?而又為啥不用target-action方式?
正如我們在流程圖中看到的一樣,使用target-action時,不能傳遞自定義的數(shù)據(jù)。而在選中tableview的某個cell時,collectionview不僅僅需要告訴我們有一個cell被選中了,還需要告訴我們是哪個cell被選中了(index path)。按照這樣的一種思路,那么從流程圖中可以看到應該使用delegation機制。
如果消息傳遞中,不包含選中cell的index path,而是每當選中項改變時,我們主動去table view中獲取到選中cell的相關(guān)信息,會怎樣呢?其實這會非常的麻煩,因為這樣一來,我們就必須記住當前選中項相關(guān)數(shù)據(jù),以便獲知被選中的cell。
同理,雖然我們也可以通過觀察table view中選中項的index paths屬性值,當該值發(fā)生改變時,獲得一個選中項改變的通知。不過,我們會遇到與上面同樣的問題:不做任何記錄的話,我們?nèi)绾潍@知被選中項的相關(guān)信息。
Blocks
關(guān)于block的介紹,
我們來看看
[NSURLSession dataTaskWithURL:completionHandler:]
吧。從URL loading system返回到調(diào)用者,這個過程具體是如何傳遞消息的呢?首先,作為這個API的調(diào)用者,我們知道消息的發(fā)送者,但是我們并沒有retain這個發(fā)送者。另外,這屬于單向消息傳遞——直接調(diào)用dataTaskWithURL:方法。如果按照這樣的思路對照著流程圖,我們會發(fā)現(xiàn)應該使用基于block消息傳遞的機制。
還有其它可選的機制嗎?當然有了,蘋果自己的NSURLConnection就是最好的例子。NSURLConnection在block問世之前就已經(jīng)存在了,所以它并沒有利用block進行消息傳遞,而是使用delegation機制。當block出現(xiàn)之后,蘋果在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
Target-Action用的最明顯的一個地方就是button(按鈕)。button除了需要發(fā)送一個click事件以外,并不需要再發(fā)送別的信息了。所以Target-Action在用戶界面事件傳遞過程中,是最佳的選擇。
如果taget已經(jīng)明確指定了,那么action消息回直接發(fā)送給指定的對象。如果taget是nil,action消息會以冒泡的方式在響應鏈中查找一個能夠處理該消息的對象。此時,我們擁有一種完全解耦的消息傳遞機制——發(fā)送者不需要知道接收者,以及其它一些信息。
Target-Action非常適用于用戶界面中的事件。目前也沒有其它合適的消息傳遞機制能夠提供同樣的功能。雖然notification最接近這種在發(fā)送者和接收者解耦關(guān)系,但是target-action可以用于響應鏈(responder chain)——只有一個對象獲得action并作出響應,并且action可以在響應鏈中傳遞,直到遇到能夠響應該action的對象。
小結(jié)
首次接觸這些機制,感覺它們都能用于兩個對象間的消息傳遞。但是仔細琢磨一番,會發(fā)現(xiàn)它們各自有其需求和功能。
文中給出的決策流程圖可以為我們選擇使用何種機制提供參考,不過圖中給出的方案并不是最終答案,好多地方還需要親自去實踐。
注1:本文由破船譯自Communication Patterns。
<p>
<p>
Notification
場景:一對多的時候 用通知
如果不是兩個控制器 都有對方的控制器屬性 可也直接用通知
block和delegate乍看上去在作用上是很相似,但是關(guān)于它們的選型有一條嚴格的規(guī)范:當回調(diào)之后要做的任務在每次回調(diào)時都是一致的情況下,選擇delegate,在回調(diào)之后要做的任務在每次回調(diào)時無法保證一致,選擇block。在離散型調(diào)用的場景下,每一次回調(diào)都是能夠保證任務一致的,因此適用delegate。
測試代碼
A控制器發(fā)送通知處
<code>
-(void)textFieldDidEndEditing:(UITextField *)textField{
[[NSNotificationCenter defaultCenter] postNotificationName:@"Haaa" object:self userInfo:@{@"name": self.field.text}];
// 也可以寫為 [[NSNotificationCenter defaultCenter] postNotificationName:@"Haaa" object:self.field.text];
}</code>
postNotificationName:這里的name也可以寫成 外部變量方便修改
.m文件
NSString * const TextNotName = @"Haaa";
.h文件
extern NSString * const TextNotName;
然后其他接收通知的控制器import發(fā)送通知的頭文件就可以了
B,C,D控制器接收通知處 注冊通知
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(test:) name:@"Haaa" object:nil];
<code>
-(void)test:(NSNotification*)notification
{
self.lable.text = [[notification userInfo] valueForKey:@"name"];
// self.lable.text = notification.object;
}</code>
分析
先看通知
----------------NSNotification----------------
<code>
@property (readonly, copy) NSString *name;
@property (nullable, readonly, retain) id object;
@property (nullable, readonly, copy) NSDictionary*userInfo;
-(instancetype)initWithName:(NSString *)name object:(nullable id)object userInfo:(nullable NSDictionary *)userInfo
-(nullable instancetype)initWithCoder:(NSCoder *)aDecoder;
</code>
在看通知中心
----------------NSNotificationCenter----------------
+ (NSNotificationCenter *)defaultCenter;
要發(fā)送通知先調(diào)用上面方法獲取全局的通知中心對象 看這名字這貨應該是單例
- (void)postNotificationName:(NSString *)aName object:(nullable id)anObject;
例:
[[NSNotificationCenter defaultCenter] postNotificationName:@"Haaa" object:self.field.text];
aName是通知名字 接收的地方靠這個名字來接收通知
anObject 是你傳出去的東西
- (void)postNotificationName:(NSString *)aName object:(nullable id)anObject userInfo:(nullable NSDictionary *)aUserInfo;
aUserInfo
[[NSNotificationCenter defaultCenter] postNotificationName:@"Haaa" object:self userInfo:@{@"name": self.field.text}];
-removeObserver:
根據(jù)接收到的分派表 刪除 所有條目指定的觀察者
在addObserver:selector:name:object:被銷毀前一定要調(diào)用該方法
或者removeObserver:name:object:
You should not use this method to remove all observers from an object that is going to be long-lived, because your code may not be the only code adding observers that involve the object.
你不應該使用這種方法刪除所有觀察者從一個對象,該對象將是長期存在的,因為您的代碼可能并不是唯一代碼添加涉及對象的觀察者。
The following example illustrates how to unregister someObserver for all notifications for which it had previously registered. This is safe to do in the dealloc method, but should not otherwise be used—use removeObserver:name:object: instead.
下面的例子說明了如何從所有的通知里面注銷一些先前注冊過的觀察者 。
在dealloc方法里面這么做是安全的
[[NSNotificationCenter defaultCenter] removeObserver:someObserver];
NSDictionary *parameter = @{@"key":peripheral};
[[NSNotificationCenter defaultCenter] postNotificationName:PeripheralDisconnected object:self userInfo:parameter];
[[NSNotificationCenter defaultCenter] postNotificationName:PeripheralDisconnected object:peripheral];
The object whose notifications the observer wants to receive; that is, only notifications sent by this sender are delivered to the observer.
通知觀察者希望接收的對象,也就是說,只有這個發(fā)送方發(fā)送的通知交付給觀察者。
If you pass nil, the notification center doesn’t use a notification’s sender to decide whether to deliver it to the observer.
如果你通過nil,通知中心不使用通知的發(fā)送方?jīng)Q定是否發(fā)送給觀察者。
http://www.lxweimin.com/p/a2cb99dcd4fe
http://www.lxweimin.com/p/a4d519e4e0d5
http://www.lxweimin.com/p/d09f0262f6b4
http://blog.sina.com.cn/s/blog_8d918466010174tx.html
http://blog.csdn.net/wzq9706/article/details/39002491
代理
@required//此關(guān)鍵詞下的方法是必須實現(xiàn)的沒實現(xiàn)會有警告 系統(tǒng)的不實現(xiàn)基本會crash
@optional//可以不實現(xiàn)的 沒警告
自己寫的沒加關(guān)鍵詞的話 默認是有警告的
從BViewController傳到AViewController
//BViewController.h 中
#import<UIKit/UIKit.h>
@protocol BDelegate<NSObject>
- (void)setIDValue:(NSString *)value;
@end
@interface BViewController : UIViewController
@property(nonatomic, weak) id<BDelegate>delegate; // 注冊一個代理
@end
//BViewController.m中
在需要回傳的地方傳值
if (_delegate && [_delegate respondsToSelector:@selector(setIDValue:)])
{
[_delegate setIDValue:需要傳的值];
}
[self.navigationController popViewControllerAnimated:YES];
在AViewController.m中
@interface AViewController ()<BDelegate>// 遵守協(xié)議
@end
//在點擊跳到下一個頁面的方法中
BViewController *b = [[BViewController alloc] init];
b.delegate = self;
[self.navigationController pushViewController:b animated:YES];
- (void)setIDValue:(NSString *)value{
_contactIdField.text = value ;
}