NSNotificationCenter 通知中心

1.29 通過 NSNotificationCenter 發(fā)送通知

問題

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

解決方案

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

討論

通知中心是通知對(duì)象的調(diào)度中心。例如,當(dāng)用戶在你的應(yīng)用程序內(nèi)的任何地方彈出鍵盤時(shí),iOS 會(huì)向你的應(yīng)用程序發(fā)送一個(gè)通知。你的應(yīng)用程序中任何愿意收聽此通知的對(duì)象都可以將自己添加到默認(rèn)的通知中心,作為該特定通知的觀察者。一旦你的對(duì)象的生命周期結(jié)束,它必須從通知中心的調(diào)度表中刪除自己。因此,通知是一條通過通知中心廣播給觀察者的消息。

通知中心是 NSNotificationCenter 類型的一個(gè)實(shí)例。我們使用 NSNotificationCenter 的類方法 defaultCenter 來獲取系統(tǒng)默認(rèn)的通知中心對(duì)象。

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

備注

你可以指定你的通知的名稱。你不需要為此使用 API。只要確保你的通知名稱是唯一的,不會(huì)與系統(tǒng)通知發(fā)生沖突。

  • Sender Object

    這是發(fā)布通知的對(duì)象的實(shí)例。觀察者可以使用 NSNotification 類的對(duì)象實(shí)例方法訪問這個(gè)對(duì)象。

  • User-Info Dictionary

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

警告

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

為了構(gòu)造一個(gè) NSNotification 類型的通知,需要使用 NSNotificationnotificationWithName:object:userInfo: 類方法,我們很快會(huì)看到了。

備注

最好使用 Notification 這個(gè)單詞來作為你的通知名稱的后綴。例如,你當(dāng)然可以給你的通知起一個(gè)類似 ResultOfAppendingTwoStrings 的名字。不過最好是起一個(gè)像 ResultOfAppendingTwoStringsNotification 這樣的名字,因?yàn)樗宄乇砻髁诉@個(gè)名字的歸屬。

讓我們來看一個(gè)例子。我們將簡單地取一個(gè)名字和一個(gè)姓氏,將它們拼接來創(chuàng)建一個(gè)字符串(名字+姓氏),然后使用默認(rèn)的通知中心廣播這個(gè)結(jié)果。我們將在用戶啟動(dòng)我們的應(yīng)用時(shí),在我們的應(yīng)用委托的實(shí)現(xiàn)中完成這一工作:

#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;
}

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

#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 監(jiān)聽來自 NSNotificationCenter 的通知

問題

你想使用 NSNotificationCenter 監(jiān)聽各種系統(tǒng)廣播通知和自定義廣播通知。

解決方案

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

討論

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

下面是 addObserver:selector:name:object: 實(shí)例方法接受的每個(gè)參數(shù)的簡要描述:

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

在章節(jié) 1.29 中,我們學(xué)習(xí)了如何發(fā)布通知。現(xiàn)在讓我們試著觀察一下我們在那里學(xué)到的發(fā)布通知的方法:

#import "ViewController.h"

@interface ViewController ()

@end

@implementation ViewController

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

/* 通知中發(fā)送的字典的 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];
}

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

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

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

@end

當(dāng)你運(yùn)行該應(yīng)用程序時(shí),你會(huì)看到類似于下面的東西打印到控制臺(tái)窗口:

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

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

在iOS 9中取消注冊 NSNotificationCenter 觀察者對(duì)象

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

iOS 9 中 NSNotificationCenter 的變化

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

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

使用 iOS 8 或更早的版本,你需要在刪除觀察者對(duì)象之前取消對(duì)該通知的注冊。如果你忘記了,當(dāng)通知中心向一個(gè)不再存在的對(duì)象發(fā)送下一條通知時(shí),你會(huì)有崩潰的風(fēng)險(xiǎn)。

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

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

通知中心現(xiàn)在保持對(duì)觀察者歸零的弱引用(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.

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

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

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

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

通過 [NSNotificationCenter addObserverForName:object:queue:usingBlock] 方法創(chuàng)建的基于 block 的觀察者在不再使用時(shí)仍然需要被取消注冊,因?yàn)橄到y(tǒng)仍然持有這些觀察者的強(qiáng)引用。

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

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

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

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

調(diào)試信息

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

(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

你可能會(huì)發(fā)現(xiàn)你的應(yīng)用程序所觀察到的通知數(shù)量多得令人吃驚,你很可能需要增加輸出字符串的最大長度,以便在LLDB控制臺(tái)中看到它們。

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

參考

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

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