Key Value Observing (KVO) - 允许将其他对象的指定属性变更通知给对象
参考链接
一、At a Glance
KVO 主要用于 Model
和 Controller
之间的通信
1. View
通过 Controller
观察 Model
的属性进行改变
2. Model
也可以观察其他 Model
,甚至可以观察 Model
本身(通常用于确认从属值何时改变)
>> Example
image.png-
假设
Person
实例与Account
实例进行交互,表示该人在银行的储蓄账户。Person
实例可能需要知道Account
实例的某些方面何时发生改变 -
Person
可以定期轮询Account
的属性blance
或interestRate
,但是这样效率低下,所以衍生出 KVO,能够在Account
发生改变时收到通知并作出响应
二、Registering for Key-Value Observing
- 将观察者注册到观察对象
addObserver:forKeyPath:options:context:
-
observeValueForKeyPath:ofObject:change:context:
在观察者内部实现该方法以接收更改通知消息 - 当观察者不再接收消息,应该使用该方法注销观察者
removeObserver:forKeyPath:
,并在观察者释放之前调用该方法
并非所有的类都支持 KVO
1. Registering as an Observer
addObserver:forKeyPath:options:context:
注册成为观察者
不强引用观察者、被观察者和上下文
Options | Description |
---|---|
NSKeyValueObservingOptionOld |
指定 change dictionary 包含变更后的旧值 |
NSKeyValueObservingOptionNew |
指定 change dictionary 包含变更后的新值 |
NSKeyValueObservingOptionInitial |
在注册方法 return 之前发送一个通知哦 |
NSKeyValueObservingOptionPrior |
注册后发送一个预改变通知,change dictionary 中包括 NSKeyValueChangeNotificationIsPriorKey : @(YES)
|
context:
可以为每个观察到的键路径创建一个上下文,区分父类与该类观察相同键路径的情况
/// Creating context pointers
static void *PersonAccountBalanceContext = &PersonAccountBalanceContext;
static void *PersonAccountInterestRateContext = &PersonAccountInterestRateContext;
- (void)registerAsObserverForAccount:(Account*)account {
[account addObserver:self
forKeyPath:@"balance"
options:(NSKeyValueObservingOptionNew |
NSKeyValueObservingOptionOld)
context:PersonAccountBalanceContext];
[account addObserver:self
forKeyPath:@"interestRate"
options:(NSKeyValueObservingOptionNew |
NSKeyValueObservingOptionOld)
context:PersonAccountInterestRateContext];
}
2. Receiving Notification of a Change
Change Dictionary | Description |
---|---|
NSKeyValueChangeKindKey |
NSKeyValueChangeSetting 如果观察到对象的值已更改 |
NSKeyValueChangeNewKey |
提供更改之后的新值 |
NSKeyValueChangeOldKey |
提供更改之前的旧值 |
NSKeyValueChangeIndexesKey |
对应是一个 NSIndexSet 对象, 如果 NSKeyValueChangeKindKey 对应的键是 NSKeyValueChangeInsertion , NSKeyValueChangeRemoval , or NSKeyValueChangeReplacement ,那么该值为插入、移除、替换的对象 |
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context {
if (context == PersonAccountBalanceContext) {
// Do something with the balance…
// if ([keyPath isEqualToString: @"keyPath"])
/// 如果通过 NULL 指定上下文,则通过通知的 keypath 和 正在观察的 keypath 进行比较;
} 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];
}
}
3. Removing an Object as an Observer
- (void)unregisterAsObserverForAccount:(Account *)account {
[account removeObserver:self
forKeyPath:@"balance"
context:PersonAccountBalanceContext];
[account removeObserver:self
forKeyPath:@"interestRate"
context:PersonAccountInterestRateContext];
}
在移除观察者的时候需要注意以下几点
- 如果在观察者移除时之前没有注册过,将会抛出异常
请确保removeObserver:forKeyPath:context:
和addObserver:forKeyPath:options:context:
是成对调用的;如果不能保证需要放在 try/catch 中处理可能存在的异常 - 观察者不会在 deallocated 时自动移除自己,如果在观察者已被释放的情况下,被观察者发送变更通知,相当于发送到一个 released 对象,触发内存访问异常
- 该协议无法查询对象是观察者和非观察者;经典做法是在观察者初始化期间(例如 init 或者 viewDidLoad)注册成为观察者,在释放过程中(dealloc)注销(注意 1: 1 配对添加和移除消息)
三、Registering Dependent Keys
1. To-One Relationships
一个属性的值取决于另一个对象中一个或多个其他属性的值
keyPathsForValuesAffectingValueForKey:
- (NSString *)fullName {
return [NSString stringWithFormat:@"%@ %@",firstName, lastName];
}
当 firstName
或 lastName
发生改变时,需要通知观察 fullName
属性
/// Example 1
+ (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key {
NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
if ([key isEqualToString:@"fullName"]) {
NSArray *affectingKeys = @[@"lastName", @"firstName"];
keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys];
}
return keyPaths;
}
/// Example 2
+ (NSSet *)keyPathsForValuesAffectingFullName {
return [NSSet setWithObjects:@"lastName", @"firstName", nil];
}
2. To-Many Relationships
假如有一个对象 Department
,他有很多 employees
;然后每一个 employee
都具有 salary
属性
我们需要通过所有 employee.salary
计算出 totalSalary
一对多(一个父项对应多个子项)
- (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:@"employees.@sum.salary"]];
}
- (void)setTotalSalary:(NSNumber *)newTotalSalary {
if (totalSalary != newTotalSalary) {
[self willChangeValueForKey:@"totalSalary"];
_totalSalary = newTotalSalary;
[self didChangeValueForKey:@"totalSalary"];
}
}
- (NSNumber *)totalSalary {
return _totalSalary;
}
如果使用的是 Core Data,可以在通知中心将父项注册为 Managed object 上下文的观察者,响应子项发布的相关变更通知
四、Key-Value Observing Implementation Details
- 使用 isa-swizzling 技术;在对对象的属性注册观察者时,将修改观察对象的 isa 指针,指向一个中间类而不是真实的类,isa 指针的值不一定反映实例的实际类型
- isa 指向对象的类,实际上是一个调度表,包含指向该类实现的方法的指针及其他数据
- 不要依靠 isa 指针来确定类成员,相反应该使用 class 方法来确定对象实例的类
网友评论