Kealdish's Studio.

庖丁解牛KVO(一)——KVO概览和使用

字数统计: 2.2k阅读时长: 8 min
2016/04/13 Share

KVO是对Objective-c观察者模式的实现,也是OC非常强大和有用的特性,同时还是实现cocoa bndings的基础(-bind:toObject:withKeyPath:options:)。因此,理解和掌握KVO可以给我们带来很多便利以及意想不到的效果。

KVO可以让观察者在被观察者的属性被修改时直接接收到通知。在KVO的作用下,一个对象可以观察另一个对象的任何属性,同时,也可以知道某个属性的修改前和修改后的值。对多关系的观察者不仅能知道改变发生的类型,而且还能知道哪些对象被改变了。

在通知机制中,KVO与NSNotification提供的通知机制类似,但是也存在很鲜明的区别。NSNotification是以广播的形式将通知传递给所有注册为观察者的对象,即“一对多”,而KVO则会在属性值发生改变时直接将通知传递给观察者,即“点对点”。

由于基类NSObject提供了对于KVO的基本实现,因而所有的cocoa对象本质上都能够使用KVO。为了能够接收到KVO的通知,你必须要做以下几件事:

  • 你必须确保被观察类的属性符合KVO规则(KVO Compliance)。KVO规定被观察对象的类必须符合KVC规则(KVC Compliance)同时允许属性的自动观察者通知或者手动实现属性的KVO。

  • 给值会发生改变的对象(即被观察者)添加观察者。你可以通过调用addObserver:forKeyPath:options:context:方法做到。

  • 在观察者对象中,实现方法observeValueForKeyPath:ofObject:change:context:。该方法在被观察对象的属性值发生变化时会被调用。

第二点和第三点很好理解,第一点则需要多说一点。并不是说所有类的属性都满足KVO规则,他需要满足以下几个条件:

  • 类的属性必须满足KVC规则。KVO与KVC支持的数据类型相同,包括OC对象、标量和结构体。

  • 类要发送属性变化的KVO通知。

  • 依赖的键需要被正确的注册。

关于第一点,何为满足KVC规则?对于一个满足KVC规则的指定属性,它必须实现valueForKey:setValue:forKey:来作用于指定属性。更详细地:

对于対一关系的属性,它的类必须做到:

  • 实现方法名为-<key>-is<key>的方法,或者创建对象名为或者_的实例对象。
  • 如果属性是可变的,那么它还需要实现-set<key>:方法。
  • -set<Key>:的方法实现不应该包含验证操作。
  • 若key需要进行验证它的类还需要实现-validate<Key>:error:方法。

对于有索引的对多关系的属性,它的类必须要做到:

  • 实现方法名为-<key>的方法并返回数组。
  • 或者有变量名为或_的实例变量。
  • 或者实现方法-countOf<key>-objectIn<key>``-<key>AtIndexes:两个方法中的一个。
  • 另外,你可以实现-get<key>:range:来优化性能。

对于无序的对多关系的属性,它的类需要做到:

  • 实现方法名为-<key>的方法并并返回一个集合。
  • 或者创建变量名为或_的实例变量。
  • 或者实现方法-countOf<key>-enumeratorOf<key>-memberOf<key>:
    若该属性还是可变的,它的类还需做到:
  • 实现-add<key>Object:-add<key>:中的一个或两个。
  • 实现-remove<key>Object:-remove<key>:中的一个或两个。
  • 另外,你可以实现-intersect<key>:-set<key>:来优化性能。

关于第二点,如何确保通知被发送?对于所有满足KVC规则的类的属性已经自动支持KVO。因此,只要你遵守标准Cocoa编码和命名规范,你可以使用自动变化通知(Automatic Change Notification)————你不需要编写额外的代码。
以下代码示例会触发KVO变化通知被发送:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 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)则派上用场了。
在这种情况下,你需要重写NSObjectautomaticallyNotifiesObserversForKey:方法。若你想将某个属性从自动变化通知中移除,则在automaticallyNotifiesObserversForKey:方法中返回NO。

1
2
3
4
5
6
7
8
9
10
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)theKey {
BOOL automatic = NO;
if ([theKey isEqualToString:@"balance"]) {
automatic = NO;
}
else {
automatic = [super automaticallyNotifiesObserversForKey:theKey];
}
return automatic;
}

为了实现手动变化通知,你需要在改变变量值�前触发willChangeValueForKey:方法并在改变变量值后触发didChangeValueForKey:方法。

1
2
3
4
5
- (void)setBalance:(double)theBalance {
[self willChangeValueForKey:@"balance"];
_balance = theBalance;
[self didChangeValueForKey:@"balance"];
}

你可以通过检验值是否发生改变来减少不必要的通知发送。

1
2
3
4
5
6
7
- (void)setBalance:(double)theBalance {
if (theBalance != _balance) {
[self willChangeValueForKey:@"balance"];
_balance = theBalance;
[self didChangeValueForKey:@"balance"];
}
}

若一个操作引起多个属性值发生变化,你必须像下面这样嵌套变化的通知:

1
2
3
4
5
6
7
8
- (void)setBalance:(double)theBalance {
[self willChangeValueForKey:@"balance"];
[self willChangeValueForKey:@"itemChanged"];
_balance = theBalance;
_itemChanged = _itemChanged+1;
[self didChangeValueForKey:@"itemChanged"];
[self didChangeValueForKey:@"balance"];
}

在有序的对多关系的属性中,你必须不仅指明改变的属性,而且还需指明改变的类型和涉及到的对象的索引值。改变的类型是NSKeyValueChange枚举,包含NSKeyValueChangeInsertion, NSKeyValueChangeRemoval, NSKeyValueChangeReplacement。受影响的对象的索引值传入的是NSIndexSet对象。

1
2
3
4
5
6
7
8
9
- (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通知能正确发送?

在対一关系中,为了触发自动通知你需要重写keyPathsForValuesAffectingValueForKey:方法。
比如,一个人的名字包含姓和名。他的名字的全称可以用以下方法来写:

1
2
3
- (NSString *)fullName {
return [NSString stringWithFormat:@"%@ %@",firstName, lastName];
}

当firstName或者lastName属性值发生改变时,观察fullName的对象必须接收到通知。其中一个解决方法是重写keyPathsForValuesAffectingValueForKey:方法来指明fullName属性依赖于lastName和firstName属性。

1
2
3
4
5
6
7
8
9
10
+ (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key {
NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
if ([key isEqualToString:@"fullName"]) {
NSArray *affectingKeys = @[@"lastName", @"firstName"];
keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys];
}
return keyPaths;
}

如上面代码示例中,你的重写方法实现中必须要触发super方法调用,并且返回的集合中要包含父类的keys。
你还可以通过实现命名规则遵循keyPathsForValuesAffecting<Key>(Key为属性名并且首字母大写)的类方法来得到同样的结果。使用该方案对上面代码进行改写如下:

1
2
3
+ (NSSet *)keyPathsForValuesAffectingFullName {
return [NSSet setWithObjects:@"lastName", @"firstName", nil];
}

当你使用分类给现有的类添加属性时,不能重写keyPathsForValuesAffectingValueForKey:方法,因为在分类中不支持重写该方法。在这种情况下,只能通过实现keyPathsForValuesAffecting<Key>类方法来达到效果。

在对多关系中,keyPathsForValuesAffectingValueForKey:方法不支持。比如,你有一个Deprtment对象与employee对象的对多关系,并且employee有salary属性。在Department对象中有一个totalSalary属性依赖于所有的employee的salary的关系。
这里有两种解决方案:
1.你可以使用KVO注册父类(Department)为所有子类(employee)的salary属性的观察者。你必须在添加和移除子类对象的过程中添加和移除观察者。以下为示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
- (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;
}

2.如果你使用Core Data,你可以注册父类为它管理对象上下文的观察者。当子类相关属性值发生改变其父类就会得到相应的通知,类似于KVO。

CATALOG