部分內容轉自:http://www.jianshu.com/p/2503e3e5fc64
一、NSNotificationCenter(通知中心)
通知中心是整個通知機制的關鍵所在,它管理着監聽者的注冊和注銷,通知的發送和接收。通知中心維護着一個通知的分發表,把所有通知發送者
發送的通知,轉發給對應的監聽者們。Cocoa中有2種通知中心,一種是NSNotificationCenter
,它只能處理一個程序內部的通知,另一種是NSDistributedNotificationCenter
(mas OS上才有),它能處理一台機器上不同程序之間的通知。
NSDistributedNotificationCenter
每個任務都有一個缺省的分布式通告中心,您可以通過NSDistributedNotificationCenter
的defaultCenter
類方法來訪問。這個分布式通告中心負責處理同一個機器的不通任務之間的通告。如果需要實現不同機器上的任務間通訊,請使用分布式對象。
發送分布式通告是一個開銷昂貴的操作。通告會被發送給一個系統級別的服務器,然后再分發到注冊了該分布式通告的對象所在的任務中。發送通告和通告到達另一個任務之間的延遲是很大的。事實上,如果發出的通告太多,以致於充滿了服務器的隊列,就可能被丟棄。
分布式通告通過任務的運行循環來分發。任務必須運行在某種“常見”模式的運行循環下,比如NSDefaultRunLoopMode
模式,才能接收分布式通告。如果接收通告的任務是多線程的,則不要以通告會到達主線程作為前提。通告通常被分發到主線程的運行循環上,但是其它線程也可以接收通告。
盡管常規的通告中心允許任何對象作為通告對象(也就是通告封裝的對象),分布式通告中心只支持將NSString
對象作為它的通告對象。由於發出通告的對象和通告的觀察者可能位於不同的任務中,通告不能包含指向任意對象的指針。因此,分布式通告中心要求通告使用字符串作為通告對象。通告的匹配就是基於這個字符串進行的,而不是基於對象指針。
1、應用1:當收到“PiaoYun Notification”通知時候,會發出“ShowWindow Notification”通知
1 - (void)callbackWithNotification:(NSNotification *)myNotification; 2 { 3 if([myNotification.name isEqualToString:@"PiaoYun Notification"]){ 4 NSString *observedObject = @"com.chinapyg.notification"; 5 NSDistributedNotificationCenter *center = 6 [NSDistributedNotificationCenter defaultCenter]; 7 [center postNotificationName: @"ShowWindow Notification" 8 object: observedObject 9 userInfo: nil /* no dictionary */ 10 deliverImmediately: NO]; 11 } 12 NSLog(@"Notification Received"); 13 } 14 15 - (void)applicationDidFinishLaunching:(NSNotification *)aNotification { 16 NSString *observedObject = @"com.chinapyg.notification"; 17 // 處理單個計算機上不同的進程之間的通知 18 NSDistributedNotificationCenter *center = 19 [NSDistributedNotificationCenter defaultCenter]; 20 [center addObserver: self 21 selector: @selector(callbackWithNotification:) 22 name: nil/*@"PiaoYun Notification"*/ 23 object: observedObject]; 24 }
- (void)callbackWithNotification:(NSNotification *)myNotification; { if([myNotification.name isEqualToString:@"ShowWindow Notification"]){ dispatch_async(dispatch_get_main_queue(), ^{ self.testWindowController = [[TestWindowController alloc]initWithWindowNibName:@"TestWindowController"]; [self.testWindowController showWindow:self]; }); } NSLog(@"Notification Received1"); } - (void)applicationDidFinishLaunching:(NSNotification *)aNotification { [self.window close]; NSDistributedNotificationCenter *center = [NSDistributedNotificationCenter defaultCenter]; NSString *observedObject = @"com.chinapyg.notification"; [center addObserver: self selector: @selector(callbackWithNotification:) name: nil/*@"PiaoYun Notification"*/ object: observedObject]; NSLog(@"notification send"); [center postNotificationName: @"PiaoYun Notification" object: observedObject userInfo: nil /* no dictionary */ deliverImmediately: NO]; }
2、應用程序2運行起來,先發送“PiaoYun Notification”通知,當收到“ShowWindow Notification”時候,顯示窗口。
二、NSNotificationQueue(通知隊列)
通知隊列,顧名思義,就是放通知(Notification對象)的隊列。一般的發送通知方式,通知中心收到發送者發出的通知后,會立刻分發給監聽者,但是如果把通知放在通知隊列中,通知就可以等到某些特定時刻再發出,比如等到之前發出的通知在runloop中處理完,或者runloop空閑的時候。它就像通知中心的緩沖池,把一些不着急發出的通知存在通知隊列中。

這些存儲在通知隊列中的通知會以先進先出的方法發出(FIFO),放一個通知到達隊列的頭部,它將被通知隊列轉發給通知中心,然后通知中心再分發給相應的監聽者們。
每個線程有一個默認的通知隊列,它和通知中心關聯着,你也可以自己為線程或者通知中心創建多個通知隊列。
通知隊列給通知機制提供了2個重要的特性:通知合並和異步發送通知
通知合並
有時候,對一個可能會發生不止一次的事件,你想發送一個通知去通知某些對象做一些事,但當這個事件重復發生時,你又不想再發送同樣的通知了。
你可能會這樣做,設置一個flag來決定是否還需要發送通知,當第一個通知發出去時,把這個flag設置為不在需要發送通知,那么當相同的事件再發生時,就不會發送相同的通知了,看起來很美好,不過這樣是不能達到我們的目的的,還是那個問題,因為普通的通知發送方式默認是同步的,通知的發送者需要等到所有的監聽者都接收並處理完消息才能接着處理接下來的業務邏輯,也就是說當第一個通知發出的時候,可能還沒回來,第二個通知已經發出去了,在你改變flag的值的時候,可能已經發出去若干個通知了...
這個時候,就需要用到通知隊列的通知合並功能了。使用NSNotificationQueue
的enqueueNotification:postingStyle:coalesceMask:forModes:
方法,設置第三個參數coalesceMask的值,來指定不同的合並規則,coalesceMask有3個給定的值:
typedef NS_OPTIONS(NSUInteger, NSNotificationCoalescing) { NSNotificationNoCoalescing = 0, NSNotificationCoalescingOnName = 1, NSNotificationCoalescingOnSender = 2 };
分別是不合並,按通知的名字合並,和按通知的發送者合並。
設置合並規則后再加入到通知隊列中,通知隊列會按照給定的合並規則,在之前入隊的通知中查找,然后移除符合合並規則的通知,這樣就達到了只發送一個通知的目的。
合並規則還可以用|
符號連接,指定多個:
NSNotification *myNotification = [NSNotification notificationWithName:@"MyNotificationName" object:nil]; [[NSNotificationQueue defaultQueue] enqueueNotification:myNotification postingStyle:NSPostWhenIdle coalesceMask:NSNotificationCoalescingOnName | NSNotificationCoalescingOnSender forModes:nil];
異步發送通知
使用通知隊列的下面2個方法,將通知加到通知隊列中,就可以將一個通知異步的發送到當前的線程,這些方法調用后會立即返回,不用再等待通知的所有監聽者都接收並處理完。
- (void)enqueueNotification:(NSNotification *)notification postingStyle:(NSPostingStyle)postingStyle; - (void)enqueueNotification:(NSNotification *)notification postingStyle:(NSPostingStyle)postingStyle coalesceMask:(NSNotificationCoalescing)coalesceMask forModes:(nullable NSArray<NSString *> *)modes;
注意:如果通知入隊的線程在該通知被通知隊列發送到通知中心之前結束了,那么這個通知將不會被發送了。
注意到上面第二個方法中,有一個modes
參數,當指定了某種特定runloop mode后,該通知值有在當前runloop為指定mode的下,才會被發出。
通知隊列發送通知有3種類型,也就是上面方法中的postingStyle
參數,它有3種取值:
typedef NS_ENUM(NSUInteger, NSPostingStyle) { NSPostWhenIdle = 1, NSPostASAP = 2, NSPostNow = 3 };
-
NSPostASAP (盡快發送 Posting As Soon As Possible)
以NSPostASAP風格進入隊列的通知會在運行循環的當前迭代完成時被發送給通知中心,如果當前運行循環模式和請求的模式相匹配的話(如果請求的模式和當前模式不同,則通知在進入請求的模式時被發出)。由於運行循環在每個迭代過程中可能進行多個調用分支(callout),所以在當前調用分支退出及控制權返回運行循環時,通知可能被分發,也可能不被分發。其它的調用分支可能先發生,比如定時器或由其它源觸發了事件,或者其它異步的通知被分發了。開發者通常可以將NSPostASAP風格用於開銷昂貴的資源,比如顯示服務器。如果在運行循環的一個調用分支過程中有很多客戶代碼在窗口緩沖區中進行描畫,在每次描畫之后將緩沖區的內容刷新到顯示服務器的開銷是很昂貴的。在這種情況下,每個draw...方法都會將諸如“FlushTheServer” 這樣的通知排入隊列,並指定按名稱和對象進行合並,以及使用NSPostASAP風格。結果,在運行循環的最后,那些通知中只有一個被派發,而窗口緩沖區也只被刷新一次。
-
NSPostWhenIdle(空閑時發送)
以NSPostWhenIdle風格進入隊列的通知只在運行循環處於等待狀態時才被發出。在這種狀態下,運行循環的輸入通道中沒有任何事件,包括定時器和異步事件。以NSPostWhenIdle風格進入隊列的一個典型的例子是當用戶鍵入文本、而程序的其它地方需要顯示文本字節長度的時候。在用戶輸入每一個字符后都對文本輸入框的尺寸進行更新的開銷是很大的(而且不是特別有用),特別是當用戶快速輸入的時候。在這種情況下,Cocoa會在每個字符鍵入之后,將諸如“ChangeTheDisplayedSize”這樣的通知進行排隊,同時把合並開關打開,並使用NSPostWhenIdle風格。當用戶停止輸入的時候,隊列中只有一個“ChangeTheDisplayedSize”通知(由於合並的原因)會在運行循環進入等待狀態時被發出,顯示部分也因此被刷新。請注意,運行循環即將退出(當所有的輸入通道都過時的時候,會發生這種情況)時並不處於等待狀態,因此也不會發出通知。
-
NSPostNow(立即發送)
以NSPostNow風格進入隊列的通知會在合並之后,立即發送到通知中心。開發者可以在不需要異步調用行為的時候 使用NSPostNow風格(或者通過NSNotificationCenter的postNotification:方法來發送)。在很多編程環境下,我們不僅允許同步的行為,而且希望使用這種行為:即開發者希望通知中心在通知派發之后返回,以便確定觀察者對象收到通知並進行了處理。當然,當開發者希望通過合並移除隊列中類似的通知時,應該用enqueueNotification...方法,且使用NSPostNow風格,而不是使用postNotification:方法。
發送通知到指定線程
通知中心分發通知的線程一般就是通知的發出者發送通知的線程。但是有時候,你可能想自己決定通知發出的線程,而不是由通知中心來決定。舉個栗子,在后台線程中有一個對象監聽者主線程中界面上的一些變化,比如一個window的關閉或者一個按鈕的點擊,這時通知是在主線程中發出的,通常來說只能在主線程中接受,但是你會希望這個對象能在后台線程中接到通知,而不是主線程中。這時你就需要在這些通知本來在的線程中抓住它們,然后將它們重定向到你想要指定的線程。
一種實現思路就是實現自定義的通知隊列(不是NSNotificationQueue
)去保存那些通知,然后將他們重定向到你指定的線程,大致流程就是:照常用一個對象去監聽一個通知,當這個通知被觸發時,監聽者接受到后,判斷當前線程是否為處理該通知正確的線程,如果是則處理,否則,將改通知保存到我們自定義的通知隊列中,然后給目標隊列發送一個信號,表明這個通知需要在目標隊列中處理,目標隊列接受到信號后,從通知隊列中取出通知並處理。