由于oc的语言特性,使得开发者根本不必进行任何操作就可以进行属性的动态读写,这种方式就是Key Value Coding(简称KVC)。
KVC的操作方法由NSKeyValueCoding协议提供,而NSObject就实现了这个协议,也就是说OC中几乎所有的对象都支持KVC操作。
如果是动态设置属性,则优先考虑调用setA方法。如果没有该方法则优先考虑搜索成员变量_a,如果仍然不存在则搜索成员变量a,如果最后仍然没有搜索到这会调用这个类的setValue:forUndefinedKey:方法。在搜索过程中,不管这些方法、成员变量是私有还是公共的都能正确设置。
如果是动态读取属性,则优先调用a的getter方法,如果没有搜索到则会优先搜索成员变量_a,如果仍然不存在则会搜索成员变量a,如果仍然没搜索到就会调用这个类的valueforUndefinedKey:方法。而且,在搜索过程中,不管这些方法、成员变量是私有的还是公有的都能正确读取。
调用方式
kvo调用关于容器类(如:NSMutableArray)的观察, 当通过addObject: 向数组中添加对象, 不会触发KVO, 因为并没有触发set方法,解决方法: 通过KVC 方法 - mutableArrayValueForKey:
key 与 KeyPath要区分开来,key 可以从一个对象中获取值,而KeyPath可以将多个 key 用点号 "." 分割连接起来 . 比如:@"account.name" 相当于
[p valuefForKey:@"account"] valuefForKey:@"name" ];
KVO全称KeyValueObserving,是苹果提供的一套事件通知机制,主要用来做键值观察操作,当一个对象的属性值发生改变后,通知观察对象变并触发事件回调。依赖于Runtime机制来实现。
KVO和NSNotificationCenter都是iOS中观察者模式的一种实现。
区别在于,相对于被观察者和观察者之间的关系,KVO是一对一的,NSNotification 是一对多的。KVO对被监听对象无侵入性,不需要修改其内部代码即可实现监听。一般继承自NSObject的对象都默认支持KVO。
使用KVO分为三个步骤:
1. 通过 addObserver:forKeyPath:options:context: 方法注册观察者,观察者可以接收keyPath属性的变化事件。
options参数枚举类型: 如果传入NSKeyValueObservingOptionNew和NSKeyValueObservingOptionOld表示接收新值和旧值,默认为只接收新值。如果想在注册观察者后,立即接收一次回调,则可以加入NSKeyValueObservingOptionInitial枚举。
context可传入任意类型的对象,在接收消息回调的代码中可以接收到这个对象,是KVO中的一种传值方式。还可以用来精准区分父类和子类同时对一个属性进行观察时所需要做的不同处理。
2. 在观察者中实现 observeValueForKeyPath:ofObject:change:context:方法,当keyPath属性发生改变后,KVO会回调这个方法来通知观察者。如果没有实现会导致Crash。
3. 当观察者不需要监听时,可以调用removeObserver:forKeyPath:方法将KVO移除,一般在delloc 中移除。
KVO的 addObserver 和 removeObserver 需要是成对的,如果重复 remove 会导致NSRangeException 类型的 Crash。在调用 addObserver 方法后,KVO并不会对观察者进行强引用,所以需要注意观察者的生命周期,否则会导致观察者被释放带来的Crash。
KVO基本原理
当对象注册观察者时,对象的ISA指针被修改,指向中间类,而不是原来真正的类
注册后,KVO默认会自动通知观察者。
如果想取消自动通知,改为自己手动控制。需要实现方法:
// 返回NO可取消系统自动通知。
+ (BOOL) automaticallyNotifiesObserversForKey:(NSString*)key。
举例说明:(监听一个Person类底层实现)
1. 系统在运行期动态的创建 Person 类的子类NSKVONotifying_Person.
2. 修改当前对象的isa指针 -> 指向子类 NSKVONotifying_Person;
3. 只要调用对象的set, 顺着isa 指向 会调用NSKVONotifying_Person的 set 方法;此时的isa指针指的是子类,而不是原生类。
4. 重写NSKVONotifying_Person 的 set方法,方法的实现分三步:
1. 调用willChangeValueForKey:方法
2. 调用super 的 setAge: 方法,真正的改变属性的值
3. 调用didChangeValueForKey: 方法,通知监听者属性值已经改变,执行监听者的 observeValueForKeyPath 方法。
NSKVONotifying_A 类中有 isKVOA 方法,可以判断当前类是否是KVO动态生成的类,可以从方法列表中搜索这个方法,是 KVO类 的一个标记。
NSKVONotifying_Person 对外默认是隐藏的。
1 . 使用 runtime 的 object_getClass() 方法: object_getClass(person) ,结果是 NSKVONotifying_Person,反应了真实类型。
2 . 直接调用实例对象的 class 方法:[person class], 结果是 Person
NSKVONotifying_Person 这个类重写了 class 方法,直接返回父类的 class 类型:[Person class]。因此应该使用class 方法来确认对象的真实类型。
系统方法getIsa( ) 为获取 isa 指向的类。
注意: isa 指针不总是指向实例对象所属的类,不能依靠它来确定类型,而是应该用 class 方法来确定实例对象的类。因为KVO的实现机理就是将被观察对象的 isa 指针指向一个中间类而不是真实的类,这是一种叫做isa-swizzling的技术。
KVO的本质是set方法,只有调用了set方法才会触发KVO。
如果直接修改属性对应的成员变量,不会触发 KVO。
例如:_age = 10; 就不会触发KVO。
键值观察通知触发依赖于NSObject的两个方法:系统默认是自动触发的,会调用下面3个方法:
1. willChangeValueForKey: //被 观察属性发生改变之前调用,记录旧的值
2. observeValueForKey:ofObject:change:context:
3. didChangeValueForKey: // 改变发生后调用。
如何手动触发KVO ?
手动调用 willChangeValueForKey 和 didChangeValueForKey 方法。
底层实现参考:
KVO用法总结 - Null959_的博客 - CSDN博客 详细讲解观察者注册,实现,移除的方法中的每个参数的具体含义
KVO的本质 - 简书 可以从现象倒推KVO在运行时都做了什么
KVO实现剖析 可以帮助我们理解KVO内部的实现细节
网友评论