KVO,即:Key-Value Observing,是 Objective-C 對 觀察者模式(Observer Pattern)的實現。它提供一種機制,當指定的對象的屬性被修改後,觀察者就會接受到通知。簡單的說就是每次指定的被觀察的對象的屬性被修改後,KVO就會自動通知相應的觀察者了。html
KVO本質上是基於runtime的動態分發機制,經過key來監聽value的值。 OC可以實現監聽由於都遵照了NSKeyValueCoding協議。OC全部的類都是繼承自NSObject,其默認已經遵照了該協議,但Swift不是基於runtime的。Swift中繼承自NSObject的屬性處於性能等方面的考慮,默認是關閉動態分發的, 因此沒法使用KVO,只有在屬性前加 @objc dynamic 纔會開啓運行時,容許監聽屬性的變化。swift
在Swift3中只須要加上dynamic就能夠了,而Swift4之後則還須要@objc安全
- (void)addObserver:(NSObject *)observer
forKeyPath:(NSString *)keyPath
options:(NSKeyValueObservingOptions)options
context:(void *)context;
複製代碼
observer:觀察者,也就是KVO通知的訂閱者。訂閱着必須實現。
keyPath:描述將要觀察的屬性,相對於被觀察者。
options:KVO的一些屬性配置;有四個選項。bash
NSKeyValueObservingOptionNew:change字典包括改變後的值
NSKeyValueObservingOptionOld:change字典包括改變前的值
NSKeyValueObservingOptionInitial:註冊後馬上觸發KVO通知
NSKeyValueObservingOptionPrior:值改變前是否也要通知(這個key決定了是否在改變前改變後通知兩次)
複製代碼
context:上下文,這個會傳遞到訂閱着的函數中,能夠爲kvo的回調方法傳值。是unsafePointer類型,表示不安全的指針類型(由於在Swift手動操做指針,修改內存是一件很是不安全且不考靠的行爲),能夠傳入一個指針地址。app
在觀察者內重寫這個方法。在屬性變化時,觀察者則能夠在函數內對屬性變化作處理。ide
- (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。學習
主要方法:ui
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指針被指向了一個新建的子類 ,且這個子類重寫了被觀察值的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。
能夠發現: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
複製代碼
能夠發現:
.cxx_destruct方法本來是爲了C++對象析構的,ARC借用了這個方法插入代碼實現了自動內存釋放的 工做。具體的實現能夠查看一下這邊文章ARC下dealloc過程及.cxx_destruct的探究。