iOS KVO(键值观察) 总览

作者: yahtzee_ | 来源:发表于2016-04-07 22:10 被阅读4500次

    原文链接 Cyrus'blog

    本文主要内容来自于对官方文档 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<Key>)在一个类中同时使用自动和手动通知是可行的。对于想要手动通知的属性,可以根据它的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<Key> 方法来达到前面同样的效果,这里的<Key>就是属性名,不过第一个字母要大写,用前面的例子来说就是这样:

    + (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:@"employees.@sum.salary"]];
    }
     
    - (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 的简单理解与使用

    相关文章

      网友评论

      • 9d426ee34834:KVO 我观察单例么 我单例里面写了 一个字典 我现在要检测这个字典的变化。
      • coderChrisLee:翻译理解得挺到位的,我注册观察的时候只观察了old | new ,看来可以再增加一个IsPriorKey,把3中option 放一起 注册观察, old | new | isPriorKey !
        yahtzee_:@coderChrisLee 还是看具体的需求吧,我一般用的时候也就只用用New:smile:

      本文标题:iOS KVO(键值观察) 总览

      本文链接:https://www.haomeiwen.com/subject/xqcvlttx.html