我們在開發(fā)程序的時候,程序內(nèi)不同對象間的通信是不可避免的,iOS中主要有以下這些通信方式:
圖中按照耦合度的強弱和通信的形式(一對一還是一對多)進行了劃分,這篇文章我們主要說一下Notifications。
通知機制想必大家都很熟悉,平常的開發(fā)中或多或少的應該都用過。它是Cocoa中一個非常重要的機制,能把一個事件發(fā)送給多個監(jiān)聽該事件的對象,而消息的發(fā)送者不需要知道消息接收者的任何信息,消息的接受者也只是向通知中心(NSNotificationCenter
)注冊監(jiān)聽自己感興趣的通知即可,任何對象都可以監(jiān)聽或者發(fā)送通知,這在很大程度上降低了消息發(fā)送者和接受者之間的耦合度。這也是iOS中觀察者模式的一種實現(xiàn)方式。(關于觀察者模式的另一種實現(xiàn)方式,可以看看我的這一篇文章:談談 KVO)
Notification(通知)
當我們發(fā)通知時,我們發(fā)送的是什么?答案是Notification,一個Notification對象封裝了通知發(fā)送者想要傳遞給監(jiān)聽的的信息,它有3個屬性:
@property (readonly, copy) NSString *name;
@property (nullable, readonly, retain) id object;
@property (nullable, readonly, copy) NSDictionary *userInfo;
name:通知的名稱,用來標示一個通知,一般為字符串
object:任意想要攜帶的對象,通常為發(fā)送者自己
userInfo:附加信息
通知就是以Notification的形式從通知發(fā)送者發(fā)出,到通知中心,然后再分發(fā)給所有監(jiān)聽該通知的對象的,通知監(jiān)聽者們接收到通知之后,可以獲取到傳遞過來的Notification對象,從而獲取里面封裝的一些信息,做相應的處理。
NSNotificationCenter(通知中心)
通知中心是整個通知機制的關鍵所在,它管理著監(jiān)聽者的注冊和注銷,通知的發(fā)送和接收。通知中心維護著一個通知的分發(fā)表,把所有發(fā)送者發(fā)送的通知,轉發(fā)給對應的監(jiān)聽者們。每一個iOS程序都有一個唯一的通知中心,你不必自己去創(chuàng)建一個,它是一個單例,通過[NSNotificationCenter defaultCenter]
方法獲取。
注冊監(jiān)聽者方法:
- (void)addObserver:(id)observer selector:(SEL)aSelector name:(nullable NSString *)aName object:(nullable id)anObject;
- (id <NSObject>)addObserverForName:(nullable NSString *)name object:(nullable id)obj queue:(nullable NSOperationQueue *)queue usingBlock:(void (^)(NSNotification *note))block;
第一個方法是大家常用的方法,不用多說,第二個方法帶了一個block,這個block就是通知被觸發(fā)時要執(zhí)行的block,這個block帶有一個notification參數(shù);該方法還有一個queue參數(shù),可以指定這個block在哪個隊列上執(zhí)行,如果傳nil,這個block將會在發(fā)送通知的線程中同步執(zhí)行。然后注意到,這個方法有一個id類型的返回值,這個返回值是一個不透明的中間值,用來充當監(jiān)聽者,使用時,我們需要將這個返回的監(jiān)聽者保存起來,在后面移除監(jiān)聽者的時候用到。
移除監(jiān)聽者方法:
- (void)removeObserver:(id)observer;
- (void)removeObserver:(id)observer name:(nullable NSString *)aName object:(nullable id)anObject;
在監(jiān)聽對象銷毀前,記得把該對象監(jiān)聽的通知移除掉。
發(fā)送通知方法:
- (void)postNotification:(NSNotification *)notification;
- (void)postNotificationName:(NSString *)aName object:(nullable id)anObject;
- (void)postNotificationName:(NSString *)aName object:(nullable id)anObject userInfo:(nullable NSDictionary *)aUserInfo;
還有2點需要注意的是:
- 通知中心默認是以同步的方式發(fā)送通知的,也就是說,當一個對象發(fā)送了一個通知,只有當該通知的所有接受者都接受到了通知中心分發(fā)的通知消息并且處理完成后,發(fā)送通知的對象才能繼續(xù)執(zhí)行接下來的方法。異步發(fā)送通知的方法下面會說到。
- 在一個多線程的程序中,發(fā)送方發(fā)送通知的線程通常就是監(jiān)聽者接受通知的線程,這可能和監(jiān)聽者注冊時的線程不一樣。
Cocoa中有2中通知中心,一種是NSNotificationCenter,它只能處理一個程序內(nèi)部的通知,另一種是NSDistributedNotificationCenter(mas OS上才有),它能處理一臺機器上不同程序之間的通知,由于本篇主要講iOS的通知,所以在這就不贅述,感興趣的同學可以去蘋果官方文檔里詳細了解。
NSNotificationQueue(通知隊列)
通知隊列,顧名思義,就是放通知(Notification對象)的隊列。一般的發(fā)送通知方式,通知中心收到發(fā)送者發(fā)出的通知后,會立刻分發(fā)給監(jiān)聽者,但是如果把通知放在通知隊列中,通知就可以等到某些特定時刻再發(fā)出,比如等到之前發(fā)出的通知在runloop中處理完,或者runloop空閑的時候。它就像通知中心的緩沖池,把一些不著急發(fā)出的通知存在通知隊列中。
這些存儲在通知隊列中的通知會以先進先出的方法發(fā)出(FIFO),放一個通知到達隊列的頭部,它將被通知隊列轉發(fā)給通知中心,然后通知中心再分發(fā)給相應的監(jiān)聽者們。
每個線程有一個默認的通知隊列,它和通知中心關聯(lián)著,你也可以自己為線程或者通知中心創(chuàng)建多個通知隊列。
通知隊列給通知機制提供了2個重要的特性:通知合并和異步發(fā)送通知
通知合并
有時候,對一個可能會發(fā)生不止一次的事件,你想發(fā)送一個通知去通知某些對象做一些事,但當這個事件重復發(fā)生時,你又不想再發(fā)送同樣的通知了。
你可能會這樣做,設置一個flag來決定是否還需要發(fā)送通知,當?shù)谝粋€通知發(fā)出去時,把這個flag設置為不在需要發(fā)送通知,那么當相同的事件再發(fā)生時,就不會發(fā)送相同的通知了,看起來很美好,不過這樣是不能達到我們的目的的,還是那個問題,因為普通的通知發(fā)送方式默認是同步的,通知的發(fā)送者需要等到所有的監(jiān)聽者都接收并處理完消息才能接著處理接下來的業(yè)務邏輯,也就是說當?shù)谝粋€通知發(fā)出的時候,可能還沒回來,第二個通知已經(jīng)發(fā)出去了,在你改變flag的值的時候,可能已經(jīng)發(fā)出去若干個通知了...
這個時候,就需要用到通知隊列的通知合并功能了。使用NSNotificationQueue
的enqueueNotification:postingStyle:coalesceMask:forModes:
方法,設置第三個參數(shù)coalesceMask的值,來指定不同的合并規(guī)則,coalesceMask有3個給定的值:
typedef NS_OPTIONS(NSUInteger, NSNotificationCoalescing) {
NSNotificationNoCoalescing = 0,
NSNotificationCoalescingOnName = 1,
NSNotificationCoalescingOnSender = 2
};
分別是不合并,按通知的名字合并,和按通知的發(fā)送者合并。
設置合并規(guī)則后再加入到通知隊列中,通知隊列會按照給定的合并規(guī)則,在之前入隊的通知中查找,然后移除符合合并規(guī)則的通知,這樣就達到了只發(fā)送一個通知的目的。
合并規(guī)則還可以用|
符號連接,指定多個:
NSNotification *myNotification = [NSNotification notificationWithName:@"MyNotificationName" object:nil];
[[NSNotificationQueue defaultQueue] enqueueNotification:myNotification
postingStyle:NSPostWhenIdle
coalesceMask:NSNotificationCoalescingOnName | NSNotificationCoalescingOnSender
forModes:nil];
異步發(fā)送通知
使用通知隊列的下面2個方法,將通知加到通知隊列中,就可以將一個通知異步的發(fā)送到當前的線程,這些方法調(diào)用后會立即返回,不用再等待通知的所有監(jiān)聽者都接收并處理完。
- (void)enqueueNotification:(NSNotification *)notification postingStyle:(NSPostingStyle)postingStyle;
- (void)enqueueNotification:(NSNotification *)notification postingStyle:(NSPostingStyle)postingStyle coalesceMask:(NSNotificationCoalescing)coalesceMask forModes:(nullable NSArray<NSString *> *)modes;
注意:如果通知入隊的線程在該通知被通知隊列發(fā)送到通知中心之前結束了,那么這個通知將不會被發(fā)送了。
注意到上面第二個方法中,有一個modes
參數(shù),當指定了某種特定runloop mode后,該通知值有在當前runloop為指定mode的下,才會被發(fā)出。
通知隊列發(fā)送通知有3種類型,也就是上面方法中的postingStyle
參數(shù),它有3種取值:
typedef NS_ENUM(NSUInteger, NSPostingStyle) {
NSPostWhenIdle = 1,
NSPostASAP = 2,
NSPostNow = 3
};
-
NSPostASAP (盡快發(fā)送 Posting As Soon As Possible)
以NSPostASAP風格進入隊列的通知會在運行循環(huán)的當前迭代完成時被發(fā)送給通知中心,如果當前運行循環(huán)模式和請求的模式相匹配的話(如果請求的模式和當前模式不同,則通知在進入請求的模式時被發(fā)出)。由于運行循環(huán)在每個迭代過程中可能進行多個調(diào)用分支(callout),所以在當前調(diào)用分支退出及控制權返回運行循環(huán)時,通知可能被分發(fā),也可能不被分發(fā)。其它的調(diào)用分支可能先發(fā)生,比如定時器或由其它源觸發(fā)了事件,或者其它異步的通知被分發(fā)了。
開發(fā)者通常可以將NSPostASAP風格用于開銷昂貴的資源,比如顯示服務器。如果在運行循環(huán)的一個調(diào)用分支過程中有很多客戶代碼在窗口緩沖區(qū)中進行描畫,在每次描畫之后將緩沖區(qū)的內(nèi)容刷新到顯示服務器的開銷是很昂貴的。在這種情況下,每個draw...方法都會將諸如“FlushTheServer” 這樣的通知排入隊列,并指定按名稱和對象進行合并,以及使用NSPostASAP風格。結果,在運行循環(huán)的最后,那些通知中只有一個被派發(fā),而窗口緩沖區(qū)也只被刷新一次。
NSPostWhenIdle(空閑時發(fā)送)
以NSPostWhenIdle風格進入隊列的通知只在運行循環(huán)處于等待狀態(tài)時才被發(fā)出。在這種狀態(tài)下,運行循環(huán)的輸入通道中沒有任何事件,包括定時器和異步事件。以NSPostWhenIdle風格進入隊列的一個典型的例子是當用戶鍵入文本、而程序的其它地方需要顯示文本字節(jié)長度的時候。在用戶輸入每一個字符后都對文本輸入框的尺寸進行更新的開銷是很大的(而且不是特別有用),特別是當用戶快速輸入的時候。在這種情況下,Cocoa會在每個字符鍵入之后,將諸如“ChangeTheDisplayedSize”這樣的通知進行排隊,同時把合并開關打開,并使用NSPostWhenIdle風格。當用戶停止輸入的時候,隊列中只有一個“ChangeTheDisplayedSize”通知(由于合并的原因)會在運行循環(huán)進入等待狀態(tài)時被發(fā)出,顯示部分也因此被刷新。請注意,運行循環(huán)即將退出(當所有的輸入通道都過時的時候,會發(fā)生這種情況)時并不處于等待狀態(tài),因此也不會發(fā)出通知。NSPostNow(立即發(fā)送)
以NSPostNow風格進入隊列的通知會在合并之后,立即發(fā)送到通知中心。開發(fā)者可以在不需要異步調(diào)用行為的時候 使用NSPostNow風格(或者通過NSNotificationCenter的postNotification:方法來發(fā)送)。在很多編程環(huán)境下,我們不僅允許同步的行為,而且希望使用這種行為:即開發(fā)者希望通知中心在通知派發(fā)之后返回,以便確定觀察者對象收到通知并進行了處理。當然,當開發(fā)者希望通過合并移除隊列中類似的通知時,應該用enqueueNotification...方法,且使用NSPostNow風格,而不是使用postNotification:方法。
發(fā)送通知到指定線程
通知中心分發(fā)通知的線程一般就是通知的發(fā)出者發(fā)送通知的線程。但是有時候,你可能想自己決定通知發(fā)出的線程,而不是由通知中心來決定。舉個栗子,在后臺線程中有一個對象監(jiān)聽者主線程中界面上的一些變化,比如一個window的關閉或者一個按鈕的點擊,這時通知是在主線程中發(fā)出的,通常來說只能在主線程中接受,但是你會希望這個對象能在后臺線程中接到通知,而不是主線程中。這時你就需要在這些通知本來在的線程中抓住它們,然后將它們重定向到你想要指定的線程。
一種實現(xiàn)思路就是實現(xiàn)自定義的通知隊列(不是NSNotificationQueue
)去保存那些通知,然后將他們重定向到你指定的線程,大致流程就是:照常用一個對象去監(jiān)聽一個通知,當這個通知被觸發(fā)時,監(jiān)聽者接受到后,判斷當前線程是否為處理該通知正確的線程,如果是則處理,否則,將該通知保存到我們自定義的通知隊列中,然后給目標隊列發(fā)送一個信號,表明這個通知需要在目標隊列中處理,目標隊列接受到信號后,從通知隊列中取出通知并處理。
不過這個處理思路也是有一些局限性的,蘋果官方文檔中給出了一些基本代碼和思路,感興趣的同學可以看看。