Swift 4 前後 KVO 的變化

FFIB發表於2018-02-01

如果瞭解過設計模式的同學,應該都知道有一種設計模式叫做觀察者模式,屬於行為型模式,即當物件存在一對多的依賴關係,當一個物件發生變化時,需要自動通知它的依賴物件。通常用於實時事件處理。

我們來研究一下 iOS 裡對觀察者模式的支援,即 KVO(key-value observing) ,鍵值對觀察,其原理是基於 KVC(key-value-coding)runtime。通過 Swfit 研究。

KVO 使用

由於 Swift4 之後 KVOapi 有所改變,所以先來看看 Swift4 之前使用 KVO

class Test: NSObject {
    dynamic var field = "field"
}
複製程式碼
var test = Test()
override func viewDidLoad() {
    super.viewDidLoad()
    test.addObserver(self, forKeyPath: "field", options: [.new, .initial], context: nil)
    test.field = "change"
}

override func observeValue(forKeyPath keyPath: String?,
                           of object: Any?,
                           change: [NSKeyValueChangeKey : Any]?,
                           context: UnsafeMutableRawPointer?) {
    print(change)
}
deinit {
    test.removeObserver(self, forKeyPath: "field")
}
複製程式碼

在Swift4之前使用 KVO ,即需要在 deinit 中呼叫 removeObserver ,否則會crash,還需要重寫 NSObject

func observeValue(forKeyPath keyPath: String?, 
                  of object: Any?, 
                  change: [NSKeyValueChangeKey : Any]?, 
                  context: UnsafeMutableRawPointer?)
複製程式碼

以完成更多的操作。

而在Swift4中,KVO的 API 變得友好很多。

@objcMembers class Test: NSObject {
    dynamic var field = "field"
}

var observer: NSKeyValueObservation!
    override func viewDidLoad() {
        super.viewDidLoad()
        let test = Test()
        observer = test.observe(\.field, options: [.new, .initial]) { (object, change) in
            print(change)
        }
        test.field = "change"
    }
複製程式碼

通過閉包優化了 KVO 的實現,在官方WWDC視訊上What's New in Foundation專題上介紹 KVO 時,表示該函式會返回一個 NSKeyValueObservation,所以只需要管理這個例項的生命週期,不再需要移除觀察者,再也不用擔心忘記移除觀察者而導致crash。(ps: 可以嘗試在方法內宣告 NSKeyValueObservation 物件,可以發現即使改變了屬性值也不會呼叫閉包內的操作。 因為隨著方法的結束,這個例項和閉包的生命週期都結束了。)

在閉包中第一個引數是對被觀察者的引用,防止在閉包內使用被觀察者而導致迴圈引用的問題(相當nice)。

遺憾的是隻有繼承於 NSObject 的物件才能夠使用 KVO

KVO 原理

Swift 4 之前

採用斷點除錯

Swift 4 前後 KVO 的變化
addObserver 之前

Swift 4 前後 KVO 的變化
addObserver 之後

可見,在 addObserver 之後, test 例項會將isa指標指向 NSKVONotifying_Test 的派生類

再通過runtime來具體看看這個兩個類的區別

Swift 4 前後 KVO 的變化
採用輔助函式,檢視 test 類和方法的變化。

func find() {
    print(NSStringFromClass(object_getClass(test)!))
    print(String(describing: class_getName(object_getClass(test))))
    var count: UInt32 = 0
    let methodlist = class_copyMethodList(object_getClass(test), &count)
    for i in 0..<count {
        print(NSStringFromSelector(method_getName(methodlist![Int(i)])))
    }
    print("\n")
}
複製程式碼

Swift 4 前後 KVO 的變化
addObserver之前

Swift 4 前後 KVO 的變化
addObserver之後

從上圖輸出結果可見,在 addObserver 之後類發生了改變,並且新增了一個私有屬性 _isKVOA,從名字可以推測是用於對類標示,以此來標示是 KVO

從圖中可以看出 NSKVONotifying_Test 重寫了被觀察屬性 fieldset 方法(即 setField: )。再來看看具體是怎麼重寫的。

根據 KVOapi 有手動呼叫的方法。

func willChangeValue(forKey key: String)
func didChangeValue(forKey key: String)
複製程式碼

可以推測是在 set 方法內新增 func willChangeValue(forKey key: String)func didChangeValue(forKey key: String)

通過堆疊資訊具體看一看呼叫情況。

Swift 4 前後 KVO 的變化
Swift 4 前後 KVO 的變化

第一次呼叫時 initial ,第二次是屬性發生變化時呼叫的。從堆疊資訊一目瞭然。

Swift 4 之後

接下來我們來探討下,Swift4 之後 KVO的新 API ,具體的底層原理。 先根據上面的方法測試下是否是通過 runtime 新增 NSKVONotifying_Test 派生類實現的。

Swift 4 前後 KVO 的變化

Swift 4 前後 KVO 的變化

Swift 4 前後 KVO 的變化

Swift 4 前後 KVO 的變化

從測試結果可見,與 Swift4 之前的原理一致。但是區別在於 NSKVONotifying_Test 的生命週期由 NSKeyValueObservation 管理,通過斷點除錯看看 NSKeyValueObservation

Swift 4 前後 KVO 的變化
從上圖可見, NSKeyValueObservation 內有一個 object 屬性 是指向觀察者 testcallback 回撥閉包,以及 path 代指被觀察的屬性。

Swift 4 前後 KVO 的變化

從堆疊資訊,可以一目瞭然的看到當被觀察屬性發生改變時,呼叫情況。 NSKeyValueObservation 作為了觀察者和訊息轉發者,接收通知和通知 test 的屬性發生改變,從而呼叫 閉包 內的具體操作。

才疏學淺,如有什麼理解不到位的歡迎指出。

相關文章