KVO & KVC

由于oc的语言特性,使得开发者根本不必进行任何操作就可以进行属性的动态读写,这种方式就是Key Value Coding(简称KVC)。

KVC的操作方法由NSKeyValueCoding协议提供,而NSObject就实现了这个协议,也就是说OC中几乎所有的对象都支持KVC操作。

假设现在要利用KVC对a属性进行读取。

如果是动态设置属性,则优先考虑调用setA方法。如果没有该方法则优先考虑搜索成员变量_a,如果仍然不存在则搜索成员变量a,如果最后仍然没有搜索到这会调用这个类的setValue:forUndefinedKey:方法。在搜索过程中,不管这些方法、成员变量是私有还是公共的都能正确设置。

如果是动态读取属性,则优先调用a的getter方法,如果没有搜索到则会优先搜索成员变量_a,如果仍然不存在则会搜索成员变量a,如果仍然没搜索到就会调用这个类的valueforUndefinedKey:方法。而且,在搜索过程中,不管这些方法、成员变量是私有的还是公有的都能正确读取。

调用方式

// 直接调用set方法,或者通过属性的点语法间接调用

[account setName:@"Savings"];

// 使用KVC的setValue:forKey:方法

[account setValue:@"Savings" forKey:@"name"];

// 使用KVC的setValue:forKeyPath:方法

[document setValue:@"Savings" forKeyPath:@"account.name"];

// 通过mutableArrayValueForKey:方法获取到代理对象,并使用代理对象进行操作

Transaction *newTransaction = <#Create a new transaction for the account#>;

NSMutableArray *transactions = [account mutableArrayValueForKey:@"transactions"];

[transactions addObject:newTransaction];

关于容器类(如:NSMutableArray)的观察, 当通过addObject: 向数组中添加对象, 不会触发KVO, 因为并没有触发set方法,解决方法: 通过KVC 方法 - mutableArrayValueForKey:

key 与 KeyPath要区分开来,key 可以从一个对象中获取值,而KeyPath可以将多个 key 用点号 "." 分割连接起来 . 比如:@"account.name" 相当于

 [p valuefForKey:@"account"]   valuefForKey:@"name" ];

KVO全称KeyValueObserving,是苹果提供的一套事件通知机制,主要用来做键值观察操作,想要一个对象的属性值发生改变后通知观察对象变并触发事件回调。依赖于Runtime机制来实现。KVO对被监听对象无侵入性,不需要修改其内部代码即可实现监听。一般继承自NSObject的对象都默认支持KVO。

使用KVO分为三个步骤:

通过addObserver:forKeyPath:options:context:方法注册观察者,观察者可以接收keyPath属性的变化事件。

在注册观察者时,可以传入options参数,参数是一个枚举类型。如果传入NSKeyValueObservingOptionNew和NSKeyValueObservingOptionOld表示接收新值和旧值,默认为只接收新值。如果想在注册观察者后,立即接收一次回调,则可以加入NSKeyValueObservingOptionInitial枚举。

还可以通过方法context传入任意类型的对象,在接收消息回调的代码中可以接收到这个对象,是KVO中的一种传值方式。还可以用来精准区分父类和子类同时对一个属性进行观察时所需要做的不同处理。

在观察者中实现observeValueForKeyPath:ofObject:change:context:方法,当keyPath属性发生改变后,KVO会回调这个方法来通知观察者。

当观察者不需要监听时,可以调用removeObserver:forKeyPath:方法将KVO移除。

观察者需要实现observeValueForKeyPath:ofObject:change:context:方法,当KVO事件到来时会调用这个方法,如果没有实现会导致Crash。

KVO的addObserver和removeObserver需要是成对的,如果重复remove则会导致NSRangeException类型的Crash。在调用addObserver方法后,KVO并不会对观察者进行强引用,所以需要注意观察者的生命周期,否则会导致观察者被释放带来的Crash。

KVO基本原理

当某个类A的属性p对象第一次被观察时,系统就会在运行期动态的创建A类的一个派生类B(子类)。如果原类为A,那么生成的派生类名为NSKVONotifying_A.

在这个派生类B中重写A类被观察属性P的setter方法,更改观察对象p的isa指针指向这派生类B。派生类B在重写的setter方法内实现真正的通知机制 1[super set:] 2、通知观察者,告诉属性改变。

所以对象的isa指针的值并不一定反映对象的实际类。应该使用[A class]方法来确定对象实例的类。更改对象属性值的时候会调用对象p的setter方法此时就会去调用B类里面重写的setter方法(因为更改了p内部isa指针的指向,使得此时的isa指针指的是派生类,而不是原生类)

这个派生类B对外部默认是隐藏的,因此会重写自己的class方法,使得返回值是它继承的类A的Class。

NSKVONotifying_A类中有_isKVOA方法,这个方法可以当做使用了KVO的一个标记,系统可能也是这么用的。如果我们想判断当前类是否是KVO动态生成的类,就可以从方法列表中搜索这个方法。

设置属性会调用 setter 方法,要通过遵循 KVO 的属性设置方式来变更属性值,如果仅是直接修改属性对应的成员变量,是无法实现 KVO。

例如:_age = 10;就不会触发KVO。

键值观察通知触发依赖于NSObject的两个方法:willChangeValueForKey:和didChangeValueForKey: 

被观察属性发生改变之前,willChangeValueForKey:会被调用,这就会记录旧的值,继而observeValueForKey:ofObject:change:context: 会被调用,而当改变发生后,didChangeValueForKey:会被调用。

系统默认是自动触发的,也可以手动控制 就需要自己调用willChangeValueForKey:和didChangeValueForKey:这两个方法。

直接修改成员变量会触发KVO吗?

不会,KVO的本质是set方法,只有调用了set方法才会触发KVO。

object_getClass

调用runtime的object_getClass函数,就可以获取到真正的类,因为调用object_getClass函数后其返回的是一个Class类型,Class是objc_class定义的一个typedef别名,通过object_getClass就可以获取到对象的isa指针指向的Class,也就是对象的真实的类对象。object_getClass函数内部返回的是对象的isa指针。

KVO开源第三方框架----KVOController

想在项目中安全便捷的使用KVO的话,推荐Facebook的一个KVO开源第三方框架-KVOVC。KVOController本质上是对系统KVO的封装,具有原生KVO所有的功能,而且规避了原生KVO的很多问题,兼容block和action两种回调方式。

参考地址:

KVO用法总结 - Null959_的博客 - CSDN博客  详细讲解观察者注册,实现,移除的方法中的每个参数的具体含义  

KVO的本质 -   可以从现象倒推KVO在运行时都做了什么 

KVO实现剖析  可以帮助我们理解KVO内部的实现细节 

你可能感兴趣的:(KVO & KVC)