iOS开发中的KVC 和 KVO

在项目开发中,KVC 和 KVO多次被使用到。KVC用来设置对象的属性值和获取对象的属性值,KVO 可以用来监听对象属性值的变化。那么,苹果是如何实现KVO的呢?以及KVO在实际使用中有什么注意事项呢(KVO使用错误的话,程序会直接crash)?本文从这几个问题入手,介绍下 KVC 和 KVO。

KVC

KVC 全称 Key Value Coding,用来动态的设置对象的属性值,以及动态的获取对象的属性值。KVC的实现依赖于 NSKeyValueCoding 协议,而NSObject 类已经实现了该协议,因此Objective-C中几乎所有的对象都可以使用KVC 操作。

KVC的使用

KVC相关的方法分两种:
(1)设置对象属性值
setValue:value forKey:key (用于简单路径,也就是直接属性)
setValue:value forKeyPath:key (用于复杂路径,也就是间接属性)
(2)获取对象属性值
valueForKey:key (用于获取简单路径的属性值)
valueForKeyPath:key (用于获取复杂路径的属性值)
可以看到,设置对象属性值和获取对象属性值是一一对应的。
下面写代码来看一下KVC的使用。
假设有Dog类和Person类,Person对象有一个Dog对象。
Dog类的定义如下:

@interface Dog : NSObject
@property (nonatomic, copy) NSString *name;
@end

Dog有一个name属性。
Person类的定义如下:

@class Dog;
@interface Person : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *sex;
@property (nonatomic, strong) Dog *dog;
@end

使用KVC代码如下:

Person *person = [[Person alloc] init];
//kvc  设置值
[person setValue:@"Jim" forKey:@"name"];
[person setValue:@"boy" forKey:@"sex"];

//kvc 获得值
NSLog(@"name = %@  sex = %@",[person valueForKey:@"name"],[person valueForKey:@"sex"]);

Dog *dog = [[Dog alloc] init];
person.dog = dog;
//kvc  设置复杂路径值
[person setValue:@"Teddy" forKeyPath:@"dog.name"];
//kvc  获得复杂路径值
NSLog(@"dog name = %@",[person valueForKeyPath:@"dog.name"]);

KVC设置属性和读取属性的原则

先看一行代码:

 [person setValue:@"Jim" forKey:@"name"];

person对象是如何找到name变量,并且给他赋值 Jim呢?
KVC在设置属性时遵循以下顺序:
(1)首先查看setter方法是否存在,如果存在,则直接调用setter方法;否则进行下一步
(2)搜索成员变量 _name,如果找到了,则对 _name进行赋值;否则进行下一步
(3)搜索成员变量 name,如果找到了,则对 name 进行赋值;否则进行下一步
(4)调用 setValue: forUndefineKey 方法
同理,KVC在读取属性时的顺序和设置相似:
(1)首先查看getter方法是否存在,如果存在,则直接读取getter方法;否则进行下一步
(2)搜索成员变量_name,如果找到了,读取_name 的值;否则进行下一步
(3)搜索成员变量 name,如果找到了,读取 name的值;否则进行下一步
(4)调用 valueForUndefineKey 方法

KVO

KVO 全称 Key Value Observeing,用于监听对象属性值的变化。KVO的一种使用场景是分离视图和数据模型。视图作为监听器,当数据模型的值变化后,视图做对应的刷新。和KVC 类似,KVO的方法由 NSKeyValueObserving 协议提供,NSObject 已经实现了该协议,因此基本上所有的类对象都可以使用 KVO。
KVO 常用的方法有:
注册指定路径的监听器: addObserver: forKeyPath: option: context:
删除指定路径的监听器: removeObserver: forKeyPath:
触发监听时的方法: observerValueForKeyPath: ofObject: change: context:

KVO的实现原理

KVO 的实现依赖于 NSObject中的 willChangeValueForKey 方法 和 didChangeValueForKey 方法。
当对一个对象的属性添加 Observer 之后,在该属性的值改变之前,会调用 willChangeValueForKey 方法,此时会记录下该属性旧的值;在该属性的值改变之后,会调用 didChangeValueForKey 方法,并通知观察者对象。
实际上,系统实现 KVO 的方法基本上就是上面所说的。在被监听属性的 setter 方法中,以某种方式插入 willChangeValueForKey 、 didChangeValueForKey、observerValueForKeyPath: ofObject: change: context 方法。
那么,系统到底是以何种方式插入对应的方法呢?
事实上,系统使用了 isa-swizzling(isa 混写)的方式实现的 KVO。当观察对象A的属性时,系统会动态的创建一个新的类,新类的名为 NSKVONotifying_A,NSKVONotifying_A 类是 A 类的子类。假设所观察的是 对象A 的 name 属性,那么在新生成的类 NSKVONotifying_A 中,会重写 name属性的 setter 方法。在新的 setter 方法中,负责在赋值之前调用 willChangeValueForKey,在赋值之后调用 didChangeValueForKey。在此过程中,被观察对象的 isa 指针指向了新的类,也就是 NSKVONotifying_A,这样在对 name 属性赋值时,会调用 NSKVONotifying_A 中的 setter 方法,观察者就会收到对应的消息通知。
根据上面的所述,使用 KVO 时,被观察对象的 isa 指针指向了一个新的类,按照常理,如果我们打印被观察对象的 class,应该会输出新的类名。然而,通过代码验证,发现输出被观察对象的 class还是原类名,这是什么原因呢?
这是因为,系统不仅仅是新生成了 NSKVONotifying_A 类,还重写了 class 方法,在class 方法中仍返回原来的类。让我们以为 isa 指针的指向没有改变,实际上,被观察对象已经成为了新生成类的实例对象。
上面说道,使用 KVO时,系统会生成一个新的类,那么如何验证呢?
还是以上面的例子来说,观察对象A的属性,如果我们在工程中新建一个类,类名为 NSKVONotifying_A,当程序运行到 addObserver: forKeyPath: option: context: 代码时会崩溃,这种现象说明两个结论:
(1)观察对象 A 时,确实新生成了一个子类,而且该类名称为 NSKVONotifying_A
(2)新类的生成时机是在运行时生成的,因为只有运行到那行代码才崩溃,而不是编译时报错。
至此,KVO 的实现原理介绍完毕。

KVO的使用注意事项

下面介绍一些 KVO 在使用时的一些坑,或者说一些注意事项。
(1)使用 KVO 结束后,一定要记得移除监听器,即 removeObserver: forKeyPath:
(2)使用KVO 时,对于同一个 keyPath, 如果进行了多次 removeObserver: forKeyPath: 操作,程序会直接 crash 掉。也就是说, addObserver: forKeyPath: options: context: 需要和 removeObserver: forKeyPath: 对应起来。添加一次,使用完毕后,移除一次。对于同一个keyPath,不要重复的添加,也不要重复的移除。
(3)在 UITableView 中使用 KVO时,由于 UITableViewCell有重用机制,一定要注意上面所说的第(2)点,即不要重复的添加,不要重复的移除。
(4)使用 KVO 时,假设是观察A 对象的 name 属性,当在子线程中调用了 name 的 setter 方法时, observerValueForKeyPath: ofObject: change: context: 的调用也是在子线程。因此,倘若在observerValueForKeyPath: ofObject: change: context: 中有刷新 UI 的操作,要注意这个情况,确认是否需要判断是否在主线程,然后进行刷新 UI。

总结

以上就是我关于 KVC 和 KVO的全部理解了,有什么不对的欢迎大家留言交流指出。实际上, KVO 的实现依赖于 Objective-C 的动态行和 runtime。这也是 KVO 被称之为黑魔法的原因,使用简单,但是内部实现逻辑复杂,用到了很多runtime 的知识。

参考资料:

https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/KeyValueObserving/Articles/KVOImplementation.html

猜你喜欢

转载自blog.csdn.net/tugele/article/details/79663795