讀古今文學網 > iOS編程基礎:Swift、Xcode和Cocoa入門指南 > 11.3 通知 >

11.3 通知

Cocoa為應用提供了一個單例的NSNotificationCenter,它的非正式名稱叫作通知中心。該實例可以通過調用NSNotificationCenter.defaultCenter()來獲取,它是消息發送(叫作通知)機制的基礎。一個通知就是一個NSNotification實例。其想法是任何對象都可以註冊到通知中心以接收某些通知。另一個對象可以向通知中心發送通知對像(叫作發佈通知)。接下來,通知中心就會向所有註冊以準備接收通知的對象發送該通知。

人們經常將通知機制稱為分發或廣播機制,這樣描述的理由也很充分。憑借該機制,對象可以發送消息而不必瞭解或關心什麼對像、多少對像會接收到該通知。這樣做簡化了應用的架構,使得系統不必再將實例連接起來才能實現彼此間的消息傳遞(這有時是很難做到的,第13章將會介紹)。當對像從概念上做到了彼此間的「隔離」,通知就是一個相當輕量級的方式,可以讓一個對像向另一個對像發送消息。

一個NSNotification對像包含了3部分信息,這些信息可以通過其實例方法獲取到:

name

一個NSString,表示通知的含義。

object

與通知關聯的一個實例;一般來說是發送通知的實例。

userInfo

並非每個通知都有userInfo;它是個NSDictionary,可以包含與通知相關的一些附加信息,該NSDictionary到底包含什麼信息,信息位於哪些鍵中取決於具體的通知;你需要查詢文檔才能獲悉。比如,文檔表明UIApplication的UIApplicationDidChange-StatusBarOrientationNotification包含了一個userInfo字典,字典有一個UIAppli-cationStatusBarOrientationUserInfoKey鍵,其值是狀態欄之前的方向。在自己發佈通知時,你可以將任何感興趣的信息放到userInfo中供通知接收者獲取。

Cocoa本身會通過通知中心來發送通知,你的代碼可以註冊到通知中心來接收通知。對於提供了通知的類的文檔,你會看到有一個單獨的Notifications部分來介紹它們。

11.3.1 接收通知

要想註冊以接收通知,你需要向通知中心發送如下兩條消息之一,一個是addObserver:selector:name:object:,其參數如下所示。

observer:

通知發向的實例。它通常是self;一般不會出現一個實例將另一個不同的實例註冊為通知接收者的情況。

selector:

當通知出現時,發送給觀察者實例的消息。指定的方法應該不返回結果(Void)並且帶有一個參數,參數是NSNotification實例(因此,參數的類型應該是NSNotification或AnyObject)。在Swift中,可以通過將方法名作為字符串來指定選擇器。

不要將方法的字符串名搞錯了,也不要忘記實現方法!如果通知中心通過調用作為選擇器的方法來發送通知,並且該方法不存在,那麼應用就會崩潰。參見附錄A瞭解關於如何將方法轉換為字符串名的規則。

只有當選擇器所命名的方法公開給了Objective-C時才能調用它。如果通知中心通過調用作為選擇器的方法來發送通知,但Objective-C不知道這個方法,那麼應用就會崩潰。如果類是NSObject的子類,或方法標記為@objc(或dynamic),那麼Objective-C才能知道這個方法。

name:

你想要接收的通知的字符串名。如果該參數為nil,那麼你就會接收到與object:參數中所指定的對象相關的所有通知。內建的Cocoa通知名通常是個常量。這是很有用的,因為如果搞亂了常量名,編譯器就會報錯,如果直接以字符串字面值的形式輸入通知名,但卻輸錯了,那麼編譯器就不會報錯,但卻無法收到任何通知了(因為沒有與你輸入的名字所對應的通知),這種錯誤是很難追蹤的。

object:

你所感興趣的通知對象,通常是發佈通知的那個對象。如果為nil,那麼你就會接收到name:參數中所指定名字的所有通知(如果name:與object:參數都為nil,那麼你就會接收到所有通知)。

比如,在我的一個應用中,我希望在設備的音樂播放器開始播放下一首歌曲時界面能夠變化。設備內建的音樂播放器API屬於MPMusicPlayerController類;該類提供了一個通知,告訴我內建的音樂播放器何時改變了正在播放的歌曲,該通知的說明位於MPMusicPlayerController類文檔中的Notifications下,名字是MPMusicPlayerController-NowPlayingItemDidChangeNotification。

查看文檔會發現,只有先調用MPMusicPlayerController的beginGeneratingPlaybackNotifications實例方法後才會發送該通知。這種架構很常見;對於某些通知來說,只有開啟後Cocoa才會發送,從而提升了效率。這樣,我首先需要獲取到MPMusicPlayerController的一個實例,然後調用該方法:


let mp = MPMusicPlayerController.systemMusicPlayer
mp.beginGeneratingPlaybackNotifications
  

現在,註冊自身以接收所需的播放通知:


NSNotificationCenter.defaultCenter.addObserver(self,
    selector: \"nowPlayingItemChanged:\",
    name: MPMusicPlayerControllerNowPlayingItemDidChangeNotification,
    object: nil)
  

這樣,當發佈MPMusicPlayerControllerNowPlayingItemDidChangeNotification通知後,nowPlayingItemChanged:方法就會被調用:


func nowPlayingItemChanged (n:NSNotification) {
    self.updateNowPlayingItem
    // ... and so on ...
}
  

要想讓addObserver:selector:name:object:能夠正常運作,你需要獲取到正確的選擇器,並確保實現了相應的方法。大量使用addObserver:selector:name:object:意味著代碼中會充斥著大量僅供通知中心調用的方法。並沒有關於這些方法的說明(你需要添加一些註釋來提醒自己),同時這些方法又獨立於註冊調用,所有這一切使代碼變得非常混亂。

可以通過另一種通知註冊方式來解決這個問題,即調用addObserverForName:object:queue:usingBlock:。它會返回一個值,這個值的目的將會在後面介紹。queue:通常為nil;非nil的queue:用於後台線程。name:與object:參數就像是addObserver:selector:name:object:中相應的參數一樣。相對於使用觀察者與選擇器,你需要提供一個Swift函數,它包含了通知到達時所要執行的實際代碼。該函數接收一個參數,即NSNotification本身。如果使用了匿名函數,那麼對於所註冊的通知的響應就會成為註冊的一部分:


let ob = NSNotificationCenter.defaultCenter
    .addObserverForName(
        MPMusicPlayerControllerNowPlayingItemDidChangeNotification,
        object: nil, queue: nil) {
            _ in
            self.updateNowPlayingItem
            // ... and so on ...
}   

使用addObserverForName:...會導致一些額外的內存管理問題,第12章將會對此進行介紹。

11.3.2 取消註冊

對於註冊為通知接收者的每個對象,你可以在其銷毀之前取消註冊。如果沒有取消註冊,對像又不存在了,同時該對像註冊以接收的消息又發送出來了,那麼通知中心就會嘗試向該對像發送恰當的消息,現在就接收不到了。這樣,最好的結果就是應用崩潰,最糟糕的結果就是出現了混亂。

要想取消註冊通知接收者對象,請向通知中心發送removeObserver:消息(此外,還可以使用removeObserver:name:object:讓對像取消註冊特定的通知集)。作為observer:參數所傳遞的對象就是不再接收通知的那個對象。這個對象是什麼取決於你一開始是如何註冊的:

調用了addObserver:...

一開始就提供了觀察者;它就是現在要取消註冊的那個觀察者。

調用了addObserverForName:...

對addObserverForName:...的調用會返回一個類型為NSObjectProtocol的觀察者標記對像(無須關心其真實的類型與特性);它就是現在要取消註冊的那個觀察者。

棘手之處在於要找到恰當的時機來取消註冊。靠譜的解決方案是註冊實例的deinit方法,它是實例銷毀前所接收到的最後一個生命週期事件。

如果調用了同一個類的addObserverForName:...多次,那就會從通知中心接收到多個觀察者標記,你需要將其保存起來以便後續可以取消他們的註冊。如果想一次取消註冊全部對象,一種方案就是使用類型為可變集合的實例屬性。我喜歡使用Set屬性:


var observers = Set<NSObject>
  

每次使用addObserverForName...註冊通知時,我都會捕獲到結果並將其添加到集合中:


let ob = NSNotificationCenter.defaultCenter.addObserverForName(/*...*/)
self.observers.insert(ob as! NSObject)
  

在取消註冊時,我會枚舉集合併將其清空:


for ob in self.observers {
    NSNotificationCenter.defaultCenter.removeObserver(ob)
}
self.observers.removeAll   

NSNotificationCenter是無法內省的:你不能通過NSNotificationCenter獲取到註冊為通知接收者的對象。這是Cocoa功能的一個欠缺,如果犯了諸如過早取消某個觀察者的註冊這類錯誤,那麼Bug是很難追蹤的(與往常一樣,這也來自於我痛苦的經歷)。

11.3.3 發佈通知

雖然很多時候都是從Cocoa接收通知的,不過也可以利用通知機制實現自定義對像間的通信。這麼做的一個原因在於兩個對像從概念上會彼此獨立。不過,值得注意的是不要過多地使用通知,也不要將其作為對像間通信的鏈路;但在某些情況下它們還是非常適合的(第13章將會介紹)。

要想按照這種方式使用通知,對像會在通信鏈路中扮演兩個角色。一個或多個對象會註冊以接收通知,如前所述,這是通過名字、對像或二者的結合體來標識的。另一個對像會發佈通知,標識方式也是一樣的。接下來,通知中心會將消息從發送者傳遞給註冊的接收者。

要想發佈通知,請向通知中心發送消息postNotificationName:object:userInfo:。

比如,我曾開發過一款簡單的紙牌遊戲。遊戲需要知道用戶什麼時候輕拍了紙牌,不過紙牌卻對遊戲一無所知;當用戶輕拍紙牌時,它只是通過發佈通知發出聲音而已:


NSNotificationCenter.defaultCenter.postNotificationName(
    \"cardTapped\", object: self)
  

遊戲對像註冊過了\"cardTapped\"通知,因此它會知道這一點並接收到通知的object;現在,它知道用戶輕拍了哪個紙牌,並且可以正確地進行處理。

11.3.4  NSTimer

嚴格來說,定時器(NSTimer)並非通知;但其行為非常類似於通知。它是一個對象,在某段時間間隔後會發出一個信號。這個信號就是發給一個實例的消息。這樣,當某段時間過後,你就可以收到通知了。時間並不是非常精確的,但用起來沒什麼問題。

定時器管理並不難,但有點與眾不同。定時器會不斷檢查時鐘,我們稱這種行為為調度。定時器可能會被觸發一次,也可能是個重複定時器。要想銷毀定時器,首先要將其置為無效狀態。設定為只觸發一次的定時器會在觸發後自動變為無效狀態;重複定時器會不斷重複執行,直到你通過向其發送invalidate消息將其置為無效狀態。你不應該再使用處於無效狀態的定時器;也不能將其復活或使用它做別的事情,你也不應該向其發送任何消息。

創建定時器的直接方式是使用NSTimer類的scheduledTimerWithTimeInterval:target:selector:userInfo:repeats:方法。這會創建定時器並對其進行調度,這樣定時器就會自動開始檢查時鐘了。目標與選擇符決定了當定時器觸發時可以向什麼對像發送什麼消息;處理的方法應該接收一個參數,該參數是指向定時器的引用。userInfo就像是通知的userInfo一樣。

對於Timer的target:與關於NSNotifications的selector:也要小心。當定時器觸發時,目標一定要存在,它要有一個與動作選擇器相對應的方法,Objective-C必須要能調用該方法。否則就會出問題。

NSTimer有個tolerance屬性,它是個時間間隔,表示定時器可以在指定的觸發時間與這個時間加上tolerance之間的某一時刻觸發。文檔表明可以通過為其提供一個至少為timeInterval 10%的值來改進設備電池壽命與應用響應性。

比如,我開發過一個應用,它是個遊戲並且帶有分數;如果用戶在10秒內沒有移動,那麼我就要減分來懲罰用戶。這樣,每次用戶移動時,我都會創建一個重複定時器,其時間間隔是10秒(我還會將任何現有的定時器都置為無效);在定時器調用的方法中,我會減分。

定時器存在一些內存管理問題,第12章將會介紹,此外定時器還有基於塊的替代方案。