KVO是什么
- KVO是OC的一种观察者模式,另一种观察者模式是通知机制(notification)
- KVO的机制:指定一个被观察对象,当该对象某个属性发生变化时,观察对象会获得通知,并作出相应动作,并且被观察的对象不需要添加任何额外的代码
在MVC设计架构下,KVO机制通常实现数据模型和视图之间的通讯,这样可以保证数据和视图显示达到同步。
实现原理
KVO 的实现依赖于OC强大的运行时 Runtime
。
原理:
当观察某对象时,KVO动态创建该对象的子类,将原始类和子类的属性设置setter
方法进行交互,并重写子类被观察属性 setter
方法,随后通知观察者该属性的变化状况。
因此运行时主要做了三件事。
- 创建子类
- 方法交换
- 通知观察者
运行时在 KVO 机制中扮演了黑客的身份,截获了用户的信息,稍加改造之后继续发出去,但同时将用户的信息解析之后贩卖给了别人(监听者)。
实现过程:
Apple使用方法交换(isa-swizzling)来实现KVO。
当观察对象A时,KVO动态创建了新的名为 NSKVONotifying_A
的新类,该类是对象A的子类,并且使用 swizzling
交换了所观察的属性的 setter
方法,KVO重写了新类的观察属性的 setter
方法,在调用原类中的 setter
方法后,通知所有观察者该属性的变化情况。
- NSKVONotifying_A
每个对象内部都有 isa
指针,这个指针指向该对象的类,在KVO机制中,该isa
指针被修改为指向系统新创建的子类 NSKVONotifying_A
,那么当被观察者修改被检测的属性的值时候,就会调用KVO重写的setter
方法,从而激活键值通知机制,实现当前类属性改变的监听。
所以当我们从应用层面上来看,并没有意识到有新类的出现,这是apple隐瞒类对KVO的底层实现过程,而我们还以为是原来的类,但是此时如果我们创建一个新的名为 NSKVONotifying_A
的类时,就会发现系统运行到注册KVO的那段代码时,程序发生崩溃,因为系统在注册监听的时候动态创建了名为NSKVONotifying_A
的中间类,并指向这个中间类了。
- 子类重写
setter
方法
KVO的键值观察通知依赖于NSObject的两个方法:-willChangeValueForKey:
和 didChangeValueForKey:
,在存取数值的前后分别调用2个方法;
被观察属性发生改变之前,-willChangeValueForKey:
被调用,通知系统该keyPath的属性值即将变更;当改变发生后,-didChangeValueForKey:
被调用,通知系统keyPath的属性值已经发生改变,之后,observeValueForKey:ofObject:change:context:
也会被调用。
注意:重写观察属性的setter方法这种继承方式的注入是在运行时而不是编译时实现的
注意
观察者观察的是属性,只有遵循 KVO 变更属性值的方式才会执行 KVO 的回调方法,例如执行 setter
方法、KVC方式赋值等,如果赋值没有通过KVO来变更属性值,而是直接修改属性对应的成员变量,例如:仅调用 _name=@"newName"
,这时是不会触发KVO机制的,更加不会调用回调方法的,因此,使用KVO机制的前提是遵循KVO属性设置方式来变更属性值。
拓展
1、和KVC的比较?
- KVC,即Key-Value-Coding,是一个非正式协议,使用字符串(key)来访问一个对象实例变量的机制
- KVO,即Key-Value-Observing,它提供一种机制,当被观察者的属性值更改时,观察者就会接收到通知
2、和通知的区别?
- 相比于KVO重写setter时调用
observeValueForKey:ofObject:change:context:
方法,通知要多发送通知操作,例如重写类的setter方法,使用通知也可以实现类似KVO的监听 - 两者都是一对多,通知监听不局限于属性的变化,还可以是状态的变化,监听范围广,例如键盘的出现、app进入后台等,使用也更灵活方便,但是通知的开销要大。
3、和delegate的不同?
KVO、通知、delegate三者都可以实现类之间的通信,但是delegate不同的是:
- KVO和通知都负责发送和接收通知,剩下的事情都由系统来完成,所以不用返回值,而delegate则需要协议和代理对象来关联
- delegate适用于一对一,KVO和通知则适用于一对多情况
4、涉及的技术
KVC和KVO实现的根本是OC语言的动态性和运行时runtime,以及访问器方法的实现。
总结
相比如其他消息回调的方式,KVO机制的实现更多的依赖于系统支持,它能够提供被观察属性的 newValue
和 oldValue
;但是由于需要创建对应的子类、重写setter方法等,内存消耗也是很大的,所以对于两个类之间的消息通信,我们应该根据实际应用的场景来选择通信方式。
另外需要注意的是,由于这中继承方式的注入是在运行时而非编译时实现,所以当没有观察者时,KVO不会有任何开销,此时也根本就没有KVO代码存在,但是如果时委托和通知那还是需要开销,这也是KVO零开销观察的优势。