KVO
KVO的使用
KVO(Key Value Observing),键值监听,用于监听对象的属性值的改变。
使用方式如下:
1 | // 被监听对象 |
1 | - (void)viewDidLoad { |
log信息如下:
1 | { |
上述代码可以看出,在添加监听之后,name属性的值在发生改变时,就会通知到监听者,执行监听者的observeValueForKeyPath方法。
注意:在合适的时机要移除监听。
KVO的原理
分析
通过上述代码我们发现,一旦name属性的值发生改变,就会通知到观察者,并且赋值操作都是调用set方法实现的,我们可以重写Animal类中name的set方法,观察是否是KVO在setter方法内部做了一些操作来通知监听者。
我们发现即使重写了set方法,animal对象依然会调用set方法和执行监听器的observeValueForKeyPath方法。
说明KVO在运行时会对animal对象做一些改变,使得animal对象在调用set方法的时候可能做了一些额外的操作,所以谜底还是在对象本身。
本质
首先我们对上述代码中添加监听的地方打断点,观察一下addObserver方法对animal对象做了什么处理?也就是说animal对象在经过addObserver方法之后发生了什么改变,我们通过打印isa指针如下所示
1 | // 添加监听之前的isa |
我们发现,animal对象执行过addObserver操作之后,animal实例的isa指针由之前的指向类对象Animal变为指向NSKVONotifyin_Animal类对象。也就是说一旦animal对象添加了KVO监听以后,其isa指针就会发生变化,因此类对象里面的的set方法的执行效果就不一样了。
NSKVONotifying_Animal
其实是Animal的子类,那么也就是说其super_class指针是指向Animal类对象的,NSKVONotifying_Animal
类是runtime在运行时动态创建的。
那么animal对象在调用setName方法的时候,会根据isa找到NSKVONotifying_Animal
类对象,在NSKVONotifying_Animal
中找setName的方法及实现。
NSKVONotifying_Animal
中的setName方法其实调用了Fundation框架中C语言函数_NSSetCharValueAndNotify
。
_NSSetCharValueAndNotify
函数内部做的操作相当于:
1,调用willChangeValueForKey将要改变方法,类似于[self willChangeValueForKey:],
2,调用父类的setName方法对成员变量赋值,类似于[super setName:]
3,最后调用didChangeValueForKey已经改变方法,类似于[self didChangeValueForKey:]
didChangeValueForKey中会调用监听者的监听方法,最终来到监听者的observeValueForKeyPath方法中。
NSKVONotifying_Animal
NSKVONotifying_Animal
作为Animal的子类,其super_class指针指向Animal类对象,并且NSKVONotifying_Animal内部一定对setName:方法做了单独的实现,那么NSKVONotifying_Animal同Animal类的差别可能就在于其存储的实例方法及方法实现不同。
通过runtime打印Animal类对象和NSKVONotifying_Animal类对象内存储的实例方法
1 | // 通过runtime打印类对象的方法名 |
log信息如下:
1 | Person类的方法 - name,setName:, |
通过上述代码我们发现NSKVONotifying_Animal
中有4个对象方法。分别为setName:、class、 dealloc、 _isKVOA,那么至此我们可以画出NSKVONotifying_Animal
的内存结构以及方法调用顺序。
这里NSKVONotifying_Animal
重写class方法是为了隐藏自己不被外界所看到。
我们在添加KVO监听之后,打印animal对象的class方法可以发现返回Animal。
如果NSKVONotifying_Animal
不重写class方法,那么当对象要调用class对象方法的时候就会一直向上找来到NSObject,而NSObject的class的实现大致为返回自己真实isa指向的类,就是animal的isa指向的类那么打印出来的类就是NSKVONotifying_Animal
。
但是官方不希望将NSKVONotifying_Animal
类暴露出来,并且不希望我们知道其内部实现,所以在内部重写了class类,直接返回Animal类,所以外界在调用animal的class对象方法时是Animal类。这样animal实例给外界的感觉还是Animal类,并不知道NSKVONotifying_Animal
子类的存在。
那么我们可以猜测NSKVONotifying_Animal
内重写的class方法内部实现大致为:
1 | - (Class) class { |
手动触发KVO
通过手动调用对象的didChangeValueForKey方法可以触发KVO。
1 | - (void)viewDidLoad { |
log信息如下
1 | { |
通过打印我们可以发现,didChangeValueForKey方法内部成功调用了observeValueForKeyPath:ofObject:change:context:
,并且name的值并没有发生改变。
总结
KVO原理:
当对象使注册了KVO监听,iOS系统会修改这个对象的isa指针,改为指向一个全新的通过Runtime动态创建的子类,子类拥有自己的setter方法实现,setter方法实现内部会调用Foundation的_NSSet*ValueAndNotify
方法。
_NSSet*ValueAndNotify
方法内部顺序调用willChangeValueForKey方法、原父类的setter方法实现、didChangeValueForKey方法,而didChangeValueForKey方法内部又会调用监听器的observeValueForKeyPath:ofObject:change:context:
监听方法。
手动触发KVO:
被监听的属性的值被修改时,就会自动触发KVO。如果想要手动触发KVO,则需要我们自己调用willChangeValueForKey和didChangeValueForKey方法即可在不改变属性值的情况下手动触发KVO,并且这两个方法缺一不可。
注意:KVO是重写了setter方法,直接修改成员变量不会触发setter方法,所以不会触发KVO。
KVC
KVC的使用
KVC(Key Value Coding),键值编码,用于通过key来读写属性。
常用API:
1 | - (void)setValue:(id)value forKeyPath:(NSString *)keyPath; |
KVC赋值的原理
KVC赋值的方式,是通过访问属性的setter方法和访问成员变量来实现的。
1,首先会调用属性的-setKey:
方法来赋值。
1 | - setKey: |
2,如果没有找到-setKey:
方法,那么就会寻找-_setKey:
方法来赋值。
1 | - _setKey: |
3,如果没有找到-_setKey:
方法,那么就会寻找-setIsKey:
方法,来询问是否可以访问成员变量来赋值。
1 | - setIsKey: |
4,如果没有找到-setIsKey:
方法,那么就会寻找accessInstanceVariablesDirectly
方法,来询问是否可以访问成员变量来赋值。
1 | + accessInstanceVariablesDirectly |
如果返回NO,那么会抛出异常,如果返回YES,则表示可以通过访问成员变量来赋值。
5,访问成员变量会按照_Key、_isKey、Key、isKey的顺序来逐个访问赋值,如果这4个成员变量都没有找到,就抛出异常。
KVC会触发KVO
1 | @interface Animal : NSObject { |
我们发现通过KVC改变成员变量照样可以触发KVO,但是我们自己访问成员变量就不会触发KVO,原因就是通过KVC访问成员变量的时候,系统会主动触发KVO,相当于:
1 | [animal willChangeValueForKey:@"name"]; |
KVC取值的原理
和赋值一样,也是按顺序访问方法和成员变量获得值。
1,通过getKey
方法取值。
1 | - getKey |
2,如果没有getKey
方法,通过key
方法取值。
1 | - key |
3,如果没有key
方法,通过isKey
方法取值。
1 | - isKey |
4,如果没有isKey
方法,通过_key
方法取值。
1 | - _key |
5,如果以上方法都没有找到,那么会通过accessInstanceVariablesDirectly
方法的返回值来确定是否可以访问成员变量取值。
1 | _key |