概述
KVO全称KeyValueObserving
,是苹果提供的一套事件通知机制。允许对象监听另一个对象特定属性的改变,并在改变时接收到事件。由于KVO的实现机制,所以对属性才会发生作用,一般继承自NSObject的对象都默认支持KVO。
KVO和NSNotificationCenter都是iOS中观察者模式
的一种实现。区别在于,相对于被观察者和观察者之间的关系,KVO是一对一
的,而不一对多的。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;
}
image.png
基础使用
使用KVO分为三个步骤:
1、通过addObserver:forKeyPath:options:context:
方法注册观察者,观察者可以接收keyPath
属性的变化事件。
2、在观察者中实现observeValueForKeyPath:ofObject:change:context:
方法,当keyPath
属性发生改变后,KVO会回调这个方法来通知观察者。
3、当观察者不需要监听时,可以调用removeObserver:forKeyPath:
方法将KVO移除。需要注意的是,调用removeObserver
需要在观察者消失之前,否则会导致Crash。
注册方法
在注册观察者时,可以传入options
参数,参数是一个枚举类型。如果传入NSKeyValueObservingOptionNew
和NSKeyValueObservingOptionOld
表示接收新值和旧值,默认为只接收新值。如果想在注册观察者后,立即接收一次回调,则可以加入NSKeyValueObservingOptionInitial
枚举。
还可以通过方法context
传入任意类型的对象,在接收消息回调的代码中可以接收到这个对象,是KVO中的一种传值方式。
在调用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
参考资料
网友评论