讀古今文學網 > iOS編程基礎:Swift、Xcode和Cocoa入門指南 > 11.8 鍵值觀測 >

11.8 鍵值觀測

鍵值觀測(KVO)是一種不使用NSNotificationCenter的通知機制。一個對象可以通過KVO直接註冊到第2個對象上,當第2個對象中的值發生變化時,第1個對象就會收到通知。此外,第2個對象(被觀察的對象)不必做任何額外的事情,它甚至都意識不到註冊已經發生了。當被觀測對像中的值發生了變化,註冊對像(觀察者)就會自動收到通知(也許更好的一個架構上的類比就是目標-動作機制;這是一種介於任意兩個對像之間的目標-動作機制)。

在使用KVO時,觀察者就是你自己的對象;當觀察者接收到被觀察者改變的通知時,你需要編寫代碼進行響應。不過,被觀察的對象(註冊到其上以監聽變化的對象)無需是你自己的對象;實際上,通常情況下它也不是你自己的對象。很多Cocoa對象的行為都是KVO形式的,你可以對其使用KVO。一般來說,KVO主要用於替代委託與通知。

KVO的使用可以劃分為如下3個階段:

註冊

要想監聽被觀察對像中某個值的變化,你需要註冊到被觀察對像上。這通常需要調用被觀察對象的addObserver:forKeyPath:options:context:方法(所有繼承自NSObject的對象都有這個方法,因為它是通過非正式協議NSKeyValueObserving注入NSObject中的,而NSKeyValueObserving則是NSObject與其他類上的一組類別)。

變化

變化發生在被觀察對像中的值上,而且方式比較特別,即必須以KVO兼容的形式。通常,這意味著要使用鍵值編碼兼容的訪問器來作出改變。通過鍵值編碼兼容的訪問器來設置屬性。

通知

當被觀察對像中的值發生變化時,觀察者會自動收到通知:其observeValueForKeyPath:ofObject:change:context:方法(我們已經針對這個目的實現了該方法)會在運行期得到調用。

如果不想再接收通知了,那就需要取消對被觀察對象的註冊,這是通過向其發送removeObserver:forKeyPath:(或removeObserver:forKeyPath:context:)來實現的。這是非常重要的,原因與取消對NSNotification的註冊相同:如果不取消註冊,那麼當通知發送給了已經不存在的觀察者時,應用就會崩潰。你需要顯式取消觀察者所註冊的每個鍵路徑;不能將nil作為第2個參數來表示「所有鍵路徑」。取消註冊的最後一個機會是觀察者的deinit;顯然,這要求觀察者擁有對被觀察對象的引用。

事情還沒有結束。在被觀察對像銷毀前,所有的觀察者都必須要顯式取消對其的註冊!如果對像銷毀了,但觀察者並沒有取消註冊,那麼應用就會崩潰,同時控制台會打印出一條消息:「An instance...was deallocated while key value observers were still registered with it.」

如下示例來自於我自己的代碼。AVPlayerViewController是個視圖控制器,其視圖用於顯示視頻內容。當該視圖首次出現時會閃一下,因為視圖是黑色的,到視頻內容出現前中間會有一點延時。解決辦法就是一開始讓視圖不可見,直到視頻內容出現後才讓其可見。這樣,我們希望當視頻內容出現後能夠收到通知。AVPlayerViewController有個readyForDisplay屬性,我們希望該屬性變為true時能夠收到通知。不過,AVPlayerViewController並沒有委託,也沒有提供通知。那麼,解決之道就是使用KVO:將自身註冊到AVPlayerViewController上,監聽其readyForDisplay屬性的變化。如下代碼展示了如何配置並呈現AVPlayerViewController的視圖:


func setUpChild {
    // ...
    let av = AVPlayerViewController
    av.player = player
    av.view.frame = CGRectMake(10,10,300,200)
    av.view.hidden = true // looks nicer if we don\'t show until ready
    av.addObserver(self,
        forKeyPath: \"readyForDisplay\", options: , context: nil) 1
    // ...
}
override func observeValueForKeyPath(keyPath: String?,
    ofObject object: AnyObject?, change: [String : AnyObject]?,
    context: UnsafeMutablePointer<>) { 2
        if keyPath == \"readyForDisplay\" {
            if let obj = object as? AVPlayerViewController {
                dispatch_async(dispatch_get_main_queue, {
                    self.finishConstructingInterface(obj)
                })
            }
        }
}
func finishConstructingInterface (vc:AVPlayerViewController) {
    if !vc.readyForDisplay {
        return
    }
    vc.removeObserver(self, forKeyPath:\"readyForDisplay\") 3
    vc.view.hidden = false
}
  

1AVPlayerViewController的視圖一開始是不可見的(hidden為true)。我們註冊並監聽其readyForDisplay屬性的變化。

2AVPlayerViewController的readyForDisplay屬性發生了變化,我們收到了通知,因為observeValueForKeyPath:...得到了調用。我們要確保這是個正確的通知;如果是,那麼就繼續完成界面的構建。注意到被觀察對像(AVPlayerViewController)會作為object參數傳遞進來;這不僅有助於識別通知,還可以讓我們與該對像通信。對於observeValueForKeyPath:...是在哪個線程上調用的是沒有任何保證的,因此在做任何會影響界面的事情前我們需要移到主線程外。

3最後檢查一次,確保readyForDisplay已經從false變為了true,取消註冊(我們只需要監聽其改變一次)並讓視圖可見(hidden為false)。

options:參數是個位掩碼(NSKeyValueObservingOptions)。該參數可以將改變的屬性的新值以change:字典的形式發給我們。這樣,我們就可以改寫代碼,將檢查readyForDisplay是否為true的代碼移動到observeValueForKeyPath....實現中。現在的註冊代碼如下所示:


av.addObserver(
    self, forKeyPath: \"readyForDisplay\", options: .New, context: nil)
  

下面是剩餘部分的代碼;如第5章所述,這是一系列guad語句:


override func observeValueForKeyPath(keyPath: String?,
    ofObject object: AnyObject?, change: [String : AnyObject]?,
    context: UnsafeMutablePointer<>) {
        guard keyPath == \"readyForDisplay\" else {return}
        guard let obj = object as? AVPlayerViewController else {return}
        guard let ok = change?[NSKeyValueChangeNewKey] as? Bool else {return}
        guard ok else {return}
        dispatch_async(dispatch_get_main_queue, {
            self.finishConstructingInterface(obj)
        })
}
func finishConstructingInterface (vc:AVPlayerViewController) {
    vc.removeObserver(self, forKeyPath:\"readyForDisplay\")
    vc.view.hidden = false
}
  

你可能想知道addObserver:...與observeValueForKeyPath:....中context:參數的含義。基本上,我不建議你使用這個參數,不過無論怎樣還是要介紹一下。context:參數表示傳遞給addObserver:...以及從observeValueForKeyPath:....獲取的「任何數據」。不過,你需要注意其值,因為其類型是UnsafeMutablePointer<Void>。這意味著即便運行時持有它,其內存也不是由運行時管理的;你需要通過持有它的一個持久化引用來管理其內存。通常的做法是使用全局變量(聲明在文件頂部的變量);為了防止任何地方都能訪問這個變量,你可以將其聲明為private的,如以下代碼所示:


private var con = \"ObserveValue\"
  

在調用addObserver:...時,你會將該變量的地址&con作為context:參數傳遞進去。當observeValueForKeyPath:...接收到通知時,你可以將context:參數作為標識符,將其與&con進行比較:


override func observeValueForKeyPath(keyPath: String?,
    ofObject object: AnyObject?, change: [String : AnyObject]?,
    context: UnsafeMutablePointer<Void>) {
        if context != &con {
        return // wrong notification
    }
    // ...
}
  

在上述代碼中,存儲在全局變量中的值是沒什麼意義的;我們只是將其地址作為標識符而已。如果想要使用存儲在全局變量中的值,請將UnsafeMutablePointer強制類型轉換為另一個UnsafeMutablePointer底層類型。接下來就可以將底層值作為UnsafeMutablePointer的memory屬性了。在該示例中,con是個String:


override func observeValueForKeyPath(keyPath: String?,
    ofObject object: AnyObject?, change: [String : AnyObject]?,
    context: UnsafeMutablePointer<Void>) {
        let c = UnsafeMutablePointer<String>(context)
        let s = c.memory // \"ObserveValue\"
        // ...
    }
  

鍵值觀測是個很複雜的機制;請查閱Apple的Key-Value Observing Guide瞭解詳細信息(比如,可以觀測可變的NSArray,不過其機制要比之前介紹的更加複雜)。KVO也有一些令人遺憾的缺點。首先,所有通知都是通過調用同一個方法出現的,而這個方法則會成為瓶頸,這非常遺憾。追蹤誰觀察了誰,確保觀察者與被觀察者都有恰當的生命週期並且能夠及時取消註冊是一件很棘手的事情。不過一般來說,KVO有助於確保不同對像中的值協調一致;如前所述,Cocoa中的一些地方希望你使用KVO。

KVO中被觀察者與觀察者都要繼承自NSObject。此外,如果被觀察的屬性聲明在Swift中,那就必須將其標記為dynamic,否則KVO將無法使用(原因在於KVO通過改寫訪問器方法來工作;Cocoa要能進入方法中並修改對像代碼才可以,如果屬性沒有聲明為dynamic,那麼這一切是無法實現的)。