深入学习KVC和KVO。研究源码NSKeyValueCoding.h和NSKeyValueObserving.h.在Foundation框架中。
KVC
什么是 KVC?
- KVC 是 Key-Value-Coding 的简称。
- KVC 是一种可以直接通过字符串的名字 key 来访问类属性的机制,而不是通过调用 setter、getter 方法去访问。
- 我们可以通过在运行时动态的访问和修改对象的属性。而不是在编译时确定,KVC 是 iOS 开发中的黑魔法之一。
键值编码(key-value coding)是一种间接更改对象状态的方式。通过传入的字符串(key)查找要更改的对象的状态。查找的规则是,先查找以字符串(key)命名的getter和setter方法。如果没有找到对应的方法,再查找key和_key的实例变量。KVC 是字典转模型,模型转字典的神器
KVC的主要方法
KVC 定义了一种按名称访问对象属性的机制,支持这种访问的主要方法是:
- 设置值
keyPath包含了key的功能
key:只能访问当前对象的属性
keyPath:能利用运算符一层一层往内部访问属性
对于标量值,会自动进行装箱和拆箱。
1 | // value的值为OC对象,如果是基本数据类型要包装成NSNumber |
- 获取值:
1 | - (id)valueForKey:(NSString *)key; |
KVC的底层实现
当一个对象调用setValue方法时,方法内部会做以下操作:
①检查是否存在相应key的set方法,如果存在,就调用set方法
②如果set方法不存在,就会查找与key相同名称并且带下划线的成员属性,如果有,则直接给成员属性赋值
③如果没有找到_key,就会查找相同名称的属性key,如果有就直接赋值
④如果还没找到,则调用valueForUndefinedKey:和setValue:forUndefinedKey:方法。
这些方法的默认实现都是抛出异常,我们可以根据需要重写它们。
KVC 与点语法比较
用 KVC 访问属性和用点语法访问属性的区别:
- 用点语法编译器会做预编译检查,访问不存在的属性编译器会报错,但是用 KVC 方式编译器无法做检查,如果有错误只能运行的时候才能发现(crash)。
- 相比点语法用 KVC 方式 KVC 的效率会稍低一点,但是灵活,可以在程序运行时决定访问哪些属性。
- 用 KVC 可以访问对象的私有成员变量。
应用
字典转模型
- (void)setValuesForKeysWithDictionary:(NSDictionary *)keyedValues;
KVC 总结
键值编码是一种间接访问对象的属性使用字符串来标识属性,而不是通过调用存取方法直接或通过实例变量访问的机制,非对象类型的变量将被自动封装或者解封成对象,很多情况下会简化程序代码。
优点:
- 不需要通过 setter、getter 方法去访问对象的属性,
- 可以访问私有成员变量的值,可以间接修改私有变量的值。—重点
- 可以轻松处理集合类(NSArray)。
缺点:
- 一旦使用KVC你的编译器无法检查出错误,即不会对设置的键、键值路径进行错误检查。
- 执行效率要低于 setter 和 getter 方法。因为使用 KVC 键值编码,它必须先解析字符串,然后在设置或者访问对象的实例变量。
- 使用 KVC 会破坏类的封装性。
KVO
KVO 是 Key-Value-Observing 的简称。在Foundation框架中的NSKeyValueObserver.h文件中。
KVO 是一个观察者模式。观察一个对象的属性,注册一个指定的路径,若这个对象的的属性被修改,则 KVO 会自动通知观察者。
更通俗的话来说就是任何对象都允许观察其他对象的属性,并且可以接收其他对象状态变化的通知。
1 | 1.// 注册观察者,实施监听; |
KVO是基于runtime机制实现的
Apple 使用了 isa 搅拌技术(isa-swizzling)来实现的 KVO 。当一个观察者注册对象的一个属性 isa 观察对象的指针被修改,指着一个中间类而不是在真正的类。
当某个类的对象第一次被观察时,系统就会在运行期动态地创建该类的一个派生类,在这个派生类中重写基类中任何被观察属性的 setter 方法。
派生类在被重写的 setter 方法实现真正的通知机制(Person->NSKVONotifying_Person)
当你观察一个对象时,一个新的类会动态被创建。这个类继承自该对象的原本的类,并重写了被观察属性的 setter 方法。自然,重写的 setter 方法会负责在调用原 setter 方法之前和之后,通知所有观察对象值的更改。最后把这个对象的 isa 指针 ( isa 指针告诉 Runtime 系统这个对象的类是什么 ) 指向这个新创建的子类,对象就神奇的变成了新创建的子类的实例。
原来,这个中间类,继承自原本的那个类。不仅如此,Apple 还重写了 -class 方法,企图欺骗我们这个类没有变,就是原本那个类。更具体的信息,去跑一下 Mike Ash 的那篇文章里的代码就能明白,这里就不再重复。
KVO底层实现
KVO-键值观察机制,原理如下:
1.当给A类添加KVO的时候,runtime动态的生成了一个子类NSKVONotifying_A,让A类的isa指针指向NSKVONotifying_A类,重写class方法,隐藏对象真实类信息
2.重写监听属性的setter方法,在setter方法内部调用了Foundation 的 _NSSetObjectValueAndNotify 函数
3._NSSetObjectValueAndNotify函数内部
a) 首先会调用 willChangeValueForKey
b) 然后给属性赋值
c) 最后调用 didChangeValueForKey
d) 最后调用 observer 的 observeValueForKeyPath 去告诉监听器属性值发生了改变 .
4.重写了dealloc做一些 KVO 内存释放
当你观察一个对象时,一个新的类会被动态创建。这个类继承自该对象的原本的类,并重写了被观察属性的 setter 方法。重写的 setter 方法会负责在调用原 setter 方法之前和之后,通知所有观察对象:值的更改。最后通过 isa 混写(isa-swizzling) 把这个对象的 isa 指针 (isa 指针告诉 Runtime 系统这个对象的类是什么) 指向这个新创建的子类,对象就神奇的变成了新创建的子类的实例,如下所示:
KVO在调用存取方法之前总是调用 willChangeValueForKey: ,之后总是调用 didChangeValueForkey: 怎么做到的呢?
答案是通过 isa 混写(isa-swizzling)。
第一次对一个对象调用 addObserver:forKeyPath:options:context:
时,框架会创建这个类的新的 KVO 子类,并将被观察对象转换为新子类的对象。在这个 KVO 特殊子类中, Cocoa 创建观察属性的 setter ,大致工作原理如下:
1 | - (void)setNow:(NSDate *)aDate { |
这种继承和方法注入是在运行时而不是编译时实现的。这就是正确命名如此重要的原因。只有在使用KVC命名约定时,KVO才能做到这一点。
应用
AFNetworking
在AFN中的AFURLSessionManager中使用了KVO
UIProgressView (AFNetworking)
面试题
- [※※]addObserver:forKeyPath:options:context:各个参数的作用分别是什么,observer中需要实现哪个方法才能获得KVO回调?
1 | // 添加键值观察 |
observer中需要实现一下方法:
1 | // 所有的 kvo 监听到事件,都会调用此方法 |
- [※※※]如何手动触发一个value的KVO?
自动触发 KVO 的原理:
键值观察通知依赖于 NSObject 的两个方法: willChangeValueForKey:
和 didChangevlueForKey:
。在一个被观察属性发生改变之前, willChangeValueForKey: 一定会被调用,这就 会记录旧的值。而当改变发生后, observeValueForKey:ofObject:change:context:
会被调用,继而 didChangeValueForKey: 也会被调用。
如果可以手动实现这些调用,就可以实现“手动触发”了。
- [※※※]若一个类有实例变量NSString *_foo,调用setValue:forKey:时,可以以foo还是_foo作为key?
都可以。 - [※※※※]KVC的keyPath中的集合运算符如何使用?
- 必须用在集合对象上或普通对象的集合属性上
- 简单集合运算符有@avg, @count , @max , @min ,@sum,
- 格式 @”@sum.age”或 @”集合属性.@max.age“
- [※※※※]KVC和KVO的keyPath一定是属性么?
[※※※※※]如何关闭默认的KVO的默认实现,并进入自定义的KVO实现?
[※※※※※]apple用什么方式实现对一个对象的KVO?
参考:
https://github.com/leejayID/KVC-KVO
Objective-C中的KVC和KVOhttp://yulingtianxia.com/blog/2014/05/12/objective-czhong-de-kvche-kvo/#KVO
如何自己动手实现 KVOhttp://tech.glowing.com/cn/implement-kvo/