如果了解过设计模式的同学,应该都知道有一种设计模式叫做观察者模式,属于行为型模式,即当对象存在一对多的依赖关系,当一个对象发生变化时,需要自动通知它的依赖对象。通常用于实时事件处理。
我们来研究一下 iOS 里对观察者模式的支持,即 KVO(key-value observing) ,键值对观察,其原理是基于 KVC(key-value-coding) 和 runtime。通过 Swfit 研究。
KVO 使用
由于 Swift4 之后 KVO 的 api 有所改变,所以先来看看 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 之前
采用断点调试

addObserver 之前

addObserver 之后
可见,在 addObserver 之后, test 实例会将isa指针指向 NSKVONotifying_Test 的派生类
再通过runtime来具体看看这个两个类的区别

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")
}

addObserver之前

addObserver之后
从上图输出结果可见,在 addObserver 之后类发生了改变,并且添加了一个私有属性 _isKVOA,从名字可以推测是用于对类标示,以此来标示是 KVO 。
从图中可以看出 NSKVONotifying_Test 重写了被观察属性 field 的 set 方法(即 setField: )。再来看看具体是怎么重写的。
根据 KVO 的 api 有手动调用的方法。
func willChangeValue(forKey key: String)
func didChangeValue(forKey key: String)
可以推测是在 set 方法内添加 func willChangeValue(forKey key: String) 和 func didChangeValue(forKey key: String)
通过堆栈信息具体看一看调用情况。


第一次调用时 initial ,第二次是属性发生变化时调用的。从堆栈信息一目了然。
Swift 4 之后
接下来我们来探讨下,Swift4 之后 KVO的新 API ,具体的底层原理。
先根据上面的方法测试下是否是通过 runtime 新增 NSKVONotifying_Test 派生类实现的。




从测试结果可见,与 Swift4 之前的原理一致。但是区别在于 NSKVONotifying_Test 的生命周期由 NSKeyValueObservation 管理,通过断点调试看看 NSKeyValueObservation 。

NSKeyValueObservation 内有一个 object 属性 是指向观察者 test 和 callback 回调闭包,以及 path 代指被观察的属性。

从堆栈信息,可以一目了然的看到当被观察属性发生改变时,调用情况。
NSKeyValueObservation 作为了观察者和消息转发者,接收通知和通知 test 的属性发生改变,从而调用 闭包 内的具体操作。
才疏学浅,如有什么理解不到位的欢迎指出。