NSNotificationCenter 通知中心

1.29 通過 NSNotificationCenter 發送通知

問題

你想在你的應用程序中廣播一個事件,并允許任何愿意收聽的對象(取決于廣播的通知)采取行動。

解決方案

使用默認的通知中心 NSNotificationCenterpostNotificationName:object:userInfo: 方法來發布一個通知,該通知攜帶一個對象(通常是發布通知的對象)和一個用戶信息字典,該字典可以攜帶關于通知和/或發布通知的對象的額外信息。

討論

通知中心是通知對象的調度中心。例如,當用戶在你的應用程序內的任何地方彈出鍵盤時,iOS 會向你的應用程序發送一個通知。你的應用程序中任何愿意收聽此通知的對象都可以將自己添加到默認的通知中心,作為該特定通知的觀察者。一旦你的對象的生命周期結束,它必須從通知中心的調度表中刪除自己。因此,通知是一條通過通知中心廣播給觀察者的消息。

通知中心是 NSNotificationCenter 類型的一個實例。我們使用 NSNotificationCenter 的類方法 defaultCenter 來獲取系統默認的通知中心對象。

通知是 NSNotification 類型的對象。一個通知對象有一個名字(指定為 NSString 類型),并且可以攜帶兩個關鍵信息:

備注

你可以指定你的通知的名稱。你不需要為此使用 API。只要確保你的通知名稱是唯一的,不會與系統通知發生沖突。

  • Sender Object

    這是發布通知的對象的實例。觀察者可以使用 NSNotification 類的對象實例方法訪問這個對象。

  • User-Info Dictionary

    這是一個(發送者對象可以創建并與通知對象一起發送的)可選的字典。這個字典通常包含關于通知的更多信息。例如,當你的應用程序內的任何組件的鍵盤即將在 iOS 中得到顯示時,iOS 會將 UIKeyboardWillShowNotification 通知發送到默認的通知中心。這個通知的用戶信息字典中包含了一些值,例如動畫前后的鍵盤矩形以及鍵盤的動畫持續時間。利用這些數據,觀察者可以做出決定,例如,一旦鍵盤顯示在屏幕上,該如何處理可能會被阻擋的 UI 組件。

警告

通知是實現代碼解耦的一個好方法。我的意思是,使用通知,你可以擺脫完成處理程序(completion handlers)和委托(delegation)。然而,關于通知有一個潛在的注意事項:它們不會被立即交付。它們是由通知中心派發的,而 NSNotificationCenter 的默認實現對應用程序的程序員是隱藏的。傳遞有時可能會延遲幾毫秒,或者在極端情況下(我從未遇到過),延遲幾秒鐘。因此,應該由你來決定在哪里使用通知,在哪里不使用通知。

為了構造一個 NSNotification 類型的通知,需要使用 NSNotificationnotificationWithName:object:userInfo: 類方法,我們很快會看到了。

備注

最好使用 Notification 這個單詞來作為你的通知名稱的后綴。例如,你當然可以給你的通知起一個類似 ResultOfAppendingTwoStrings 的名字。不過最好是起一個像 ResultOfAppendingTwoStringsNotification 這樣的名字,因為它清楚地表明了這個名字的歸屬。

讓我們來看一個例子。我們將簡單地取一個名字和一個姓氏,將它們拼接來創建一個字符串(名字+姓氏),然后使用默認的通知中心廣播這個結果。我們將在用戶啟動我們的應用時,在我們的應用委托的實現中完成這一工作:

#import "AppDelegate.h"

@implementation AppDelegate

/* The notification name */
const NSString *ResultOfAppendingTwoStringsNotification =
                @"ResultOfAppendingTwoStringsNotification";

/* Keys inside the dictionary that our notification sends */
const NSString
  *ResultOfAppendingTwoStringsFirstStringInfoKey = @"firstString";

const NSString
  *ResultOfAppendingTwoStringsSecondStringInfoKey = @"secondString";

const NSString
  *ResultOfAppendingTwoStringsResultStringInfoKey = @"resultString";

- (BOOL)            application:(UIApplication *)application
  didFinishLaunchingWithOptions:(NSDictionary *)launchOptions{

  NSString *firstName = @"Anthony";
  NSString *lastName = @"Robbins";
  NSString *fullName = [firstName stringByAppendingString:lastName];

  NSArray *objects = [[NSArray alloc] initWithObjects:
                      firstName,
                      lastName,
                      fullName,
                      nil];

  NSArray *keys = [[NSArray alloc] initWithObjects:
                   ResultOfAppendingTwoStringsFirstStringInfoKey,
                   ResultOfAppendingTwoStringsSecondStringInfoKey,
                   ResultOfAppendingTwoStringsResultStringInfoKey,
                   nil];

  NSDictionary *userInfo = [[NSDictionary alloc] initWithObjects:objects
                                                         forKeys:keys];

  NSNotification *notificationObject =
  [NSNotification
   notificationWithName:(NSString *)ResultOfAppendingTwoStringsNotification
   object:self
   userInfo:userInfo];

  [[NSNotificationCenter defaultCenter] postNotification:notificationObject];

  self.window = [[UIWindow alloc] initWithFrame:
                 [[UIScreen mainScreen] bounds]];
  self.window.backgroundColor = [UIColor whiteColor];
  [self.window makeKeyAndVisible];
  return YES;
}

當然,你不必為每一個你想廣播的通知指定一個對象或一個用戶信息字典。但是,如果你和一個團隊的開發人員在同一個應用程序上工作,或者你正在編寫一個靜態庫,我建議你完整地記錄你的通知,并清楚地指出你的通知是否攜帶一個對象和/或一個用戶信息字典。如果有,你必須說明每個通知攜帶什么對象,以及用戶信息字典里有什么鍵和值。如果你不打算發送對象或用戶信息字典,那么我建議你使用 NSNotificationCenter 的實例方法 postNotificationName:object: 。指定一個代表你的通知名稱的字符串作為第一個參數,第二個參數是 nil,它是應該與通知一起被攜帶的對象。下面是一個例子:

#import "AppDelegate.h"

@implementation AppDelegate

/* The notification name */
const NSString *NetworkConnectivityWasFoundNotification =
              @"NetworkConnectivityWasFoundNotification";

- (BOOL)            application:(UIApplication *)application
  didFinishLaunchingWithOptions:(NSDictionary *)launchOptions{

  [[NSNotificationCenter defaultCenter]
   postNotificationName:(NSString *)NetworkConnectivityWasFoundNotification
   object:nil];

  self.window = [[UIWindow alloc] initWithFrame:
                 [[UIScreen mainScreen] bounds]];
  self.window.backgroundColor = [UIColor whiteColor];
  [self.window makeKeyAndVisible];
  return YES;
}

1.30 監聽來自 NSNotificationCenter 的通知

問題

你想使用 NSNotificationCenter 監聽各種系統廣播通知和自定義廣播通知。

解決方案

在一個通知被廣播之前,使用 NSNotificationCenter 的實例方法 addObserver:selector:name:object: 將你的觀察者對象添加到通知中心。要停止監聽一個通知,使用 NSNotificationCenter 的實例方法 removeObserver:name:object: 并傳遞你的觀察者對象,然后是你想停止觀察的通知的名稱和你最初訂閱的對象(這將在本章節的討論部分詳細解釋)。

討論

任何對象都可以廣播通知,同一應用中的任何對象也都可以選擇監聽特定名稱的通知。兩個具有相同名稱的通知可以被廣播,但它們必須來自兩個不同的對象。例如,你可以有一個名稱為 DOWNLOAD_COMPLETED 的通知,從兩個類中觸發,一個用于從互聯網上下載圖片的下載管理器,另一個是從連接到 iOS 設備的附件中下載數據的下載管理器。觀察者可能只對來自特定對象的通知感興趣;例如,從附件中下載數據的下載管理器。你可以在開始監聽通知時,使用通知中心的 addObserver:selector:name:object: 方法的對象參數,指定這個源對象(廣播者)。

下面是 addObserver:selector:name:object: 實例方法接受的每個參數的簡要描述:

  • addObserver:接收通知的對象(觀察者)。
  • selector:當通知被廣播并被觀察者接收時,要在觀察者中調用的選擇器(方法)。這個方法需要一個 NSNotification 類型的單一參數。
  • name:要觀察的通知名稱。
  • object:可以選擇指定廣播通知的來源(指定發送通知的對象)。如果這個參數為 nil,無論哪個對象廣播該通知,觀察者都將收到指定名稱的通知。如果這個參數被設置,那么只有由給定對象廣播的指定名稱的通知將被觀察者接收。

在章節 1.29 中,我們學習了如何發布通知。現在讓我們試著觀察一下我們在那里學到的發布通知的方法:

#import "ViewController.h"

@interface ViewController ()

@end

@implementation ViewController

/* 通知的名稱 */
const NSString *ResultOfAppendingTwoStringsNotification = @"ResultOfAppendingTwoStringsNotification";

/* 通知中發送的字典的 keys 值 */
const NSString *ResultOfAppendingTwoStringsFirstStringInfoKey = @"firstString";
const NSString *ResultOfAppendingTwoStringsSecondStringInfoKey = @"secondString";
const NSString *ResultOfAppendingTwoStringsResultStringInfoKey = @"resultString";

/* 廣播通知 */
- (void)broadcastNotification {
    
    NSString *firstName = @"Anthony";
    NSString *lastName = @"Robbins";
    NSString *fullName = [firstName stringByAppendingString:lastName];
    
    NSArray *objects = [[NSArray alloc] initWithObjects:firstName, lastName, fullName, nil];
    NSArray *keys = [[NSArray alloc] initWithObjects:ResultOfAppendingTwoStringsFirstStringInfoKey,
                                                     ResultOfAppendingTwoStringsSecondStringInfoKey,
                                                     ResultOfAppendingTwoStringsResultStringInfoKey,
                                                     nil];
    
    NSDictionary *userInfo = [[NSDictionary alloc] initWithObjects:objects forKeys:keys];
    
    NSNotification *notificationObject = [NSNotification notificationWithName:(NSString *)ResultOfAppendingTwoStringsNotification
                                                                       object:self
                                                                     userInfo:userInfo];
    
    [[NSNotificationCenter defaultCenter] postNotification:notificationObject];
}

/* 觀察者接收到通知時,執行的方法 */
- (void)appendingIsFinished:(NSNotification *)paramNotification {
    NSLog(@"Notification is received.");
    NSLog(@"Notification Object = %@", [paramNotification object]);
    NSLog(@"Notification User-Info Dict = %@", [paramNotification userInfo]);
}

- (void)viewDidLoad {
    [super viewDidLoad];
    
    /* 監聽通知 */
    [[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(appendingIsFinished:)
                                                 name:(NSString *)ResultOfAppendingTwoStringsNotification
                                               object:self];
    /* 廣播通知 */
    [self broadcastNotification];
}

- (void)dealloc {
    /* 我們不再監聽任何通知了 */
    [[NSNotificationCenter defaultCenter] removeObserver:self];
}

@end

當你運行該應用程序時,你會看到類似于下面的東西打印到控制臺窗口:

Notification is received.
Notification Object = <ViewController: 0x14dd08570>
Notification User-Info Dict = {
    firstString = Anthony;
    resultString = AnthonyRobbins;
    secondString = Robbins;
}

正如你所看到的,我們正在使用通知中心的 removeObserver: 方法來移除我們的(作為所有通知的觀察者的)對象。當然也有其他方法將你的對象從觀察者鏈中移除。你可以像我們在這里所做的那樣冷處理,也就是說,將你的對象完全從觀察任何通知中移除,或者你可以在你的應用程序的生命周期中隨時將你的對象從觀察特定通知中移除。如果你想指定你要移除的(你的對象觀察的)通知,只需調用通知中心的removeObserver:name:object: 方法,并指定你要退訂的通知的名稱,以及(可選)發送通知的對象。

在iOS 9中取消注冊 NSNotificationCenter 觀察者對象

提醒大家注意蘋果公司在 iOS 9 和 OS X 10.11 的 Foundation Release Notes 中偷偷加入的內容。當 NSNotificationCenter 的觀察者被刪除時,它不再需要取消注冊。如果你需要支持 iOS 8 或使用基于 block 的觀察者時,有一些注意事項。我在寫上周關于detecting low power mode 的文章時忘記了這一點,所以在這里,為了喚起我的記憶,我把細節和一個額外的提示放在這里。

iOS 9 中 NSNotificationCenter 的變化

在 iOS 和 OS X 中,為 NSNotificationCenter 通知注冊一個觀察者是一項常見的任務。下面是典型的代碼示例,你可以在視圖控制器的 viewDidLoad 方法中使用,當用戶改變偏好的字體大小時接收通知:

NSNotificationCenter.defaultCenter().addObserver(self,
  selector:#selector(didChangePreferredContentSize(_:)),
  name: UIContentSizeCategoryDidChangeNotification, 
  object: nil)

使用 iOS 8 或更早的版本,你需要在刪除觀察者對象之前取消對該通知的注冊。如果你忘記了,當通知中心向一個不再存在的對象發送下一條通知時,你會有崩潰的風險。

使用 iOS 9 或更高版本的 Foundation 框架發布說明包含一些好消息:

In OS X 10.11 and iOS 9.0 NSNotificationCenter and NSDistributedNotificationCenter will no longer send notifications to registered observers that may be deallocated.

通知中心現在保持對觀察者歸零的弱引用(zeroing reference)。

If the observer is able to be stored as a zeroing-weak reference the underlying storage will store the observer as a zeroing weak reference, alternatively if the object cannot be stored weakly (i.e. it has a custom retain/release mechanism that would prevent the runtime from being able to store the object weakly) it will store the object as a non-weak zeroing reference.

如果觀察者能夠被存儲為歸零的弱引用,那么底層存儲將把觀察者存儲為歸零的弱引用。反之,如果對象不能被弱存儲(即它有一個自定義的保留/釋放機制,這將阻止運行時能夠弱存儲對象),它將把對象存儲為非歸零的弱引用。

所以下次通知中心想向觀察者發送通知時,它可以檢測到它不再存在,并為我們刪除觀察者。

這意味著觀察者不需要在其 deallocation 方法中取消注冊。下一個發送到該觀察者的通知將檢測到歸零的引用,并自動取消觀察者的注冊。

請注意,如果你使用的是基于 block 的觀察者,這并不適用。

通過 [NSNotificationCenter addObserverForName:object:queue:usingBlock] 方法創建的基于 block 的觀察者在不再使用時仍然需要被取消注冊,因為系統仍然持有這些觀察者的強引用。

另外,如果你喜歡,或需要兼容 iOS 8 或更低的版本,你仍然可以(像原來那樣)刪除觀察者。

仍然支持提前移除觀察者(無論是弱引用還是歸零引用)。

要明確的是,如果你需要兼容 iOS 8 或更低的版本,不要忘記在deinit方法中刪除觀察者。

deinit {
  NSNotificationCenter.defaultCenter().removeObserver(self, 
    name: UIContentSizeCategoryDidChangeNotification, 
    object: nil)
}

調試信息

NSNotificationCenterNSDistributedNotificationCenter 現在將在調試器打印時提供一個調試描述,該描述將列出所有注冊的觀察者,包括已被清零的引用,以幫助調試通知注冊的情況。

(lldb) p NSNotificationCenter.defaultCenter().debugDescription
(String) $R10 = "<NSNotificationCenter:0x134e0cb10>\nName, Object, Observer,  Options
UIAccessibilityForceTouchSensitivityChangedNotification, 0x19b0bbb60, 0x134d5d2e0, 1400
UIAccessibilityForceTouchSensitivityChangedNotification, 0x19b0bbb60, 0x134d605f0, 1400
...
UIContentSizeCategoryDidChangeNotification, 0x19b0bbb60, 0x134e5c2a0, 1400

你可能會發現你的應用程序所觀察到的通知數量多得令人吃驚,你很可能需要增加輸出字符串的最大長度,以便在LLDB控制臺中看到它們。

(lldb) set set target.max-string-summary-length 50000

參考

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

推薦閱讀更多精彩內容