問題的背景
IOS中委托模式和消息機制基本上開發中用到的比較多,一般最開始頁面傳值通過委托實現的比較多,類之間的傳值用到的比較多,不過委托相對來說只能是一對一,比如說頁面A跳轉到頁面B,頁面的B的值改變要映射到頁面A,頁面C的值改變也需要映射到頁面A,那么就需要需要兩個委托解決問題。NSNotificaiton則是一對多注冊一個通知,之后回調很容易解決以上的問題。
概念
iOS消息通知機制算是同步的,觀察者只要向消息中心注冊, 即可接受其他對象發送來的消息,消息發送者和消息接受者兩者可以互相一無所知,完全解耦。這種消息通知機制可以應用于任意時間和任何對象,觀察者可以有多個,所以消息具有廣播的性質,只是需要注意的是,觀察者向消息中心注冊以后,在不需要接受消息時需要向消息中心注銷,屬于典型的觀察者模式。
消息通知中重要的兩個類:
(1)NSNotificationCenter: 實現NSNotificationCenter的原理是一個觀察者模式,獲得NSNotificationCenter的方法只有一種,那就是[NSNotificationCenter defaultCenter] ,通過調用靜態方法defaultCenter就可以獲取這個通知中心的對象了。NSNotificationCenter是一個單例模式,而這個通知中心的對象會一直存在于一個應用的生命周期。
(2) NSNotification: 這是消息攜帶的載體,通過它,可以把消息內容傳遞給觀察者。
使用
1.通過NSNotificationCenter注冊通知NSNotification,viewDidLoad中代碼如下:
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(notificationFirst:) name:@"First" object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(notificationSecond:) name:@"Second" object:nil];
第一個參數是觀察者為本身,第二個參數表示消息回調的方法,第三個消息通知的名字,第四個為nil表示表示接受所有發送者的消息~
回調方法:
-(void)notificationFirst:(NSNotification *)notification{
NSString *name=[notification name];
NSString *object=[notification object];
NSLog(@"名稱:%@----對象:%@",name,object);
}
-(void)notificationSecond:(NSNotification *)notification{
NSString *name=[notification name];
NSString *object=[notification object];
NSDictionary *dict=[notification userInfo];
NSLog(@"名稱:%@----對象:%@",name,object);
NSLog(@"獲取的值:%@",[dict objectForKey:@"key"]);
}
2.消息傳遞給觀察者
[[NSNotificationCenter defaultCenter] postNotificationName:@"First" object:@"博客園-Fly_Elephant"];
NSDictionary *dict=[[NSDictionary alloc]initWithObjects:@[@"keso"] forKeys:@[@"key"]];
[[NSNotificationCenter defaultCenter] postNotificationName:@"Second" object:@"http://www.cnblogs.com/xiaofeixiang" userInfo:dict];
3.銷毀觀察者
-(void)dealloc{
NSLog(@"觀察者銷毀了");
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
也可以通過name單個刪除:
[[NSNotificationCenter defaultCenter] removeObserver:self name:@"First" object:nil];
4.運行結果
2015-04-26 15:08:25.900 CustoAlterView[2169:148380] 觀察者銷毀了
2015-04-26 15:08:29.222 CustoAlterView[2169:148380] 名稱:First----對象:博客園-Fly_Elephant
2015-04-26 15:08:29.222 CustoAlterView[2169:148380] 名稱:Second----對象:http://www.cnblogs.com/xiaofeixiang
2015-04-26 15:08:29.223 CustoAlterView[2169:148380] 獲取的值:keso
深入分析觀察者
如果想讓對象監聽某個通知,則需要在通知中心中將這個對象注冊為通知的觀察者。早先,NSNotificationCenter提供了以下方法來添加觀察者:
- (void)addObserver:(id)notificationObserver selector:(SEL)notificationSelector name:(NSString *)notificationName object:(id)notificationSender
這個方法帶有4個參數,分別指定了通知的觀察者、處理通知的回調、通知名及通知的發送對象。這里需要注意幾個問題
- notificationObserver不能為nil。
- notificationSelector回調方法有且只有一個參數(NSNotification對象)。
- 如果notificationName為nil,則會接收所有的通知(如果notificationSender不為空,則接收所有來自于notificationSender的所有通知)。如代碼清單1所示。
- 如果notificationSender為nil,則會接收所有notificationName定義的通知;否則,接收由notificationSender發送的通知。
- 監聽同一條通知的多個觀察者,在通知到達時,它們執行回調的順序是不確定的,所以我們不能去假設操作的執行會按照添加觀察者的順序來執行。
對于以上幾點,我們來重點關注一下第3條。以下代碼演示了當我們的notificationName設置為nil時,通知的監聽情況。
測試代碼如下
添加一個Observer,其中notificationName為nil
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleNotification:) name:nil object:nil];
[[NSNotificationCenter defaultCenter] postNotificationName:TEST_NOTIFICATION object:nil];
}
- (void)handleNotification:(NSNotification *)notification
{
NSLog(@"notification = %@", notification.name);
}
@end
運行后的輸出結果如下:
notification = TestNotification
notification = UIWindowDidBecomeVisibleNotification
notification = UIWindowDidBecomeKeyNotification
notification = UIApplicationDidFinishLaunchingNotification
notification = _UIWindowContentWillRotateNotification
notification = _UIApplicationWillAddDeactivationReasonNotification
notification = _UIApplicationDidRemoveDeactivationReasonNotification
notification = UIDeviceOrientationDidChangeNotification
notification = _UIApplicationDidRemoveDeactivationReasonNotification
notification = UIApplicationDidBecomeActiveNotification
可以看出,我們的對象基本上監聽了測試程序啟動后的所示消息。當然,我們很少會去這么做。
而對于第4條,使用得比較多的場景是監聽UITextField的修改事件,通常我們在一個ViewController中,只希望去監聽當前視圖中的UITextField修改事件,而不希望監聽所有UITextField的修改事件,這時我們就可以將當前頁面的UITextField對象指定為notificationSender。
NSNotification Block
在iOS 4.0之后,NSNotificationCenter為了跟上時代,又提供了一個以block方式實現的添加觀察者的方法,如下所示:
- (id<NSObject>)addObserverForName:(NSString *)name object:(id)obj queue:(NSOperationQueue *)queue usingBlock:(void (^)(NSNotification *note))block
大家第一次看到這個方法時是否會有這樣的疑問:觀察者呢?參數中并沒有指定具體的觀察者,那誰是觀察者呢?實際上,與前一個方法不同的是,前者使用一個現存的對象作為觀察者,而這個方法會創建一個匿名的對象作為觀察者(即方法返回的id<NSObject>對象),這個匿名對象會在指定的隊列(queue)上去執行我們的block。
這個方法的優點在于添加觀察者的操作與回調處理操作的代碼更加緊湊,不需要拼命滾動鼠標就能直接找到處理代碼,簡單直觀。這個方法也有幾個地方需要注意:
- name和obj為nil時的情形與前面一個方法是相同的。
- 如果queue為nil,則消息是默認在post線程中同步處理,即通知的post與轉發是在同一線程中;但如果我們指定了操作隊列,情況就變得有點意思了,我們一會再講。
- block塊會被通知中心拷貝一份(執行copy操作),以在堆中維護一個block對象,直到觀察者被從通知中心中移除。所以,應該特別注意在block中使用外部對象,避免出現對象的循環引用,這個我們在下面將舉例說明。
- 如果一個給定的通知觸發了多個觀察者的block操作,則這些操作會在各自的Operation Queue中被并發執行。所以我們不能去假設操作的執行會按照添加觀察者的順序來執行。
- 該方法會返回一個表示觀察者的對象,記得在不用時釋放這個對象。
下面我們重點說明一下第2點和第3點。
關于第2點,當我們指定一個Operation Queue時,不管通知是在哪個線程中post的,都會在Operation Queue所屬的線程中進行轉發:
代碼如下:
在指定隊列中接收通知
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
[[NSNotificationCenter defaultCenter] addObserverForName:TEST_NOTIFICATION object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *note) {
NSLog(@"receive thread = %@", [NSThread currentThread]);
}];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSLog(@"post thread = %@", [NSThread currentThread]);
[[NSNotificationCenter defaultCenter] postNotificationName:TEST_NOTIFICATION object:nil];
});
}
@end
在這里,我們在主線程里添加了一個觀察者,并指定在主線程隊列中去接收處理這個通知。然后我們在一個全局隊列中post了一個通知。我們來看下輸出結果:
post thread = <NSThread: 0x7ffe0351f5f0>{number = 2, name = (null)}
receive thread = <NSThread: 0x7ffe03508b30>{number = 1, name = main}
可以看到,消息的post與接收處理并不是在同一個線程中。如上面所提到的,如果queue為nil,則消息是默認在post線程中同步處理,大家可以試一下。
對于第3點,由于使用的是block,所以需要注意的就是避免引起循環引用的問題,如下:
block引發的循環引用問題
@interface Observer : NSObject
@property (nonatomic, assign) NSInteger i;
@property (nonatomic, weak) id<NSObject> observer;
@end
@implementation Observer
- (instancetype)init
{
self = [super init];
if (self)
{
NSLog(@"Init Observer");
// 添加觀察者
_observer = [[NSNotificationCenter defaultCenter] addObserverForName:TEST_NOTIFICATION object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *note) {
NSLog(@"handle notification");
// 使用self
self.i = 10;
}];
}
return self;
}
@end
·#pragma mark - ViewController
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
[self createObserver];
// 發送消息
[[NSNotificationCenter defaultCenter] postNotificationName:TEST_NOTIFICATION object:nil];
}
- (void)createObserver {
Observer *observer = [[Observer alloc] init];
}
@end
運行后的輸出如下:
Init Observer
handle notification
我們可以看到createObserver中創建的observer并沒有被釋放。所以,使用 – addObserverForName:object:queue:usingBlock:一定要注意這個問題。