Foundation:NSNotificationCenter淺談

一個NSNotificationCenter對象(通知中心)提供了在程序中廣播消息測機制,它實質上就是一個通知分發(fā)表。這個分發(fā)表負責維護為各個通知注冊的觀察者,并在通知到達時,去查找相應的觀察者,將通知轉發(fā)給他們進行處理。

本文主要整理了一下NSNotificationCenter的使用及需要注意的一些問題,并提出了一些未解決的問題,希望能在此得到解答。


獲取通知中心

每個程序都會有一個默認的通知中心。為此,NSNotificationCenter提供了一個類方法來獲取這個通知中心。

+ (NSNotificationCenter*)defaultCenter;

獲取了這個默認的通知中心對象后,我們就可以使用它來處理通知相關的操作了,包括注冊觀察者,移除觀察者,發(fā)送通知等。

通常如果不是出于必要,我們一般都使用這個默認的通知中心,而不自己創(chuàng)建維護一個通知中心。

添加觀察者

- (void)addObserver:(id)observer selector:(SEL)aSelector name:(nullable NSString*)aName object:(nullable id)anObject;

這個方法帶有4個參數,分別指定了通知的觀察者、處理通知的回調、通知名及通知的發(fā)送對象。這里需要注意幾個問題:

observer不能為nil。 ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? aSelector回調方法有且只有一個參數(NSNotification對象)。

如果aName為nil,則會接收所有的通知(如果anObject不為空,則接收所有來自于anObject的所有通知)。

如代碼清單1所示。

如果anObject為nil,則會接收所有aName定義的通知;否則,接收由anObject發(fā)送通知。

監(jiān)聽同一個通知的多個觀察者,在通知到達時,他們執(zhí)行回調的順序是不正確的,所以我們不能去假設操作的執(zhí)行會按照添加觀察者的順序來執(zhí)行。

對于以上幾點,我們來重點關注一下第3條。以下代碼演示了當我們的aName設置為nil時,通知的監(jiān)聽情況。

代碼清單1:添加一個Observer,其中aName為nil

@implementationfirstViewController

- (void)viewDidLoad {

[superviewDidLoad];

[[NSNotificationCenter defaultCenter]addObserver:self selector:@selector(notificationAction:)name:nil object:nil];

[[NSNotificationCenter defaultCenter]postNotificationName:TEST_NOTIFICATION object:nil];

}

-(void)notificationAction:(NSNotification*)sender

{

NSLog(@"sender = %@",sender);

}

運行后的輸出結果如下:

sender=TestNotification

sender=UIWindowDidBecomeVisibleNotification

sender=UIWindowDidBecomeKeyNotification

sender=UIApplicationDidFinishLaunchingNotification

sender=_UIWindowContentWillRotateNotification

sender=_UIApplicationWillAddDeactivationReasonNotification

sender=_UIApplicationDidRemoveDeactivationReasonNotification

sender=UIDeviceOrientationDidChangeNotification

sender=_UIApplicationDidRemoveDeactivationReasonNotification

sender=UIApplicationDidBecomeActiveNotification

可以看出,我們的對象基本上監(jiān)聽了測試程序啟動后的所示消息。當然,我們很少會去這么做。

而對于第4條,使用得比較多的場景是監(jiān)聽UITextField的修改事件,通常我們在一個ViewController中,只希望去監(jiān)聽當前視圖中的UITextField修改事件,而不希望監(jiān)聽所有UITextField的修改事件,這時我們就可以將當前頁面的UITextField對象指定為anObject。

在iOS 4.0之后,NSNotificationCenter為了跟上時代,又提供了一個以block方式實現的添加觀察者的方法,如下所示:

- (id)addObserverForName:(nullableNSString*)name object:(nullable id)obj queue:(nullableNSOperationQueue*)queue usingBlock:(void(^)(NSNotification*note))block

大家第一次看到這個方法時是否會有這樣的疑問:觀察者呢?參數中并沒有指定具體的觀察者,那誰是觀察者呢?實際上,與前一個方法不同的是,前者使用一個現存的對象作為觀察者,而這個方法會創(chuàng)建一個匿名的對象作為觀察者(即方法返回的id對象),這個匿名對象會在指定的隊列(queue)上去執(zhí)行我們的block。

這個方法的優(yōu)點在于添加觀察者的操作與回調處理操作的代碼更加緊湊,不需要拼命滾動鼠標就能直接找到處理代碼,簡單直觀。這個方法也有幾個地方需要注意:

name和obj為nil時的情形與前面一個方法是相同的。

如果queue為nil,則消息是默認在post線程中同步處理,即通知的post與轉發(fā)是在同一線程中;但如果我們指定了操作隊列,情況就變得有點意思了,我們一會再講。

block塊會被通知中心拷貝一份(執(zhí)行copy操作),以在堆中維護一個block對象,直到觀察者被從通知中心中移除。所以,應該特別注意在block中使用外部對象,避免出現對象的循環(huán)引用,這個我們在下面將舉例說明。

如果一個給定的通知觸發(fā)了多個觀察者的block操作,則這些操作會在各自的Operation Queue中被并發(fā)執(zhí)行。所以我們不能去假設操作的執(zhí)行會按照添加觀察者的順序來執(zhí)行。

該方法會返回一個表示觀察者的對象,記得在不用時釋放這個對象。

下面我們重點說明一下第2點和第3點。

關于第2點,當我們指定一個Operation Queue時,不管通知是在哪個線程中post的,都會在Operation Queue所屬的線程中進行轉發(fā),如代碼清單2所示:

代碼清單2:在指定隊列中接收通知

@implementationViewController

-(void)viewDidLoad{

[super viewDidLoad];

[[NSNotificationCenter defaultCenter]addObserverForName:TEST_NOTIFICATIONobject: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_NOTIFICATIONobject:nil];

});

}

在這里,我們在主線程里添加了一個觀察者,并指定在主線程隊列中去接收處理這個通知。然后我們在一個全局隊列中post了一個通知。我們來看下輸出結果:

post thread=NSThread:0x7ffe0351f5f0>{number=2,name=(null)}

receive thread=NSThread:0x7ffe03508b30>{number=1,name=main}

可以看到,消息的post與接收處理并不是在同一個線程中。如上面所提到的,如果queue為nil,則消息是默認在post線程中同步處理,大家可以試一下。

對于第3點,由于使用的是block,所以需要注意的就是避免引起循環(huán)引用的問題,如代碼清單3所示:

代碼清單3:block引發(fā)的循環(huán)引用問題

@interfaceObserver: NSObject

@property(nonatomic,assign)NSInteger i;

@property(nonatomic,weak)idNSObject>observer;

@end

@implementationObserver

-(instancetype)init

{

self=[super init];

if(self)

{

NSLog(@"Init Observer");

// 添加觀察者

_observer=[[NSNotificationCenter defaultCenter]addObserverForName:TEST_NOTIFICATIONobject:nil queue:[NSOperationQueue mainQueue]usingBlock:^(NSNotification*note){

NSLog(@"handle notification");

// 使用self

self.i=10;

}];

}

return self;

}

@end

#pragma mark - ViewController

@implementationViewController

-(void)viewDidLoad{

[superviewDidLoad];

[self createObserver];

// 發(fā)送消息

[[NSNotificationCenter defaultCenter]postNotificationName:TEST_NOTIFICATION object:nil];

}

-(void)createObserver{

Observer*observer=[[Observer alloc]init];

}

@end

運行后的輸出如下:

Init Observer

handle notification

我們可以看到createObserver中創(chuàng)建的observer并沒有被釋放。所以,使用addObserverForName:object:queue:usingBlock:一定要注意這個問題。

移除觀察者

與注冊觀察者相對應的,NSNotificationCenter為我們提供了兩個移除觀察者的方法。它們的定義如下:

-(void)removeObserver:(id) notificationObserver

-(void)removeObserver:(id)notification Observername:(NSString*)notificationName object:(id)notificationSender

前一個方法會將notificationObserver從通知中心中移除,這樣notificationObserver就無法再監(jiān)聽任何消息。而后一個會根據三個參數來移除相應的觀察者。

這兩個方法也有幾點需要注意:

由于注冊觀察者時(不管是哪個方法),通知中心會維護一個觀察者的弱引用,所以在釋放對象時,要確保移除對象所有監(jiān)聽的通知。否則,可能會導致程序崩潰或一些莫名其妙的問題。

對于第二個方法,如果notificationName為nil,則會移除所有匹配notificationObserver和notificationSender的通知,同理notificationSender也是一樣的。而如果notificationName和notificationSender都為nil,則其效果就與第一個方法是一樣的了。大家可以試一下。

最有趣的應該是這兩個方法的使用時機。–removeObserver:適合于在類的dealloc方法中調用,這樣可以確保將對象從通知中心中清除;而在viewWillDisappear:這樣的方法中,則適合于使用-removeObserver:name:object:方法,以避免不知情的情況下移除了不應該移除的通知觀察者。例如,假設我們的ViewController繼承自一個類庫的某個ViewController類(假設為SKViewController吧),可能SKViewController自身也監(jiān)聽了某些通知以執(zhí)行特定的操作,但我們使用時并不知道。如果直接在viewWillDisappear:中調用–removeObserver:,則也會把父類監(jiān)聽的通知也給移除。

關于注冊監(jiān)聽者,還有一個需要注意的問題是,每次調用addObserver時,都會在通知中心重新注冊一次,即使是同一對象監(jiān)聽同一個消息,而不是去覆蓋原來的監(jiān)聽。這樣,當通知中心轉發(fā)某一消息時,如果同一對象多次注冊了這個通知的觀察者,則會收到多個通知,如代碼清單4所示:

代碼清單4:同一對象多次注冊同一消息

其輸出結果如下所示:

notification=TestNotification

notification=TestNotification

可以看到對象處理了兩次通知。所以,如果我們需要在viewWillAppear監(jiān)聽一個通知時,一定要記得在對應的viewWillDisappear里面將觀察者移除,否則就可能會出現上面的情況。

最后,再特別重點強調的非常重要的一點是,在釋放對象前,一定要記住如果它監(jiān)聽了通知,一定要將它從通知中心移除。如果是用addObserverForName:object:queue:usingBlock:,也記得一定得移除這個匿名觀察者。說白了就一句話,添加和移除要配對出現。

POST消息

注冊了通知觀察者,我們便可以隨時隨地的去post一個通知了(當然,如果閑著沒事,也可以不注冊觀察者,post通知隨便玩,只是沒人理睬罷了)。NSNotificationCenter提供了三個方法來post一個通知,如下所示:

-postNotification:

–postNotificationName:object:

–postNotificationName:object:userInfo:

我們可以根據需要指定通知的發(fā)送者(object)并附帶一些與通知相關的信息(userInfo),當然這些發(fā)送者和userInfo可以封裝在一個NSNotification對象中,由- postNotification:來發(fā)送。注意,- postNotification:的參數不能為空,否則會引發(fā)一個異常,如下所示:

***Terminating app due to uncaught exception'NSInvalidArgumentException',reason:'*** -[NSNotificationCenter postNotification:]: notification is nil'

每次post一個通知時,通知中心都會去遍歷一下它的分發(fā)表,然后將通知轉發(fā)給相應的觀察者。

另外,通知的發(fā)送與處理是同步的,在某個地方post一個消息時,會等到所有觀察者對象執(zhí)行完處理操作后,才回到post的地方,繼續(xù)執(zhí)行后面的代碼。如代碼清單5所示:

代碼清單5:通知的同步處理

@implementationViewController

-(void)viewDidLoad{

[superviewDidLoad];

[[NSNotificationCenter defaultCenter]addObserver:self selector:@selector(handleNotification:) name:TEST_NOTIFICATION object:nil];

[[NSNotificationCenter defaultCenter]postNotificationName:TEST_NOTIFICATION object:nil];

NSLog(@"continue");

}

-(void)handleNotification:(NSNotification*)notification

{

NSLog(@"handle notification");

}

@end

運行后輸出結果是:

handle notification

continue

一些思考

翻了好些資料,還有兩個問題始終沒有明確的答案。

首先就是通知中心是如何維護觀察者對象的。可以明確的是,添加觀察者時,通知中心沒有對觀察者做retain操作,即不會使觀察者的引用計數加1。那通知中心維護的是觀察者的weak引用呢還是unsafe_unretained引用呢?

個人認為可能是unsafe_unretained的引用,因為我們知道如果是weak引用,其所指的對象被釋放后,這個引用會被置成nil。而實際情況是通知中心還會給這個對象發(fā)送消息,并引發(fā)一個異常。而如果向nil發(fā)送一個消息是不會導致異常的。

另外,我們知道NSNotificationCenter實現的是觀察者模式,而且通常情況下消息在哪個線程被post,就在哪個線程被轉發(fā)。而從上面的描述可以發(fā)現,

-addObserverForName:object:queue:usingBlock:添加的匿名觀察者可以在指定的隊列中處理通知,那它的實現機制是什么呢?

小結

在我們的應用程序中,一個大的話題就是兩個對象之間如何通信。我們需要根據對象之間的關系來確定采用哪一種通信方式。對象之間的通信方式主要有以下幾種:

直接方法調用

Target-Action

Delegate

回調(block)

KVO

通知

一般情況下,我們可以根據以下兩點來確定使用哪種方式:

通信對象是一對一的還是一對多的

對象之間的耦合度,是強耦合還是松耦合

Objective-C中的通知由于其廣播性及松耦合性,非常適合于大的范圍內對象之間的通信(模塊與模塊,或一些框架層級)。通知使用起來非常方便,也正因為如此,所以容易導致濫用。所以在使用前還是需要多想想,是否有更好的方法來實現我們所需要的對象間通信。畢竟,通知機制會在一定程度上會影響到程序的性能。

對于使用NSNotificationCenter,最后總結一些小建議:

在需要的地方使用通知。

注冊的觀察者在不使用時一定要記得移除,即添加和移除要配對出現。

盡可能遲地去注冊一個觀察者,并盡可能早將其移除,這樣可以改善程序的性能。因為,每post一個通知,都會是遍歷通知中心的分發(fā)表,確保通知發(fā)給每一個觀察者。

記住通知的發(fā)送和處理是在同一個線程中。

使用-addObserverForName:object:queue:usingBlock:務必處理好內存問題,避免出現循環(huán)引用。

NSNotificationCenter是線程安全的,但并不意味著在多線程環(huán)境中不需要關注線程安全問題。不恰當的使用仍然會引發(fā)線程問題。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 轉載自南峰子的技術博客 一個NSNotificationCenter對象(通知中心)提供了在程序中廣播消息的機制,...
    我消失1314閱讀 916評論 0 2
  • 一個NSNotificationCenter對象(通知中心)提供了在程序中廣播消息的機制,它實質上就是一個通知分發(fā)...
    陳云峰閱讀 1,172評論 0 1
  • NSNotificationCenter對象(通知中心)提供了在程序中廣播消息的機制,它實質上就是一個通知分發(fā)表。...
    9de75b652cd9閱讀 763評論 0 1
  • 概述 在多數移動應用中任何時候都只能有一個應用程序處于活躍狀態(tài),如果其他應用此刻發(fā)生了一些用戶感興趣的那么通過通知...
    莫離_焱閱讀 6,554評論 1 8
  • 寂寞到連個說話的人都沒有,為什么呢,因為無人理解。
    毛叁閱讀 250評論 0 0