使用KVO的前提条件
- 该类必须满足KVC命名约定查看此处
- 该类可以触发属性变更的KVO通知
继承自NSObject的类默认由NSObject实现该功能
- 依赖的属性被正确的注册到KVO。如:
fullName
依赖lastName
和firstName
使用方式
- 添加观察者:
addObserver:forKeyPath:options:context:
,如:需要观察Account
对象的balance
属性
static void *PersonAccountBalanceContext = &PersonAccountBalanceContext;
[account addObserver:self
forKeyPath:@"balance"
options:(NSKeyValueObservingOptionNew |
NSKeyValueObservingOptionOld)
context:PersonAccountBalanceContext];
- 观察回调方法处理:
observeValueForKeyPath:ofObject:change:context:
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context {
if (context == PersonAccountBalanceContext && [keyPath isEqualToString:@"balance"]) {
NSLog(@"Do something with the balance…");
} else {
// Any unrecognized context must belong to super
[super observeValueForKeyPath:keyPath
ofObject:object
change:change
context:context];
}
}
- 移除观察者:
removeObserver:forKeyPath:context:
[account removeObserver:self
forKeyPath:@"balance"
context:PersonAccountBalanceContext];
以上是我们常见的使用KVO的方式,在具体实践中还有许多坑要踩,接下来我们逐个探讨。
进阶使用
截止目前我们使用KVO的方式都是依赖系统实现的自动触发机制,在有些情况下我们需要更精确的控制KVO的触发时机,此时需要手动触发KVO。
- 手动触发KVO
手动触发KVO需要覆写NSObject
的automaticallyNotifiesObserversForKey:
方法。该方法默认返回YES,调用自动触发KVO的逻辑。对需要手动触发KVO的属性需要变更该方法的返回值为NO。
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
BOOL automatic = NO;
// 手动触发balance属性KVO
if ([key isEqualToString:@"balance"]) {
automatic = NO;
} else {
// 其它属性KVO自动触发
automatic = [super automaticallyNotifiesObserversForKey:key];
}
return automatic;
}
实现手动触发KVO通知还需要在属性值变更前调用willChangeValueForKey:
,变更后调用didChangeValueForKey:
- (void)setBalance:(double)theBalance {
if (theBalance != _balance) {
[self willChangeValueForKey:@"balance"];
_balance = theBalance;
[self didChangeValueForKey:@"balance"];
}
}
从OS X 10.5开始automaticallyNotifiesObserversForKey:
会在被观察的类中查找+automaticallyNotifiesObserversOf<Key>
方法,其中<key>表示被观察类的属性。以balance
属性为例,实现automaticallyNotifiesObserversOfBalance
即可,此时不需要再覆写automaticallyNotifiesObserversForKey:
。
+ (BOOL)automaticallyNotifiesObserversOfBalance {
return NO;
}
- 依赖属性KVO
在许多情况下一个属性需要依赖其它属性值,例如:fullName
是由firstName
和lastName
组成。
- (NSString *)fullName {
return [NSString stringWithFormat:@"%@ %@", _firstName, _lastName];
}
假设我们需要对fullName
做观察,当firstName
、lastName
有变化时自动更新fullName
的值并触发KVO通知。
实现依赖属性KVO有以下两种方式。
- 实现
keyPathsForValuesAffectingValueForKey:
方法
+ (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key {
NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
if ([key isEqualToString:@"fullName"]) {
NSArray *affectingKeys = @[@"lastName", @"firstName"];
keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys];
}
return keyPaths;
}
- 实现
keyPathsForValuesAffecting<Key>
方法,其中<key>表示被观察对象的属性
+ (NSSet *)keyPathsForValuesAffectingFullName {
return [NSSet setWithObjects:@"firstName",@"lastName", nil];
}
- 注意事项:在
category
中依赖属性观察不能覆写keyPathsForValuesAffectingValueForKey:
方法,因为category
中不能覆写方法。在category
中可以通过实现keyPathsForValuesAffecting<Key>
方法实现依赖属性观察。
注意事项
- keypath使用方式优化
截止目前我们都是以字符串的方式使用keypath,这种方式使得编译器在编译期间不能及时的发现错误,一种比较好的方式是通过NSStringFromSelector
和@selector
结合的方式来使用keypath,如下所示。
[self addObserver:self
forKeyPath:NSStringFromSelector(@selector(fullName))
options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld
context:FullNameContext];
- Context使用
在使用KVO时有一个关键点就是在添加观察者时传入一个唯一的context,如下所示。
// 观察fullName的context
static void *FullNameContext = &FullNameContext;
...
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context {
if (context == FullNameContext && [keyPath isEqualToString:NSStringFromSelector(@selector(fullName))]) {
// do somthing with fullName
} else {
// Any unrecognized context must belong to super
[super observeValueForKeyPath:keyPath
ofObject:object
change:change
context:context];
}
}
这样会保证我们观察的子类都是正确的,子类和父类都能安全的观察同样的键值而不会冲突。否则我们将会碰到难以 debug 的奇怪行为。
安全的移除观察者
当一个观察者完成了监听对象的改变使命,需要调用–removeObserver:forKeyPath:context:
来移除观察,否则会出现程序崩溃的情况。
当重复移除观察者时同样会导致程序崩溃。
目前还没有公开的API来检测一个对象是否被注册为观察者。
要安全的移除观察者可以通过以下两种方式来实现。
- try/catch
- (void)dealloc {
@try {
[self removeObserver:self
forKeyPath:NSStringFromSelector(@selector(fullName))
context:FullNameContext];
} @catch (NSException * exception) {
}
}
- 遍历observationInfo
通过查阅API我们发现NSObject有一个observationInfo
属性,官方文档对该属性的描述如下。
Returns a pointer that identifies information about all of the observers that are registered with the observed object.
基于此可以通过KVC的方式获取到对象是否注册相关keypath的观察者
observationInfo结构// 按key检索
- (BOOL)observerKeyPath:(NSString *)key observer:(id)observer
{
id info = self.observationInfo;
NSArray *array = [info valueForKey:@"_observances"];
for (id objc in array) {
id Properties = [objc valueForKeyPath:@"_property"];
id newObserver = [objc valueForKeyPath:@"_observer"];
NSString *keyPath = [Properties valueForKeyPath:@"_keyPath"];
if ([key isEqualToString:keyPath] && (newObserver == observer)) {
return YES;
}
}
return NO;
}
实现原理
KVO的实现官方文档中提到使用了isa-swizzling
技术,实现思路如下
当你观察一个对象时,一个新的类会被动态创建。这个类继承自该对象的原本的类,并重写了被观察属性的 setter 方法。重写的 setter 方法会负责在调用原 setter 方法之前和之后,通知所有观察对象:值的更改。最后通过 isa 混写(isa-swizzling) 把这个对象的 isa 指针 ( isa 指针告诉 Runtime 系统这个对象的类是什么 ) 指向这个新创建的子类,对象就神奇的变成了新创建的子类的实例。
并未透漏更多细节内容。
关于KVO实现细节的探究可以参考KVO实现原理。
优雅的使用KVO
关于KVO被吐槽最多的就是其晦涩的API和使用方式,如何解决这个问题呢,可以使用Facebook开源的KVOController,使用方式如下
// create KVO controller with observer
FBKVOController *KVOController = [FBKVOController controllerWithObserver:self];
self.KVOController = KVOController;
// observe clock date property
[self.KVOController observe:clock
keyPath:@"date"
options:NSKeyValueObservingOptionInitial|NSKeyValueObservingOptionNew
block:^(ClockView *clockView, Clock *clock, NSDictionary *change) {
// update clock view with new value
clockView.date = change[NSKeyValueChangeNewKey];
}];
使用KVOContoller解决了以下问题。
- 不需要再手动移除观察者
- 使用
Block
方式降低接口使用复杂度 - 不再需要
if
判断keypath
值
网友评论