Notification Center是在Cocoa/Cocoa Touch Framework中,物件之間可以不必需要知道彼此的存在,也可以互相傳遞訊息,交換資料/狀態的機制。
我們可以把Notification Center想像是一種廣播系統。
當一個物件A的狀態發生改變,而有多個物件需要知道這個物件發生改變的狀況下,
物件A不必直接對這些物件發出呼叫,而是告訴一個廣播中心說:
「我的狀態改變了」,至於其他需要聽取狀態的物件呢,
也只要對這個廣播中心訂閱(subscribe)指定的通知,
所以當物件A發出通知的時候,這個廣播中心就會通知有訂閱通知的其他物件。
這個廣播中心就是Notification Center。
我們經常使用Notification Center處理來自作業系統的事件。
假如我們現在寫了一個日記軟體,這個軟體裡頭已經有很多view,
每個view裡頭都有一篇日記,每篇日記上都有該篇日記的撰寫日期與時間。
我們通常會使用NSDateFormatter,使用系統偏好中的語系(Locale)設定,
將日期轉成符合語系設定的字串顯示,那麼,當用戶調整了系統偏好設定,
像是把中文改成英文,那麼我們原本用中文顯示的日期,
也應該馬上變成英文顯示-我們該怎麼做呢?
我們最常使用的通知中心是NSNotificationCenter這個class,我們也通常使用這個class的singleton物件default center(也就是說,其實Notification Center有好幾個,
不過我們最常用的還是這個)。當系統語系改變的時候,Notification Center就會
發出叫做NSCurrentLocaleDidChangeNotification的這項通知。
所以,我們所有要顯示日期的畫面物件,都應該要訂閱這個通知,
在收到通知的時候,就要重新產生日期字串。
接送與發送Notification
接收Notification
一個通知分成幾個部分
1. object:發送者,是誰送出了這個通知
2. name:這個通知叫什麼名字
3. user info:這個通知還帶了那些額外資訊 所以當我們想要監聽某個通知的時候,
便是指定要收聽由誰所出、那個名字的通知,並且指定負責處 理通知的selector,
以前面處理locale改變的例子來看,我們就會寫出這樣的code
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view from its nib.
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(localDidChange:) name:NSCurrentLocaleDidChangeNotification object:nil];
}
- (void)localeDidChange:(NSNotification *)notification {
//處理locale改變的狀況
}
- (void)dealloc {
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
意思就是,我們要指定name為NSCurrentLocaleDidChangeNotification的通知,
交由localeDidChange:處理。在這邊object設定為nil,代表不管是那個物件送出的,
只要符合NSCurrentLocaleDidChangeNotification的通知,我們統統都要處理。
每個通知當中,還有可能會有額外的資訊,會夾帶在NSNotification物件的userInfo
屬性中,userInfo是一個NSDictionary。
像是我們如果讓某個text field變成了first responder,那麼,在iOS上,就會出現螢
幕鍵盤,而當螢幕鍵盤出現的時候,我們往往要調整畫面的layout,而螢幕鍵盤
在不同輸入法的大小不一樣,像是英文鍵盤比較起來,中文輸入鍵盤上面往往會有
一塊組字區,而造成中文鑑盤比英文鑑盤大。在螢幕鍵盤升起來的時候,我們會收到
UIKeyboardWillShowNotification這項通知,而這項通知就會用userInfo告訴我們
鍵盤尺寸與位置。
如果我們在-addObserver:selector:name:object:裡頭,把name指定會nil,
就代表我們想要訂閱所有的通知,通常不太會有這種情境,不過有時候你想要知道
系統內部發生了什麼事情,可以用這種方式試試看。
當我們不需要繼續訂閱某項通知的時候,記得對Notification Center呼叫
-removeObserver:,以上面的程式為例,我們在addObserver的時候傳入了self,
在removeObserver的時候,就要傳入self。我們通常在dealloc的時候停止訂閱。
在iOS4與Mac OS X10.6之後,我們可以使用
-addObserverForName:object:queue:usingBlock:這組使用block語法的
API訂閱通知,由於傳入block,所以我們就不必另外準備一個selector,可以將
處理notification的程式與add observer的這段呼叫寫在一起。而remove observer
的寫法也會不太一樣:-addObserverForName:object:queue:usingBlock:會回
傳一個observer物件,我們想要停止訂閱的時候是對-removeObserver:傳入
之前拿到的observer物件,範例如下。
Add observer的時候:
NSNotificationCenter *observer = [[NSNotificationCenter defaultCenter] addObserverForName:NSCurrentLocaleDidChangeNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification * _Nonnull note) {
//處理locale改變的狀況。
}];
[[NSNotificationCenter defaultCenter] removeObserver:observer];
發送Notification
至於要發送notification,則是在建立了notification物件之後,對NSNotificationCenter
呼叫-postNotification:即可。
這三組method都可以用來發送notification。
- (void)postNotification:(NSNotification *)notification; - (void)postNotificationName:(NSString *)aName object:(nullable id)anObject; - (void)postNotificationName:(NSString *)aName object:(nullable id)anObject userInfo:(nullable NSDictionary *)aUserInfo
Notification與Threading
當我們訂閱某個notification之後,我們並不能夠保證負責處理notification的selector
或block會在那個thread執行:這個notification是在那條thread送出的,
負責接受的selector或是block,就會在那條thread執行。
在慣例上,絕大多數的notification都會在main thread送出,之所以說「絕大多數」
,就是因為有例外,像是在iOS上,如果我們接上耳機、拔除耳機,或是將音樂透過
AirPlay送到Apple TV的時候,系統會透過AVAudioSessionRouteChangeNotification
告訴我們音訊輸出設備改變了,這個通知就會發生在背景,而不是main thread。
不過當我們在撰寫自已的程式,要發送notification的時候,為了考慮其他開發者會
預期在main thread收到notification,所以我們也就在main thread發送notification,
像是透過GCD,把postNotification的呼叫送到dispatth_get_main_queue()上。
Notification Queue
有的時候,我們的程式可能會在很短的時間送出大量的notification,而造成資源
的浪費或效能問題。
以KKBOX來說,我們在歌單中的歌曲物件發生改動的時候,會透過notification
更新UI:一首歌曲可能會出現在多個歌單中,而我們可能用了很多不同的UI物件
來呈現不同張歌單,因此,像一首歌曲的播放次數改變,如這首歌被多播了一次,
歌曲物件就透過notification center,告訴每個跟歌單UI相關的物件重新讀取歌單資料。
照理說,只要有一首歌曲改變,就該發出這種通知,但假如我們現在做的事情是
歌單同步-把另一台裝置上的歌單資料,同步到我們這台裝置上,那麼改動的就
不只是一首歌曲,而是一大批歌曲,如果有十首歌,就送出了十次通知;但是,
其實UI只需要改動一次就好了,沒有重複更新十次UI的必要。
這時候我們就該用NSNotificationQueue。我們可以把NSNotificationQueue想成
Notification的發送端與notification_center之間的一個buffer,這個buffer可以
讓我們暫緩送出notification,而在一段緩衝期之內,決定我們是否要合併通知。
以前面的例子來看,我們就可以先把原本預計的十次通知先放進NSNotificationQueue當中,然後讓NSNotificationQueue幫我們把十次通知合併成只有一次通知。
我們要先建立一個NSNotificationQueue物件:
notificationQueue = [[NSNotificationQueue alloc] initWithNotificationCenter:[NSNotificationCenter defaultCenter]];
再來我們發送通知的程式原本像這樣:
NSNotification *n = [NSNotification notificationWithName:@"KKsongInfoDidChangeNotification" object:self]; [[NSNotificationCenter defaultCenter] postNotification:n];
改寫成這樣:
NSNotification *n = [NSNotification notificationWithName:@"KKSongInfoDidChangeNotification" object:self]; [notificationQueue enqueueNotification:n postingStyle:NSPostASAP coalesceMask:NSNotificationCoalescingOnName | NSNotificationCoalescingOnSender forModes:nil];
我們在這邊傳入了NSNotificationCoalescingOnName與NSNotificationCoalescingOnSender,代表的就是請notification queue合併
名稱相同,發送者也相同的通知。
Mac上其他的Notification Center
在iOS上面我們通常會用到NSNotificationCenter,特別是NSNotificationCenter的
defaultCenter:不過在Mac OSX上,我們還有其他的notification center可以使用。
NSDistributedNotificationCenter
果在iOS上的限制較為格,一直以來都想辦法禁止跨App之間的通訊
(IPC, Inter-Process Communication)。不過自從Mac OS X出現以來,Cocoa Framework就有Distributed Objects這套IPC機制,讓不同App之間可以傳遞Objective-C物件,後來更推出來XPC,可以在不同App之間傳遞block。
NSDistributedNotificationCenter就是在Distributed Objects技術上建立的notification center,也就是,如果你對NSDistributedNotificationCenter發送了通知,便可以讓其他的App收到來自你目前所在App送出的通知。
NSWorkSpace的Notification Center
NSWorkSpace這個物件在Mac上代表的是Mac的桌上環境,如果你想要要求Mac OS X開啟另外一個App,處理某個檔案或URL(在iOS上我們會要求UIApplication來openURL,
但是在Mac上則是交由NSWorkSpace處理),或是取得某個檔案在Finder裡頭的代表圖示...等,就會用到NSWorkSpace。
跟NSWorkSpace相關的通知,像是某個App是否被成功開啟,你的Mac電腦是否離開了休...等等,都不會透過NSNotificationCenter的defaultCenter,而是要透過[[NSWorkSpace sharedWorkspace] notificationCenter]這邊的notification center,我們要選擇正確的notification center做add observer,才能正確收到通知。
CFNotificationCenter
CFNotificationCenter是NSNotificationCenter在Core Foundation中的C實作,一般來說,如果我們有比較高階的API可以使用的話,我們會盡量避免使用比較低階的API,所以,只要有NSNotificationCenter可以使用的場合,我們應該不會用到CFNotificationCenter。
比較有可能用到CFNotificationCenter的場合,大概是iOS8之後,Hosting App與Extension之間的溝通。iOS8之後推出了Extension,可以允許開發者撰寫Today Widget、Share Widget以及模擬鍵盤等功能,Apple Watch的Watch App也屬於Extension;每個Extension都是額外可以讓作業系統載入的Bundle,Extension與我們原本的Ap p(便是Hosting App)之間,可以用Shared Data共用資料,但是當App發生改變要通知Extension,
卻沒有比較直接的辦法。在蘋果有新的API之前,我們就會倚賴透過CFNotificationCenterGetDarwinNotifyCenter()取得的darwin notification center發送通知。
而即使我們可以發送通知,CFNotificationCenter用起來也不是很方便,主要原因是CFNotificationCenter不像NSNotificationCenter,在傳遞通知的時候可以夾帶user info。再來,就是像前面說的CFNotificationCenter是C API,而我們會盡量希望使用比較高階的API。


