1.29 通過 NSNotificationCenter 發(fā)送通知
問題
你想在你的應(yīng)用程序中廣播一個(gè)事件,并允許任何愿意收聽的對(duì)象(取決于廣播的通知)采取行動(dòng)。
解決方案
使用默認(rèn)的通知中心 NSNotificationCenter
的 postNotificationName: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
類型的通知,需要使用 NSNotification
的 notificationWithName: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)試信息
NSNotificationCenter
和 NSDistributedNotificationCenter
現(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