概述
KVO
全称KeyValueObserving
,翻译成键值观察,是苹果提供的一套事件通知机制。允许对象监听另一个对象特定属性的改变,并在改变时接收到事件。由于KVO
的实现机制,所以对属性才会发生作用,一般继承自NSObject
的对象都默认支持KVO
。
KVO
和NSNotificationCenter
都是iOS中观察者模式的一种实现。区别在于,相对于被观察者和观察者之间的关系,KVO
是一对一的,而不一对多的。KVO
对被监听对象无侵入性,不需要修改其内部代码即可实现监听。
KVO
可以监听单个属性的变化,也可以监听集合对象的变化。通过KVC
的mutableArrayValueForKey:
等方法获得代理对象,当代理对象的内部对象发生改变时,会回调KVO
监听的方法。集合对象包含NSArray
和NSSet
。
基础使用
使用KVO分为三个步骤:
- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context;
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context
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
实现最为合适。通过KVO在Model和Controller之间进行通信。
注意点
-
KVO
的addObserver
和removeObserver
需要是成对的,如果重复remove
则会导致NSRangeException
类型的Crash
,如果忘记remove
则会在观察者释放后再次接收到KVO回调时Crash
。 - 苹果官方推荐的方式是,在init的时候进行
addObserver
,在dealloc
时removeObserver
,这样可以保证add
和remove
是成对出现的,是一种比较理想的使用方式。
手动调用KVO
- (void)willChangeValueForKey:(NSString *)key
- (void)didChangeValueForKey:(NSString *)key
可能有时候,我们要实现手动的KVO,或者我们实现的类库不希望被KVO。
这时候需要关闭自动生成KVO通知,然后手动的调用,手动通知的好处就是,可以灵活加上自己想要的判断条件。下面看个例子如下:
- (void)setBalance:(double)theBalance {
if (theBalance != _balance) {
[self willChangeValueForKey:@"balance"];
_balance = theBalance;
[self didChangeValueForKey:@"balance"];
}
}
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)theKey {
BOOL automatic = NO;
if ([theKey isEqualToString:@"balance"]) {
automatic = NO;
}
else {
automatic = [super automaticallyNotifiesObserversForKey:theKey];
}
return automatic;
}
如果想控制当前对象的自动调用过程,也就是由上面两个方法发起的KVO
调用,则可以重写automaticallyNotifiesObserversForKey:
方法。方法返回YES则表示可以调用,如果返回NO则表示不可以调用。
KVO实现原理
KVO
是通过isa-swizzling
技术实现的(这句话是整个KVO
实现的重点)。在运行时根据原类创建一个中间类,这个中间类是原类的子类,并动态修改当前对象的isa
指向中间类。并且将class
方法重写,返回原类的Class
。所以苹果建议在开发中不应该依赖isa
指针,而是通过class
实例方法来获取对象类型。
即当一个类型为 ObjectA 的对象,被添加了观察后,系统会生成一个 NSKVONotifying_ObjectA 类,并将对象的isa指针指向新的类,也就是说这个对象的类型发生了变化。这个类相比较于ObjectA,会重写以下几个方法。
- 1.重写setter。因为 KVO 的原理是修改 setter 方法,因此使用 KVO 必须调用 setter 。若直接访问属性对象则没有效果。
- 2.重写class。当修改了isa指向后,class的返回值不会变,但isa的值则发生改变。
- 3.重写dealloc。系统重写 dealloc 方法来释放资源。
- 4.重写_isKVOA。 这个私有方法是用来标示该类是一个 KVO 机制声称的类。
网友评论