淺談 iOS Notification

我們在開發(fā)程序的時候,程序內(nèi)不同對象間的通信是不可避免的,iOS中主要有以下這些通信方式:

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ā)出去若干個通知了...

這個時候,就需要用到通知隊列的通知合并功能了。使用NSNotificationQueueenqueueNotification: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ā)送一個信號,表明這個通知需要在目標隊列中處理,目標隊列接受到信號后,從通知隊列中取出通知并處理。

不過這個處理思路也是有一些局限性的,蘋果官方文檔中給出了一些基本代碼和思路,感興趣的同學可以看看。

Reference
最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,837評論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,196評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,688評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,654評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,456評論 6 406
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 54,955評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,044評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,195評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,725評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 40,608評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,802評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,318評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,048評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,422評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,673評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,424評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,762評論 2 372

推薦閱讀更多精彩內(nèi)容

  • 本文由我們團隊的 糾結倫 童鞋撰寫。 我們在開發(fā)程序的時候,程序內(nèi)不同對象間的通信是不可避免的,iOS中主要有以下...
    知識小集閱讀 5,096評論 1 12
  • 1.KVO,即:Key-Value Observing,它提供一種機制,當指定的對象的屬性被修改后,則對象就會接受...
    BEYOND黃閱讀 1,549評論 0 6
  • Spring Cloud為開發(fā)人員提供了快速構建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 134,781評論 18 139
  • *面試心聲:其實這些題本人都沒怎么背,但是在上海 兩周半 面了大約10家 收到差不多3個offer,總結起來就是把...
    Dove_iOS閱讀 27,184評論 30 470
  • 天邊有一個冒著蒸汽的鍋蓋。鐵銹燒得透紅發(fā)亮。撲騰撲騰地像要被揭開。海水涌著它,冒出一個一個大泡泡小泡泡。...
    弓_長弓_野閱讀 426評論 2 2