KVO原理
概述
KVO全称KeyValueObserving
,是苹果提供的一套事件通知机制。允许对象监听另一个对象特定属性的改变,并在改变时接收到事件。由于KVO的实现机制,所以对属性才会发生作用,一般继承自NSObject的对象都默认支持KVO。
KVO可以监听单个属性的变化,也可以监听集合对象的变化。通过KVC的mutableArrayValueForKey:
等方法获得代理对象,当代理对象的内部对象发生改变时,会回调KVO监听的方法。集合对象包含NSArray
和NSSet
。
当同时观察多个对象时,不但对象本身发生改变时会告知观察者,而且被观察对象发生替换、删除或插入等操作时也会告知观察者。
KVO 的原理
1: 动态生成子类 : NSKVONotifying_xxx
2: 观察的是 setter
3: 动态子类重写了很多方法 setNickName (setter) class dealloc _isKVOA
4: 移除观察的时候 isa 指向回来
5: 动态子类不会销毁
iOS用什么方式实现对一个对象的KVO?(KVO的本质是什么?)
当一个对象使用了KVO监听,iOS系统会修改这个对象的isa
指针,改为指向一个全新的通过Runtime动态创建的子类,
子类拥有自己的set方法实现,set方法
实现内部会顺序
调用willChangeValueForKey
方法、原来的setter
方法实现、didChangeValueForKey
方法,而didChangeValueForKey
方法内部
又会调用监听器的observeValueForKeyPath:ofObject:change:context:
监听方法。
如何手动触发KVO
被监听的属性的值被修改时,就会自动触发KVO。
如果想要手动触发KVO,则需要我们自己调用willChangeValueForKey
和didChangeValueForKey
方法即可在不改变属性值的情况下手动触发KVO
,并且这两个方法缺一不可。
在viewController
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
self.p1.age = 10;
[self.p1 willChangeValueForKey:@"nickName"];
self.p1.nickName = @"nickName1";
[self.p1 didChangeValueForKey:@"nickName"];
}
或者在model
- (void)setAge:(int)age {
if (age != _age) {
[self willChangeValueForKey:@"age"];
_age = age;
[self didChangeValueForKey:@"age"];
}
}
如果想控制当前对象的自动调用过程,也就是由上面两个方法发起的KVO调用,则可以重写下面方法。方法返回YES则表示可以调用,如果返回NO则表示不可以调用。
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)theKey {
BOOL automatic = NO;
return automatic;
}
基础使用
使用KVO分为三个步骤:
1、通过addObserver:forKeyPath:options:context:
方法注册观察者,观察者可以接收keyPath
属性的变化事件。
[self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:NULL];
2、在观察者中实现observeValueForKeyPath:ofObject:change:context:
方法,当keyPath
属性发生改变后,KVO会回调这个方法来通知观察者。
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
if ([keyPath isEqualToString:@"name"]) {
NSLog(@"%@",change);
}
}
3、当观察者不需要监听时,可以调用removeObserver:forKeyPath:
方法将KVO移除。需要注意的是,调用removeObserver
需要在观察者消失之前,否则会导致Crash。
[self.person removeObserver:self forKeyPath:@"nick" context:NULL];
注册方法
在注册观察者时,可以传入options
参数,参数是一个枚举类型。如果传入NSKeyValueObservingOptionNew
和NSKeyValueObservingOptionOld
表示接收新值和旧值,默认为只接收新值。如果想在注册观察者后,立即接收一次回调,则可以加入NSKeyValueObservingOptionInitial
枚举。
context使用
addObserver:forKeyPath:options:context:
方法中的上下文context
指针包含任意数据,这些数据将在相应的更改通知中传递回观察者。可以通过指定context
为NULL
,从而依靠keyPath
即键路径字符串传来确定更改通知的来源,但是这种方法可能会导致对象的父类由于不同的原因也观察到相同的键路径而导致问题。所以可以为每个观察到的keyPath
创建一个不同的context
,从而完全不需要进行字符串比较,从而可以更有效地进行通知解析
通俗的讲,context
上下文主要是用于区分不同对象的同名属性,从而在KVO回调方法中可以直接使用context
进行区分,可以大大提升性能,以及代码的可读性
context使用总结
- 不使用context,使用keyPath区分通知来源
//context的类型是 nullable void *,应该是NULL,而不是nil
[self.person addObserver:self forKeyPath:@"nick" options:NSKeyValueObservingOptionNew context:NULL];
- 使用context区分通知来源
//定义context
static void *PersonNickContext = &PersonNickContext;
static void *PersonNameContext = &PersonNameContext;
//注册观察者
[self.person addObserver:self forKeyPath:@"nick" options:NSKeyValueObservingOptionNew context:PersonNickContext];
[self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:PersonNameContext];
//KVO回调
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
if (context == PersonNickContext) {
NSLog(@"%@",change);
}else if (context == PersonNameContext){
NSLog(@"%@",change);
}
}
在调用addObserver
方法后,KVO并不会对观察者进行强引用,所以需要注意观察者的生命周期,否则会导致观察者被释放带来的Crash
。
监听方法
观察者需要实现observeValueForKeyPath:ofObject:change:context:
方法,当KVO事件到来时会调用这个方法,如果没有实现会导致Crash
。change
字典中存放KVO属性相关的值,根据options
时传入的枚举来返回。枚举会对应相应key
来从字典中取出值,例如有NSKeyValueChangeOldKey
字段,存储改变之前的旧值。
change
中还有NSKeyValueChangeKindKey
字段,和NSKeyValueChangeOldKey
是平级的关系,来提供本次更改的信息,对应NSKeyValueChange
枚举类型的value
。例如被观察属性发生改变时,字段为NSKeyValueChangeSetting
。
如果被观察对象是集合对象,在NSKeyValueChangeKindKey
字段中会包含NSKeyValueChangeInsertion
、NSKeyValueChangeRemoval
、NSKeyValueChangeReplacement
的信息,表示集合对象的操作方式。
缺点
苹果提供的KVO自身存在很多问题,首要问题在于,KVO如果使用不当很容易崩溃。例如重复
add和remove导致的Crash
,Observer被释放
导致的崩溃,keyPath传错
导致的崩溃等。
在调用KVO时需要传入一个keyPath,由于keyPath是字符串的形式,所以其对应的属性发生改变后,字符串没有改变容易导致Crash。我们可以利用系统的反射机制将keyPath反射出来,这样编译器可以在@selector()中进行合法性检查。
NSStringFromSelector(@selector(isFinished))
KVO是一种事件绑定机制的实现,在keyPath对应的值发生改变后会回调对应的方法。这种数据绑定机制,在对象关系很复杂的情况下,很容易导致不好排查的bug。例如keyPath对应的属性被调用的关系很复杂,就不太建议对这个属性进行KVO,可以想一下RAC的信号脑补一下。
demo
参考资料
作者:一个半吊子工程师
原文链接:https://www.jianshu.com/p/27b8b9faf7c1