iOS KVO(键值观察) 总览

原文链接 Cyrus'blog

iOS KVO(键值观察) 总览_第1张图片

本文主要内容来自于对官方文档 Key-Value Observing Programming Guide 的翻译,以及一部分我自己的理解和解释,如果有说错的地方请及时联系我。

At a Glance

KVO 也就是 键值观察 ,它提供了一种机制,使得当某个对象特定的属性发生改变时能够通知到别的对象。这经常用于 model 和 controller 之间的通信。KVO主要的优点是你不需要在每次属性改变时手动去发送通知。并且它支持为一个属性注册多个观察者。

注册 KVO

  • 被观察对象 的属性必须是 [KVO Compliant](file:///Users/hcy/Library/Developer/Shared/Documentation/DocSets/com.apple.adc.documentation.iOS.docset/Contents/Resources/Documents/documentation/Cocoa/Conceptual/KeyValueObserving/Articles/KVOCompliance.html#//apple_ref/doc/uid/20002178-BAJEAIEE)
  • 必须用 被观察对象addObserver:forKeyPath:options:context: 方法注册观察者
  • 观察者 必须实现 observeValueForKeyPath:ofObject:change:context: 方法

注册成为观察者


为了能够在属性改变时被通知到,一个 观察者对象 必须通过 被观察对象addObserver:forKeyPath:options:context: 方法注册成为观察者。

  • observer 参数也就是一个观察者对象

  • keyPath 表示要观察的属性

  • options 决定了提供给观察者change字典中的具体信息有哪些。(change字典是一个提供给观察者的参数,后面会提到)

    • NSKeyValueObservingOptionOld 表示在change字典中包含了改变前的值。
    • NSKeyValueObservingOptionNew 表示在change字典中包含新的值。
    • NSKeyValueObservingOptionInitial 在注册观察者的方法return的时候就会发出一次通知。
    • NSKeyValueObservingOptionPrior 会在值发生改变前发出一次通知,当然改变后的通知依旧还会发出,也就是每次change都会有两个通知。
  • context 这个参数可以是一个 C指针,也可以是一个 对象引用,它可以作为这个context的唯一标识,也可以提供一些数据给观察者。

注意: addObserver:forKeyPath:options:context: 方法不会持有观察者对象,被观察对象,以及context的强引用。你要确保自己持有了他们的强引用。

属性变化时接收通知


当一个被观察属性的值发生改变时,观察者会收到 observeValueForKeyPath:ofObject:change:context: 的消息。所有的观察者必须实现这个方法。这个方法中的参数和注册观察者方法的参数基本相同,只有一个 change 不同。 change 是一个字典,它里面包含了的信息由注册时的 options 决定。

官方提供了这些key给我们来取到 change 中的value:

NSString *const NSKeyValueChangeKindKey;
NSString *const NSKeyValueChangeNewKey;
NSString *const NSKeyValueChangeOldKey;
NSString *const NSKeyValueChangeIndexesKey;
NSString *const NSKeyValueChangeNotificationIsPriorKey;
  • NSKeyValueChangeKindKey 这个key包含的value是一个 NSNumber 里面是一个 int,与之对应的是 NSKeyValueChange 的枚举
enum {
  NSKeyValueChangeSetting = 1,
  NSKeyValueChangeInsertion = 2,
  NSKeyValueChangeRemoval = 3,
  NSKeyValueChangeReplacement = 4
};
typedef NSUInteger NSKeyValueChange;

change[NSKeyValueChangeKindKey]NSKeyValueChangeSetting 的时候,说明被观察属性的setter方法被调用了。
而下面三种,根据官方文档的意思是,当被观察属性是集合类型,且对它进行了 insert,remove,replace 操作的时候会返回这三种Key,但是我自己测试的时候没有测试出来不知道是不是我理解错了。

  • NSKeyValueChangeNewKeyNSKeyValueChangeOldKey 顾名思义,当你在注册的时候 options 参数中填了对应的 NSKeyValueObservingOptionNewNSKeyValueObservingOptionOld ,并且 NSKeyValueChangeKindKey 的值是 NSKeyValueChangeSetting ,你就可以通过这两个key取到 旧值和新值。

  • NSKeyValueChangeIndexesKey, 当 NSKeyValueChangeKindKey 的结果是 NSKeyValueChangeInsertion, NSKeyValueChangeRemovalNSKeyValueChangeReplacement 的时候,这个key的value是一个NSIndexSet,包含了发生insert,remove,replace的对象的索引集合

  • NSKeyValueChangeNotificationIsPriorKey,这个key包含了一个 NSNumber,里面是一个布尔值,如果在注册时 options 中有 NSKeyValueObservingOptionPrior,那么在前一个通知中的 change 中就会有这个key的value, 我们可以这样来判断是不是在改变前的通知[change[NSKeyValueChangeNotificationIsPriorKey] boolValue] == YES;

移除一个观察者


你可以通过 removeObserver:forKeyPath: 方法来移除一个观察。如果你的 context 是一个 对象,你必须在移除观察之前持有它的强引用。当移除了观察后,观察者对象再也不会受到这个 keyPath 的通知。

KVO Compliance

有两种方式能够保证 change notification 能够被发出。

  • 自动通知,继承自NSObject,并且所有的属性符合[KVC规范](file:///Users/hcy/Library/Developer/Shared/Documentation/DocSets/com.apple.adc.documentation.iOS.docset/Contents/Resources/Documents/documentation/Cocoa/Conceptual/KeyValueCoding/Articles/Compliant.html#//apple_ref/doc/uid/20002172)这样就不用写额外的代码去实现自动通知。
  • 手动通知,让你的子类实现 automaticallyNotifiesObserversForKey: 方法,来决定是否需要自动通知,如果是手动通知需要额外的代码。

自动通知


NSObject 已经实现了自动通知,只要通过 setter 方法去赋值,或者通过 KVC 就可以通知到观察者。自动通知也支持集合代理对象,比如 mutableArrayValueForKey: 方法。

// 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];

手动通知


手动通知提供了更自由的方式去决定什么时间,什么方式去通知观察者。这可以帮助你最少限度触发不必要的通知,或者一组改变值发出一个通知。想要使用手动通知必须实现automaticallyNotifiesObserversForKey: 方法。(或者automaticallyNotifiesObserversOfS)在一个类中同时使用自动和手动通知是可行的。对于想要手动通知的属性,可以根据它的keyPath返回NO,而其对于其他位置的keyPath,要返回父类的这个方法。

+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)theKey {
       BOOL automatic = NO;
       if ([theKey isEqualToString:@"openingBalance"]) {
           automatic = NO;
       } else {
           automatic = [super automaticallyNotifiesObserversForKey:theKey];
       }
       return automatic;
}

要实现手动通知,你需要在值改变前调用 willChangeValueForKey: 方法,在值改变后调用 didChangeValueForKey: 方法。你可以在发送通知前检查值是否改变,如果没有改变就不发送通知

- (void)setOpeningBalance:(double)theBalance {
       if (theBalance != _openingBalance) {
        [self willChangeValueForKey:@"openingBalance"];
        _openingBalance = theBalance;
        [self didChangeValueForKey:@"openingBalance"];
       }
}

如果一个操作会导致多个属性改变,你需要嵌套通知,像下面这样:

- (void)setOpeningBalance:(double)theBalance {
       [self willChangeValueForKey:@"openingBalance"];
       [self willChangeValueForKey:@"itemChanged"];
       _openingBalance = theBalance;
       _itemChanged = _itemChanged+1;
       [self didChangeValueForKey:@"itemChanged"];
       [self didChangeValueForKey:@"openingBalance"];
}

在一个一对多的关系中,你必须注意不仅仅是这个key改变了,还有它改变的类型以及索引。

- (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"];
}

键之间的依赖

在很多种情况下一个属性的值依赖于在其他对象中的属性。如果一个依赖属性的值改变了,这个属性也需要被通知到。

To-one Relationships


比如有一个教 fullName 的属性,依赖于 firstNamelastName,当 firstName 或者 lastName 改变时,这个 fullName 属性需要被通知到。

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

你可以重写 keyPathsForValuesAffectingValueForKey: 方法。其中要先调父类的这个方法拿到一个set,再做接下来的操作。

+ (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key {
 
    NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
 
    if ([key isEqualToString:@"fullName"]) {
        NSArray *affectingKeys = @[@"lastName", @"firstName"];
        keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys];
    }
    return keyPaths;
}

你也可以通过实现 keyPathsForValuesAffecting 方法来达到前面同样的效果,这里的就是属性名,不过第一个字母要大写,用前面的例子来说就是这样:

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

To-many Relationships


keyPathsForValuesAffectingValueForKey:方法不能支持 to-many 的关系。举个例子,比如你有一个 Department 对象,和很多个 Employee 对象。而 Employee 有一个 salary 属性。你可能希望 Department 对象有一个 totalSalary 的属性,依赖于所有的 Employee 的 salary 。

你可以注册 Department 成为所有 Employee 的观察者。当 Employee 被添加或者被移除时,你必须要添加和移除观察者。然后在 observeValueForKeyPath:ofObject:change:context: 方法中,根据改变做出反馈。

- (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;
}

KVO的实现细节

KVO 的实现用了一种叫 isa-swizzling 的技术。isa 指针就是指向类的指针,当一个对象的一个属性注册了观察者后,被观察对象的isa指针的就指向了一个系统为我们生成的中间类,而不是我们自己创建的类。在这个类中,系统为我们重写了被观察属性的setter方法。你可以通过 object_getClass(id obj) 方法获得对象真实的类,在 addObserver 前后分别打印,就可以看到isa指针被指向了一个中间类。似乎都是在原来的类名前面加上 NSKVONotifying_

isa指针不总是指向真实的类,所以你不应该依赖于 isa 指针来判断这个对象的类型,而应该通过 class 方法来判断对象的类型。如果你还不知道什么是isa指针,可以看我之前写的博客 Objective-C runtime 的简单理解与使用

你可能感兴趣的:(iOS KVO(键值观察) 总览)