KVO 简介
KVO 键值观察机制,就是观察指定对象的指定属性变化情况。
KVO 键值观察 依赖于 KVC 健值编码
Key-value observing 通常用于 MVC 中,model 与 controller直接的通讯。
继承于 NSObject 才可以拥有 KVO 机制
两种 KVO 应用方式
- 自动 KVO
- 手动 KVO
举个例子,一个 Person
类, 一个 Account
类
- [图1]
当 Account
的属性 余额 balance
改变时通知 Person
给 Account
添加观察者 Person
[account addObserver:person forKeyPath:@"balance" options:options context:context];
- [图2]
Person
观察 Account
的属性变化
Person
中实现观察者方法 observeValueForKeyPath:ofObject:change:context:
- [图3]
Account
移除观察者 Person
[account removeObserver:person forKeyPath:@"balance" context:context]
- [图4]
这篇文章的重要内容
Registering for Key-Value Observing 注册键值观察的过程
Registering Dependent Keys 对于存在 key 依赖的键值观察
Key-Value Observing Implementation Details 键值观察的实现细节
1. Registering for Key-Value Observing 注册键值观察的过程
KVO 生命周期的过程,必须完成下面这三个方法
- 给被观察者注册观察者
addObserver:forKeyPath:options:context:.
- 在观察者里实现方法 接受通知
observeValueForKeyPath:ofObject:change:context:
- 移除观察者
removeObserver:forKeyPath:
要在观察者的内存销毁之前 移除观察者机制
- 注册观察者 addObserver:forKeyPath:options:context:.
-
Options
Options 影响方法observeValueForKeyPath:ofObject:change:context:
中的change
字典-
NSKeyValueObservingOptionOld
:change
包含 old value -
NSKeyValueObservingOptionNew
: 'change' 包含 new value -
NSKeyValueObservingOptionOld|NSKeyValueObservingOptionNew
: 即包含 old 也包含 new -
NSKeyValueObservingOptionInitial
:change
中不包含 key 的值,会在 kvo 注册的时候立即发送通知。 -
NSKeyValueObservingOptionInitial|NSKeyValueObservingOptionNew
: 注册kvo时立即发送通知change
中有 new 值,这里的 new 值是注册之前 key 的值。 -
NSKeyValueObservingOptionPrior
: 会在值发生改变前发出一次通知,当然改变后的通知依旧还会发出,也就是每次change都会有两个通知。值变化之前发送通知的change
中包含一个键值对NSKeyValueChangeNotificationIsPriorKey:@(1)
, 值发生变化之后的的通知change
不包含上面提到的 键值对, 可以跟willChange
手动通知搭配使用
- 具体解释查看文章: KVO Options 详细介绍
-
-
Context
是一个 void * 指针,可以传任意数据进去,可以防止父类跟子类同时对同一个 key 注册观察者造成的异常。可以采用下面的方法创建一个 Context
static void *PersonAccountBalanceContext = &PersonAccountBalanceContext; static void *PersonAccountInterestRateContext = &PersonAccountInterestRateContext;
命名规范:
static void * 类名+属性名+Context
- Receiving Notification of a Change 接收通知响应
所有的观察者必须实现方法 `observeValueForKeyPath:ofObject:change:context: message.`
-
keyPath
: 注册 KVO 时的keyPath
-
object
: 被观察者对象 -
change
: 根据注册 KVO 时 option 不同而展示不同的内容,change
中可能会包含keyPath
变化前后的值,对应的键有NSKeyValueChangeOldKey
NSKeyValueChangeNewKey
keyPath
是个标量或者 c 语言结构体,会把这些包装成NSNumber
或NSValue
,
当
keyPath
是个容器,change
中可能会包含这个容器inserted
,removed
,replaced
的情况 对应的键有
- `NSKeyValueChangeInsertion`
- `NSKeyValueChangeRemoval`
- `NSKeyValueChangeReplacement`
keypath
是个NSIndexSet
:change
中可能会包含数组
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context {
if (context == PersonAccountBalanceContext) {
// Do something with the balance…
} else if (context == PersonAccountInterestRateContext) {
// Do something with the interest rate…
} else {
// Any unrecognized context must belong to super
[super observeValueForKeyPath:keyPath
ofObject:object
change:change
context:context];
}
}
- Removing an Object as an Observer 移除观察者
观察者被移除之后就不会再接受到通知。
- (void)unregisterAsObserverForAccount:(Account*)account {
[account removeObserver:self
forKeyPath:@"balance"
context:PersonAccountBalanceContext];
[account removeObserver:self
forKeyPath:@"interestRate"
context:PersonAccountInterestRateContext];
}
需要注意的地方
- 不要重复移除观察者,会造成异常,如果不知道这个观察者是否已经被移除,可以在
try/catch
安全移除观察者 - 在观察者内存销毁之前从观察者中释放出来,不然会造成内存异常
- 无法检测一个对象是否处于观察者模式中,
-
init
或者viewDidLoad
中添加观察者 -
dealloc
中移除观察者
-
Automatic Change Notification 自动 KVO 通知
NSObject
提供了自动健值更改通知的实现,自动 KVO 通知依赖于 KVC 编码机制获取, KVC method,和 集合代理(collection proxy)mutableArrayValueForKey
:
手动 KVO
当你想要控制整个 KVO 的进程可以采用手动 KVO, 比如减少不必要的通知,比如将大量的通知控制到一个通知中。
手动 KVO 跟 自动 KVO 可以共存,比如同一个对象的同一个属性,可以在父类里自动 KVO, 在子类里手动 KVO, 重写方法 automaticallyNotifiesObserversForKey
即可
-
打开手动 KVO 的开关
automaticallyNotifiesObserversForKey
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)theKey { BOOL automatic = NO; if ([theKey isEqualToString:@"balance"]) { automatic = NO; } else { automatic = [super automaticallyNotifiesObserversForKey:theKey]; } return automatic; }
- 手动触发 KVO(一般是在 set 方法里)
willChangeValueForKey
didChangeValueForKey
- (void)setBalance:(double)theBalance { if (theBalance != _balance) { [self willChangeValueForKey:@"balance"]; _balance = theBalance; [self didChangeValueForKey:@"balance"]; } }
- 也可以把多个触发 KVO 的 key 放到一起
- (void)setBalance:(double)theBalance { [self willChangeValueForKey:@"balance"]; [self willChangeValueForKey:@"itemChanged"]; _balance = theBalance; _itemChanged = _itemChanged+1; [self didChangeValueForKey:@"itemChanged"]; [self didChangeValueForKey:@"balance"]; }
- 容器内部更改时,采用采用手动 KVO(当容器内容修改时,会触发 KVO)
注意:根据容器变化的类型去设置对应的NSKeyValueChange
- NSKeyValueChangeInsertion
- NSKeyValueChangeRemoval
- NSKeyValueChangeReplacement
针对容器对象的 KVO ,需要借用 KVC 机制创建的 容器代理。
- (void)removeTransactionsAtIndexes:(NSIndexSet *)indexes { [self willChange:NSKeyValueChangeRemoval valuesAtIndexes:indexes forKey:@"transactions"]; // Remove the transaction objects at the specified indexes. [self didChange:NSKeyValueChangeRemoval valuesAtIndexes:indexes forKey:@"transactions"]; }
- 手动触发 KVO(一般是在 set 方法里)
2.KVO - 注册依赖 key
单个关系
重写下面两个方法中的一个即可
keyPathsForValuesAffectingValueForKey
-
keyPathsForValuesAffecting
用例如下+ (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key { NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key]; if ([key isEqualToString:@"fullName"]) { NSArray *affectingKeys = @[@"lastName", @"firstName"]; keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys]; } return keyPaths; }
+ (NSSet *)keyPathsForValuesAffectingFullName { return [NSSet setWithObjects:@"lastName", @"firstName", nil]; }
多个关系
比如,有个部门类 Department
,有个员工类 Employees
, 每个员工的薪水都会影响这个部门的总薪水。
可以在 Department
添加、删除 Employees
的时候,给 Employees
注册、移除观察者 Employees
用例如下
```
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
if (context == totalSalaryContext) {
[self updateTotalSalary];
}
else
// deal with other observations and/or invoke super...
}
- (void)updateTotalSalary {
[self setTotalSalary:[self valueForKeyPath:@"[email protected]"]];
}
- (void)setTotalSalary:(NSNumber *)newTotalSalary {
if (totalSalary != newTotalSalary) {
[self willChangeValueForKey:@"totalSalary"];
_totalSalary = newTotalSalary;
[self didChangeValueForKey:@"totalSalary"];
}
}
- (NSNumber *)totalSalary {
return _totalSalary;
}
```
3. KVO 内部实现细节
使用 isa-swizzling
原理
当 对象的属性注册到观察者中时,会创建一个中间类,重写了被观察属性的 setter 方法。
- KVO-demo
- KVO官方文档 Introduction to Key-Value Observing Programming Guide - KVO官方文档