iOS KVO學習記錄

即將成為型男的濤發表於2019-04-02

前言

iOS KVO學習記錄

一、概述

KVO,即:Key-Value Observing,是 Objective-C 對 觀察者模式(Observer Pattern)的實現。它提供一種機制,當指定的物件的屬性被修改後,觀察者就會接受到通知。簡單的說就是每次指定的被觀察的物件的屬性被修改後,KVO就會自動通知相應的觀察者了。

二、使用

1、基本使用步驟

KVO本質上是基於runtime的動態分發機制,通過key來監聽value的值。 OC能夠實現監聽因為都遵守了NSKeyValueCoding協議。OC所有的類都是繼承自NSObject,其預設已經遵守了該協議,但Swift不是基於runtime的。Swift中繼承自NSObject的屬性處於效能等方面的考慮,預設是關閉動態分發的, 所以無法使用KVO,只有在屬性前加 @objc dynamic 才會開啟執行時,允許監聽屬性的變化。

在Swift3中只需要加上dynamic就可以了,而Swift4以後則還需要@objc

  • 註冊
- (void)addObserver:(NSObject *)observer 
            forKeyPath:(NSString *)keyPath 
            options:(NSKeyValueObservingOptions)options 
            context:(void *)context;
複製程式碼

observer:觀察者,也就是KVO通知的訂閱者。訂閱著必須實現。
keyPath:描述將要觀察的屬性,相對於被觀察者。
options:KVO的一些屬性配置;有四個選項。

NSKeyValueObservingOptionNew:change字典包括改變後的值
NSKeyValueObservingOptionOld:change字典包括改變前的值
NSKeyValueObservingOptionInitial:註冊後立刻觸發KVO通知
NSKeyValueObservingOptionPrior:值改變前是否也要通知(這個key決定了是否在改變前改變後通知兩次)
複製程式碼

context:上下文,這個會傳遞到訂閱著的函式中,可以為kvo的回撥方法傳值。是unsafePointer型別,表示不安全的指標型別(因為在Swift手動操作指標,修改記憶體是一件非常不安全且不考靠的行為),可以傳入一個指標地址。

  • 監聽

在觀察者內重寫這個方法。在屬性變化時,觀察者則可以在函式內對屬性變化做處理。

- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(id)object
                        change:(NSDictionary *)change
                       context:(void *)context
複製程式碼
  • 移除

在不用的時候,不要忘記解除註冊,否則會導致記憶體洩露。

- (void)removeObserver:(NSObject *)observer 
                forKeyPath:(NSString *)keyPath;
複製程式碼

舉例:

class ObservedClass: NSObject {
    // 開啟執行時,允許監聽屬性的變化
    @objc dynamic var name: String = "Original"
    // age 並不會觸發KVO
    var age: Int = 18
}

class ViewController: UIViewController {
    var observed = ObservedClass()
    override func viewDidLoad() {
        super.viewDidLoad()
        
        observed.addObserver(self, forKeyPath: "age", options: [NSKeyValueObservingOptions.new, NSKeyValueObservingOptions.old], context: nil)
        observed.addObserver(self, forKeyPath: "name", options: [NSKeyValueObservingOptions.new, NSKeyValueObservingOptions.old], context: nil)
        // 修改屬性值,觸發KVO
        observed.name = "JiangT"
        observed.age = 22
    }
    
    override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
        print("屬性改變了")
        print(keyPath)
        print("change字典為:")
        print(change)
    }
}

---輸出結果---
屬性改變了
Optional("name")
change字典為:
Optional(
[__C.NSKeyValueChangeKey(_rawValue: new): JiangT, 
__C.NSKeyValueChangeKey(_rawValue: kind): 1, 
__C.NSKeyValueChangeKey(_rawValue: old): Original])
複製程式碼

從上面的程式碼上可以看到,name和age屬性都進行了設定,但是在監聽中,只有收到name修改的回撥。證明了在swift中預設是關閉動態分發的,所以無法使用KVO。

2、手動KVO 及 禁用KVO

  1. 首先,需要手動實現屬性的 setter 方法,並在設定操作的前後分別呼叫 willChangeValueForKey: 和 didChangeValueForKey方法,這兩個方法用於通知系統該 key 的屬性值即將和已經變更了。
  2. 其次,要實現類方法 automaticallyNotifiesObserversForKey,並在其中設定對該 key 不自動傳送通知(返回 NO 即可)。這裡要注意,對其它非手動實現的 key,要轉交給 super 來處理。
  3. 如果需要禁用該類KVO的話直接automaticallyNotifiesObserversForKey返回NO,實現屬性的 setter 方法,不進行呼叫willChangeValueForKey: 和 didChangeValueForKey方法。

主要方法:

open func willChangeValue(forKey key: String)

open func didChangeValue(forKey key: String)

class func automaticallyNotifiesObservers(forKey key: String) -> Bool
複製程式碼

舉例:

---被觀察類---
class ObservedClass: NSObject {
 
    private var _name: String = "Original"
    @objc dynamic var name: String {
        get {
            return _name
        }
        set (n) {
            self.willChangeValue(forKey: "name")
            _name = n
            self.didChangeValue(forKey: "name")
        }
    }
    
    override class func automaticallyNotifiesObservers(forKey key: String) -> Bool {
        // 設定對該 key 不自動傳送通知
        if key == "name" {
            return false
        }
        return super.automaticallyNotifiesObservers(forKey: key)
    }
}

class ViewController: UIViewController {
    var observed = ObservedClass()
    override func viewDidLoad() {
        super.viewDidLoad()
        
        observed.addObserver(self, forKeyPath: "name", options: [NSKeyValueObservingOptions.new, NSKeyValueObservingOptions.old], context: nil)
        // 修改屬性值,觸發KVO
        observed.name = "JiangT"
    }
    
    override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
        print("屬性改變了")
        print(keyPath)
        print("change字典為:")
        print(change)
    }
}

---輸出結果---
屬性改變了
Optional("name")
change字典為:
Optional([__C.NSKeyValueChangeKey(_rawValue: kind): 1, 
__C.NSKeyValueChangeKey(_rawValue: old): Original, 
__C.NSKeyValueChangeKey(_rawValue: new): JiangT])
複製程式碼

三、實現原理

Key-Value Observing Programming Guide中原文如下:

Automatic key-value observing is implemented using a technique called isa-swizzling.

The isa pointer, as the name suggests, points to the object's class which maintains a dispatch table. This dispatch table essentially contains pointers to the methods the class implements, among other data.

When an observer is registered for an attribute of an object the isa pointer of the observed object is modified, pointing to an intermediate class rather than at the true class. As a result the value of the isa pointer does not necessarily reflect the actual class of the instance.

You should never rely on the isa pointer to determine class membership. Instead, you should use the class method to determine the class of an object instance.

大致意思為:

蘋果使用了一種isa交換的技術,當ObjectA的被觀察後,ObjectA物件的isa指標被指向了一個新建的子類NSKVONotifying_ObjectA,且這個子類重寫了被觀察值的setter方法和class方法,dealloc和_isKVO方法,然後使ObjectA物件的isa指標指向這個新建的類,然後事實上ObjectA變為了NSKVONotifying_ ObjectA的例項物件,執行方法要從這個類的方法列表裡找。

所以我們可以得到如下結論:

  • KVO是基於runtime機制實現的。

  • 當某個類的屬性物件第一次被觀察時,系統就會在執行期動態地建立該類的一個派生類(如果原類為ObservedClass,那麼生成的派生類名為NSKVONotifying_ObservedClass),在這個派生類中重寫基類中任何被觀察屬性的setter方法。派生類在被重寫的setter方法內實現真正的通知機制

  • 每個類物件中都有一個isa指標指向當前類,當一個類物件的第一次被觀察,那麼系統會偷偷將isa指標指向動態生成的派生類(isa-swizzling,後續Runtime學習記錄中展開),從而在給被監控屬性賦值時執行的是派生類的setter方法。派生類中還偷偷重寫了class方法,讓我們誤認為還是使用的當前類,從而達到隱藏生成的派生類。

下面我們們用程式碼驗證一下:

class ObservedClass: NSObject {
    // 屬性觀察
    @objc dynamic var normalStr = ""
}

class ViewController: UIViewController {

    var observed = ObservedClass()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // isa    Class    KVODemo.ObservedClass    0x0000000100e8c990
        // isa    Class    NSKVONotifying_KVODemo.ObservedClass    0x00007f9b47403340
        
        print("斷點1:-----被觀察前-----")
        observed.addObserver(self, forKeyPath: "normalStr", options: [NSKeyValueObservingOptions.new, NSKeyValueObservingOptions.old], context: nil)
        print("斷點2:-----被觀察後-----")
    }
}
複製程式碼

在註冊觀察者前後分別打上斷點,檢視observed。

  • 斷點1:
    iOS KVO學習記錄
  • 斷點2:
    iOS KVO學習記錄

可以發現:observed在被觀察之後物件的isa指標被指向了一個新建的子類NSKVONotifying_ObservedClass。但是,我們列印observed的class資訊時,發現返回的還是ObservedClass型別。說明動態建立的派生類NSKVONotifying_ObservedClass重寫了class方法來隱藏自身。

下面我們用runtime檢視一下派生類中的方法列表

class TestClass: NSObject {
    @objc dynamic var name: String = ""
}

let test = TestClass()

var before_count: UInt32 = 0
let before_lists = class_copyMethodList(object_getClass(test), &before_count)!

print("------被觀察前-----")
for i in 0..<before_count {
    let method = before_lists[Int(i)]

    let name = method_getName(method)
    print(name.description)
}

let obj = NSObject()
test.addObserver(obj, forKeyPath: "name", options: [NSKeyValueObservingOptions.new, NSKeyValueObservingOptions.old], context: nil)

var after_count: UInt32 = 0
let after_lists = class_copyMethodList(object_getClass(test), &after_count)!

print("------被觀察後-----")
for i in 0..<after_count {
    let method = after_lists[Int(i)]

    let name = method_getName(method)
    print(name.description)
}

---------輸出結果:---------
------被觀察前-----
.cxx_destruct
name
setName:
init
------被觀察後-----
setName:
class
dealloc
_isKVOA
複製程式碼

可以發現:

  • 派生類中,在內部重寫了class類,用來隱藏自身的存在。
  • 重寫了被觀察屬性setter方法,set方法實現內部會順序呼叫 willChangeValueForKey 方法、原來的 setter 方法實現、didChangeValueForKey 方法,而 didChangeValueForKey 方法內部又會呼叫監聽器的 observeValueForKeyPath:ofObject:change:context: 監聽方法。。

.cxx_destruct方法原本是為了C++物件析構的,ARC借用了這個方法插入程式碼實現了自動記憶體釋放的 工作。具體的實現可以檢視一下這邊文章ARC下dealloc過程及.cxx_destruct的探究