官方文档点这里:
Key-Value Observing Programming Guide、NSKeyValueObserving,
Key-Value Coding Programming Guide、NSKeyValueCoding。
Introduction
Introduction to KVO Programming Guide
Key-value observing 是一个机制,允许在别的对象属性发生变化时,收到通知。
为了理解 kvo,要先了解 kvc。
At a Glance
kvo 对 Model 层和 controller 层之间的通信有重要作用。控制器通过 kvo 观察模型对象,视图层也通过控制器来观察模型对象。另外,模型对象也可能观察模型对象。
kvo 可以观察任意的对象属性,包括 simple attributes, to-one relationships, 和 to-many relationships。attributes 可以理解为标量,比如 int,或者 NSNumber 对象。to-one relationships 可以理解为一个对象,比如一个学生类对象。to-many relationships 可以理解为集合对象,比如 NSArray 对象。观察 to-many relationships 的观察者可以被通知变化的类型和发生变化的对象。
要使用 kvo,首先要确认被观察的对象实现了 kvo。NSObject 和子类实现了 kvo。也可以手动实现 kvo,具体看 KVO Compliance 部分。
其次,被观察者调用 addObserver:forKeyPath:options:context: 方法注册和添加观察者。当观察者收到通知的时候,会调用 observeValueForKeyPath:ofObject:change:context: 函数进行处理。
最后,观察者不想接收通知了,在被释放内存之前,要调用 removeObserver:forKeyPath: 方法,移除观察者。
Registering for Key-Value Observing
使用 kvo 的3个步骤:
- 被观察者调用 addObserver:forKeyPath:options:context: 方法注册和添加观察者。
- 观察者实现 observeValueForKeyPath:ofObject:change:context: 方法,收到通知的时候会调用。
- 不再接收通知,要在释放内存之前,调用 removeObserver:forKeyPath: 方法移除观察者。
Registering as an Observer
- (void)addObserver:(NSObject *)observer
forKeyPath:(NSString *)keyPath
options:(NSKeyValueObservingOptions)options
context:(void *)context;
// demo
// 假设B有个数组属性
@property (nonatomic, strong) NSMutableArray *mArray;
// 注册观察者A。注意,如果 keyPath 是".mArray"会抛出字符串 NSRangeException 异常。
[B addObserver:A forKeyPath:@"mArray" options: NSKeyValueObservingOptionNew context: NULL];
- 注册观察者,接收 kvo 通知。
- observer,观察者,必须实现 *observeValueForKeyPath: ofObject: change: context: *方法,收到通知时会调用。
- keyPath,发生变化的属性。不能为 nil。
- options,定义 kvo 通知要传递的内容,有 NSKeyValueObservingOptionNew、Old、Initial、Prior。比如 New 意味着通知的 change 字典包含变化之后的值,具体看 NSKeyValueObservingOptions 部分。
- context,随意的数据,传给观察者的 observeValueForKeyPath: ofObject: change: context: 方法。
- 不会 retain 观察者和被观察者(Neither the object receiving this message, nor observer, are retained.)。记得调用 remove Observer: for Key Path: 或 remove Observer: for Key Path: context: 移除观察者。
Options
影响通知提供给观察者的 change dictionary 中的内容,以及通知的方式。
使用 NSKeyValueObservingOptionOld 可以获得旧值,NSKeyValueObservingOptionNew 可以获得新值。可以两个同时使用。
使用 NSKeyValueObservingOptionInitial 可以获得初始值,被观察者会立即发送一个通知(在 addObserver:forKeyPath:options:context: 返回之前)。NSKeyValueObservingOptionInitial 不单独使用,也不和 OptionOld 一起使用,而是和 OptionNew 一起使用,因为对于观察者来说,第一次通知时,属性的值就是新的。
使用 NSKeyValueObservingOptionInitial 代码如下所示:
// 注册观察者时,创建 option
NSKeyValueObservingOptions option = NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew;
// 观察者处理通知
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
// 输出属性初始值
NSLog(@"new = %@", change[NSKeyValueChangeNewKey]);
// 输出 null
NSLog(@"old = %@", change[NSKeyValueChangeOldKey]);
}
使用 NSKeyValueObservingOptionPrior 可以在值改变之前和改变之后收到通知。官方文档:“You instruct the observed object to send a notification just prior to a property change (除此之外,正常的通知在值改变之后发送,in addition to the usual notification just after the change) by including the option NSKeyValueObservingOptionPrior”。值改变之前的通知的 change 字典里面有一个 NSKeyValueChangeNotificationIsPriorKey,对应的值是用 @(YES) 装箱的 NSNumber 对象。利用提前发送的通知,可以在手动实现 kvo 的时候,有机会调用 - willChangeValueForKey: 方法。
使用 NSKeyValueObservingOptionPrior 代码的断点调试:
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
// 此处有断点。会执行两次。
// NSLog(@"%@", change[NSKeyValueChangeNotificationIsPriorKey]); // 输出 1
}
值改变之前的输出结果:
(lldb) po change
{
kind = 1;
notificationIsPrior = 1;
}
值改变之后的输出结果:
(lldb) po change
{
kind = 1;
}
Context
用于观察者判断通知来源。可以传 NULL,只根据 key path 来判断。但是这样在某些情况会引起问题,比如对同一个 key path 注册多次观察者,或者父类也对这个 key path 注册了观察者。可以根据 context 来判断是否要调用 super 的方法来处理通知。子类在移除观察者的时候也可以根据 context 来选定要移除的是自己而不是父类。
独一无二的静态变量的地址可以作为不错的 context。可以为一个类定义一个 context,也可以为每个属性都定义一个 context,这样可以免去对 key path 的比较判断,提高效率。
创建 context 的例子:
Listing 1 Creating context pointers
static void *PersonAccountBalanceContext = &PersonAccountBalanceContext;
static void *PersonAccountInterestRateContext = &PersonAccountInterestRateContext;
Person、Account 是类名,balance、interestRate 是属性。void *是一个指针,变量创建的时候就会分配一块内存,用&取地址后赋值给指针变量就行,不用管内存里保存的值是多少。
Listing 2 显示使用 context 注册观察者的例子:
Listing 2 Registering the inspector as an observer of the balance and interestRate properties
- (void)registerAsObserverForAccount:(Account*)account {
[account addObserver:self
forKeyPath:@"balance"
options:(NSKeyValueObservingOptionNew |
NSKeyValueObservingOptionOld)
context:PersonAccountBalanceContext];
[account addObserver:self
forKeyPath:@"interestRate"
options:(NSKeyValueObservingOptionNew |
NSKeyValueObservingOptionOld)
context:PersonAccountInterestRateContext];
}
使用 self 作为 context 可以吗?试了两种情况,第一种失败了:
void *context = &self; // 编译器会警告
if (context == &self) {
// 不会进来,因为比较结果是 nil。
}
第二种是成功的,使用桥接:
// 定义 context
void *context = (__bridge void *)(self);
// 处理通知
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
if (context == (__bridge void *)(self)) {
NSLog(@"use __bridge void *self");
}
}
注意
addObserver:forKeyPath:options:context: 方法不会 maintain 观察者、被观察者和 context。
Receiving Notification of a Change
发生变化时,观察者会收到 observeValueForKeyPath:ofObject:change:context: 消息,所有的观察者都要实现这个函数。
change 字典包含了变化的信息,context 是注册观察者的时候提供的。
change 字典可能包含的 key 有:NSKeyValueChangeKindKey、NSKeyValueChangeNewKey、NSKeyValueChangeOldKey、NSKeyValueChangeIndexesKey、NSKeyValueChangeNotificationIsPriorKey,分别对应变化类型、变化后的值、变化前的值、发生变化的元素下标、是否变化前后都发送通知。
NSKeyValueChangeKindKey 肯定是有的,表示变化类型,对应的值可以是 NSKeyValueChangeSetting、NSKeyValueChangeInsertion、NSKeyValueChangeRemoval、NSKeyValueChangeReplacement。
NSKeyValueChangeNewKey、NSKeyValueChangeOldKey、NSKeyValueChangeNotificationIsPriorKey,由注册观察者时使用的 NSKeyValueObservingOptions 决定。NSKeyValueObservingOptions 的值可以是 NSKeyValueObservingOptionOld、NSKeyValueObservingOptionNew、NSKeyValueObservingOptionInitial、NSKeyValueObservingOptionPrior。注册观察者时,NSKeyValueObservingOptionOld 对应 change 字典的 NSKeyValueChangeOldKey,NSKeyValueObservingOptionNew 对应 NSKeyValueChangeNewKey,NSKeyValueObservingOptionPrior 对应 NSKeyValueChangeNotificationIsPriorKey。
NSKeyValueChangeIndexesKey 在被观察的是集合对象(比如数组)时,字典会包含有,表示发生变化的元素下标。注意是数组的元素发生变化,而不是数组的元素的属性发生变化。比如数组里放的是 UIView 对象 view,是 view 被删除或者替换掉,而不是 view 的背景色改变颜色。
使用 NSKeyValueChangeIndexesKey 的示例代码:
// 初始化数组
self.mArray = [NSMutableArray new];
[self.mArray addObject:[UIView new]];
// 注册观察者
[self addObserver:self forKeyPath:@"mArray" options: option context:contex];
// 修改数组。这里会引发通知,变化类型是 NSKeyValueChangeReplacement,
// change 字典里面有 NSKeyValueChangeIndexesKey 及其对应的 NSIndexSet 对象。
NSMutableArray * mArray = [self mutableArrayValueForKeyPath:@"mArray"];
mArray[0] = @"99";
// 在处理通知的函数里面断点调试,获取 NSIndexSet 对象,用 po 输出 change 字典
NSIndexSet *indexSet = change[NSKeyValueChangeIndexesKey];
(lldb) po change
{
indexes = "<_NSCachedIndexSet: 0x60800003b360>[number of indexes: 1 (in 1 ranges), indexes: (0)]";
kind = 4;
new = (
99
);
}
// 注意,如果是这样修改数组,不会引发通知。
self.mArray[0] = @"99";
// 如果是这样修改,会引发通知,类型是 NSKeyValueChangeSetting
self.mArray = nil;
// 断点输出
(lldb) po change
{
kind = 1;
new = "<null>";
}
change 字典中的 NSKeyValueChangeKindKey 提供了变化类型。如果是属性的值发生变化,NSKeyValueChangeKindKey 对应的值就是 NSKeyValueChangeSetting,那么字典中可能会有 NSKeyValueChangeOldKey、NSKeyValueChangeNewKey,两个key 分别对应属性变化前和变化后的值。字典是否包含这两个 key,由注册观察者时的 NSKeyValueObservingOptions 决定,比如 NSKeyValueObservingOptionOld、NSKeyValueObservingOptionNew。
如果观察的属性是一个 to-many relationship(比如数组),当它的元素发生变化时,NSKeyValueChangeKindKey 对应的值可能是 NSKeyValueChangeInsertion、NSKeyValueChangeRemoval 或者 NSKeyValueChangeReplacement,分别是插入、删除、替换的意思。
字典的 NSKeyValueChangeIndexesKey 是一个 NSIndexSet 对象,表示发生变化的元素的位置。如果注册观察者的时候,使用了 NSKeyValueObservingOptionNew 或者 NSKeyValueObservingOptionOld,那么字典的 NSKeyValueChangeOldKey 或者 NSKeyValueChangeNewKey 对应一个数组,包含发生变化前或者变化后的相关元素。
Listing 3 显示了处理通知的例子,一个 Person 类对象观察 Account 类对象的 balance、interestRate 属性:
Listing 3 Implementation of observeValueForKeyPath:ofObject:change:context:
- (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];
}
}
上面的例子可以看到 context 的用处,不用比较字符串,而且可以区分子类和父类。
注意
如果通知传递到了类层的顶端,NSObject 会抛出一个 NSInternalInconsistencyException 异常,因为子类注册观察者收到的通知,没有进行处理。因为不是父类观察的,所以肯定也不会处理。
Removing an Object as an Observer
Listing 4 演示移除观察者:
Listing 4 Removing the inspector as an observer of balance and interestRate
- (void)unregisterAsObserverForAccount:(Account*)account {
[account removeObserver:self
forKeyPath:@"balance"
context:PersonAccountBalanceContext];
[account removeObserver:self
forKeyPath:@"interestRate"
context:PersonAccountInterestRateContext];
}
移除观察者的注意事项:
- 移除非观察者会引发异常。如果不确定移除次数,可以放在 try/catch 块处理。
- 观察者在内存中释放时,不会自动移除,被观察者会继续发送通知。和其他消息一样,发送给释放掉的对象,会引发内存访问异常。
- 一般是在 init 或者 viewDidLoad 注册观察者,在 dealloc 移除。
KVO Compliance
kvo 有两种方式发送通知:自动和手动。子类通过重写 automaticallyNotifiesObserversForKey: 方法,控制自动发送通知。
Automatic Change Notification
Listing 1 显示自动通知:
Listing 1 Examples of method calls that cause KVO change notifications to be emitted
// Call the accessor method.
[account setName:@"Savings"];
// Use setValue:forKey:.
[account setValue:@"Savings" forKey:@"name"];
// Use a key path, where 'account' is a kvc-compliant property of 'document'.
[document setValue:@"Savings" forKeyPath:@"account.name"];
// Use mutableArrayValueForKey: to retrieve a relationship proxy object.
Transaction *newTransaction = <#Create a new transaction for the account#>; // 伪代码
NSMutableArray *transactions = [account mutableArrayValueForKey:@"transactions"];
[transactions addObject:newTransaction];
Manual Change Notification
有些情况需要手动通知,比如尽量减少不必要的通知数量,或者把一些类型的变化合并到一个通知来发送。
手动和自动通知不是互斥的。如果想关闭自动通知,可以重写 automaticallyNotifiesObserversForKey: 方法,对某些 key 返回 NO。对于子类不处理的 key,要调用 super 来处理。
Listing 2 显示重写的例子:
Listing 2 Example implementation of automaticallyNotifiesObserversForKey:
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)theKey {
BOOL automatic = NO;
if ([theKey isEqualToString:@"balance"]) {
automatic = NO;
}
else {
automatic = [super automaticallyNotifiesObserversForKey:theKey];
}
return automatic;
}
要实现手动通知,在变化发生前,调用 willChangeValueForKey:,变化发生后调用 didChangeValueForKey:。
Listing 3 实现手动通知:
Listing 3 Example accessor method implementing manual notification
- (void)setBalance:(double)theBalance {
[self willChangeValueForKey:@"balance"];
_balance = theBalance;
[self didChangeValueForKey:@"balance"];
}
可以通过检查值是否变化来减少通知的发送,如 Listing 4:
Listing 4 Testing the value for change before providing notification
- (void)setBalance:(double)theBalance {
if (theBalance != _balance) {
[self willChangeValueForKey:@"balance"];
_balance = theBalance;
[self didChangeValueForKey:@"balance"];
}
}
多个值变化还可以嵌套发送通知,如 Listing 5:
Listing 5 Nesting change notifications for multiple keys
- (void)setBalance:(double)theBalance {
[self willChangeValueForKey:@"balance"];
[self willChangeValueForKey:@"itemChanged"];
_balance = theBalance;
_itemChanged = _itemChanged+1;
[self didChangeValueForKey:@"itemChanged"];
[self didChangeValueForKey:@"balance"];
}
对于 ordered to-many relationship(比如数组),不仅要指定 key,还要指定变化的类型,和发生变化的元素下标。变化类型是 NSKeyValueChange 定义的 NSKeyValueChangeInsertion, NSKeyValueChangeRemoval, 或者 NSKeyValueChangeReplacement。下标是 NSIndexSet 对象。如 Listing 6 所示:
Listing 6 Implementation of manual observer notification in a to-many relationship
- (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"];
}
Registering Dependent Keys
有些属性是相互依赖的,一个变化会引起其他属性跟着变化。通过重写 keyPathsForValuesAffectingValueForKey: 方法定义依赖属性。
To-One Relationships
对于 to-one relationship,两种方法实现属性依赖,一种是重写 keyPathsForValuesAffectingValueForKey: 方法,另一种是实现类似 keyPathsForValuesAffecting<Key> 的方法(比如 keyPathsForValuesAffectingAge)。
比如 full name 由 first 和 last name 决定:
- (NSString *)fullName {
return [NSString stringWithFormat:@"%@ %@",firstName, lastName];
}
fullName 的观察者应该在 firstName 或者 lastName 发生变化时收到通知。
Listing 1 显示重写 keyPathsForValuesAffectingValueForKey: 方法的例子:
Listing 1 Example implementation of keyPathsForValuesAffectingValueForKey:
+ (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key {
NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
if ([key isEqualToString:@"fullName"]) {
NSArray *affectingKeys = @[@"lastName", @"firstName"];
keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys];
}
return keyPaths;
}
重写要先调用父类的方法,然后把依赖的属性名放入一个数组,再添加到父类方法返回的 set 对象。
另一个方法是实现 keyPathsForValuesAffecting<Key> 方法,如 Listing 2 所示:
Listing 2 Example implementation of the keyPathsForValuesAffecting<Key> naming convention
+ (NSSet *)keyPathsForValuesAffectingFullName {
return [NSSet setWithObjects:@"lastName", @"firstName", nil];
}
不能在 category 中重写 keyPathsForValuesAffectingValueForKey: 方法,因为会覆盖系统方法。这种情况可以在 category 添加 keyPathsForValuesAffecting<Key> 方法实现属性依赖。
To-Many Relationships
对于 to-many relationships, 不能通过重写 keyPathsForValuesAffectingValueForKey: 方法实现属性依赖。
比如 Department 对象有个 to-many relationship (employees),存放 Employee 对象。Department 有 totalSalary 属性,依赖于 Employee 的 salary 属性。不能重写 keyPathsForValuesAffectingValueForKey: 方法返回 employees.salary 作为依赖属性的 keyPath。
两种方法,一种是使用 kvo 注册 parent (in this example, Department) 成为所有 children (Employees in this example) 的相关属性(这里指 salary)的观察者。在添加和删除 child 的时候, parent 要注册或者注销观察者。然后某个 child 的 salary 属性发生变化的时候,parent 在 observeValueForKeyPath:ofObject:change:context: 方法中更新自己的 totalSalary 属性并通知自己的观察者。如 Listing 3 所示:
- (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 {
// 这里是使用 kvc 的操作符,对数组 employees 的元素求和
[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 相关,If you're using Core Data, you can register the parent with the application's notification center as an observer of its managed object context. The parent should respond to relevant change notifications posted by the children in a manner similar to that for key-value observing。
Key-Value Observing Implementation Details
Automatic key-value observing is implemented using a technique called isa-swizzling.
自动 kvo 是通过 isa-swizzling 技术实现的。
The isa pointer, as the name suggests, points to the object's class which maintains a dispatch table. This dispatch table essentially contains pointers to the methods the class implements, among other data.
isa 指针指向对象的 class,class 维护一张分发表,分发表包含了 class 实现的方法的方法指针和其他数据。
When an observer is registered for an attribute of an object the isa pointer of the observed object is modified, pointing to an intermediate class rather than at the true class. As a result the value of the isa pointer does not necessarily reflect the actual class of the instance.
注册观察者的时候,被观察者的 isa 指针被修改了,指向一个中间类,结果就是 isa 指针不一定能反映对象真实的 class。
You should never rely on the isa pointer to determine class membership. Instead, you should use the class method to determine the class of an object instance.
不要用 isa 指针来判断对象所属的 class,应该使用 class 方法 来判断。