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的探究。