深入理解KVC、KVO
网上关于KVC和KVO的介绍一大片,基本用法这里就不介绍了,这篇主要聊聊KVC和KVO的实现,以及我们自己动手实现一套KVO。
KVC
KVC也就是key-value-coding,即键值编码,通常用来给某个对象的只读属性或者隐藏属性进行赋值,或者获取某个对象的因此属性的值。虽然我们通过runtime
也可以达到这样的目的,但KVC无疑是一种更便捷的方式。
例如Person.m
:
|
Person
类的.h
文件中并没有暴露这两个属性,但是通过KVC,我们就可以修改以及读取这两个属性:
|
输出如下:
|
在KVC中,无论调用- (void)setValue:(nullable id)value forKey:(NSString *)key
还是- (void)setValue:(nullable id)value forKey:(NSString *)key
方法,都是通过NSString
对象来指定被操作属性的。
注意:Foundation
框架会按照_<key>
,_is<Key>
,<key>
,is<Key>
的顺序查找成员变量。当然,KVO的操作首先是查找成员变量对应的getter/setter
方法。
对于上述例子的[p valueForKey:@"age"]
方法,底层的执行机制如下:
- 首先KVC调用方法的顺序为
getAge
->isAge
->age
- 如果上述三个方法都没有找到,则找成员变量,顺序为
_Age
->_isAge
->age
->isAge
- 如果上面都没有找到,那么系统会执行该对象的
- (void)setValue:(id)value forUndefinedKey:(NSString *)key
,如果该对象没有实现此方法,程序异常退出。
对于上述例子的[p setValue:@10 forKey:@"age"]
方法,底层的执行机制如下:
- 首先KVC调用
setAge
方法,然后调用setIsAge
方法 - 如果没有
setIsAge
方法,则找成员变量,顺序为_Age
->_isAge
->age
->isAge
- 如果还是没有,那么系统会执行该对象的
- (id)valueForUndefinedKey:(NSString *)key
方法,如果该对象没有实现此方法,程序异常退出。
另外,注意一点,当我们将上述setValue
代码修改一下,如:[p setValue:[[Person alloc] init] forKey:@"age"]
,程序直接崩溃了,异常日志为:-[Person longLongValue]: unrecognized selector sent to instance 0x6100000330c0
,说明,KVC在赋值的时候,是会根据基本数据类型的具体类型,来调用Value
相应的intValue
、doubleValue
方法。因为NSNumber
和NSString
都实现了这些方法,所以程序能正常运行。
还有一点,假如setValue
的value
为nil
,程序则会报Terminating app due to uncaught exception 'NSInvalidArgumentException'
异常,虽然苹果对nil
做了很好的处理(这里要吐槽一下java的判空了),但是基本数据类型不能接受nil
,因此,系统会自动调用setNilValueForKey
方法,我们可以在这里做些处理。
KVC除了操作对象之外,还可以操作对象的复合属性,例如,在Person
类中,又有一个Person
属性。通过- (void)setValue:(id)value forKeyPath:(NSString *)keyPath
和- (id)valueForKeyPath:(NSString *)keyPath
来操作复合属性。
|
通过KVC,我们可以很容易的操作成员变量,最常用的就是对私有变量赋值以及字典转模型。
KVO
KVO即key-value-observing,通常情况下,我们会利用KVO来监听某个对象的属性变化。
例如:
新建模型RGBColor
,有以下四个属性:
|
Foundation
框架提供的表示属性依赖的机制如下:
+ (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key
更详细的如下:
+ (NSSet<NSString *> *)keyPathsForValuesAffecting<键名>
注意,第一个方法的优先级是大于第二个的。在我们的例子中如下:
|
具体使用如下:
|
手动通知 VS 自动通知
你会发现刚才的例子有点神奇,为什么修改r
,g
,b
的值会触发observeValueForKeyPath
,明明观察的是color
。但是实际上发送的事情是,当RGBColor
实例的-setR:
等方法被调用的时候以下方法:
|
和:
|
会在运行-setR:
中的代码之前以及之后被自动调用。但有些情况,我们希望关闭键值改变的通知或者达到某些条件再通知,我们需要做以下事情:
|
方法+ (BOOL)automaticallyNotifiesObserversOf<Key>
会告诉系统是否需要关闭自动通知。这里我们进行了收到调用。
你可能会疑惑,自动通知的情况下,系统是如何调用-willChangeValueForKey:
和-didChangeValueForKey:
的。
基本实现原理
当观察某对象时,KVO机制动态创建当前类的子类,即NSKVONotifying_<原类>
,并为这个新类重写被观察属性key
的setter
方法。之后修改当前类的isa
指向,所以,我们从应用层上看来,当前类是没有改变的。调用流程如下:
既然知道了KVO的实现原理,那么我们就可以自己来实现了。我们这里实现一个可以传Block
的KVO。
首先,定义NSObject
的分类:
|
这里定义的block
比较简单。再来看m文件的实现
|
当然,这里只是简单的实现,具体还有很多的判断逻辑。