1.29 通過 NSNotificationCenter 發送通知
問題
你想在你的應用程序中廣播一個事件,并允許任何愿意收聽的對象(取決于廣播的通知)采取行動。
解決方案
使用默認的通知中心 NSNotificationCenter
的 postNotificationName:object:userInfo:
方法來發布一個通知,該通知攜帶一個對象(通常是發布通知的對象)和一個用戶信息字典,該字典可以攜帶關于通知和/或發布通知的對象的額外信息。
討論
通知中心是通知對象的調度中心。例如,當用戶在你的應用程序內的任何地方彈出鍵盤時,iOS 會向你的應用程序發送一個通知。你的應用程序中任何愿意收聽此通知的對象都可以將自己添加到默認的通知中心,作為該特定通知的觀察者。一旦你的對象的生命周期結束,它必須從通知中心的調度表中刪除自己。因此,通知是一條通過通知中心廣播給觀察者的消息。
通知中心是 NSNotificationCenter
類型的一個實例。我們使用 NSNotificationCenter
的類方法 defaultCenter
來獲取系統默認的通知中心對象。
通知是 NSNotification
類型的對象。一個通知對象有一個名字(指定為 NSString
類型),并且可以攜帶兩個關鍵信息:
備注
你可以指定你的通知的名稱。你不需要為此使用 API。只要確保你的通知名稱是唯一的,不會與系統通知發生沖突。
-
Sender Object
這是發布通知的對象的實例。觀察者可以使用
NSNotification
類的對象實例方法訪問這個對象。 -
User-Info Dictionary
這是一個(發送者對象可以創建并與通知對象一起發送的)可選的字典。這個字典通常包含關于通知的更多信息。例如,當你的應用程序內的任何組件的鍵盤即將在 iOS 中得到顯示時,iOS 會將
UIKeyboardWillShowNotification
通知發送到默認的通知中心。這個通知的用戶信息字典中包含了一些值,例如動畫前后的鍵盤矩形以及鍵盤的動畫持續時間。利用這些數據,觀察者可以做出決定,例如,一旦鍵盤顯示在屏幕上,該如何處理可能會被阻擋的 UI 組件。
警告
通知是實現代碼解耦的一個好方法。我的意思是,使用通知,你可以擺脫完成處理程序(completion handlers)和委托(delegation)。然而,關于通知有一個潛在的注意事項:它們不會被立即交付。它們是由通知中心派發的,而 NSNotificationCenter 的默認實現對應用程序的程序員是隱藏的。傳遞有時可能會延遲幾毫秒,或者在極端情況下(我從未遇到過),延遲幾秒鐘。因此,應該由你來決定在哪里使用通知,在哪里不使用通知。
為了構造一個 NSNotification
類型的通知,需要使用 NSNotification
的 notificationWithName: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)
}
調試信息
NSNotificationCenter
和 NSDistributedNotificationCenter
現在將在調試器打印時提供一個調試描述,該描述將列出所有注冊的觀察者,包括已被清零的引用,以幫助調試通知注冊的情況。
(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