了解 Key-Value Observing

为了理解KVO,首先需要了解 Key-Value Coding。

Key-value observing提供途径允许将其他对象的特定属性的更改通知给对象。可以通过Key-value observing观察到包括简单属性,一对一关系一对多关系的属性。一对多关系的观察者被告知所做更改的类型,以及更改涉及哪些对象。

kvo

注意: 虽然 UIKit 框架的类一般不支持 KVO,但仍然可以在应用程序的自定义对象中实现它,包括自定义视图。

KVO的编程思想

响应式编程

KVO的使用

  1. 被观察者添加观察者addObserver:forKeyPath:options:context:
  2. 观察者实现监听方法observeValueForKeyPath:ofObject:change:context:
  3. 最后,被观察者移除监听removeObserver:forKeyPath:`

NSKeyValueChangeKey

typedef NSString * NSKeyValueChangeKey NS_STRING_ENUM;
/* Keys for entries in change dictionaries. See the comments for -observeValueForKeyPath:ofObject:change:context: for more information.
*/
FOUNDATION_EXPORT NSKeyValueChangeKey const NSKeyValueChangeKindKey;
FOUNDATION_EXPORT NSKeyValueChangeKey const NSKeyValueChangeNewKey;
FOUNDATION_EXPORT NSKeyValueChangeKey const NSKeyValueChangeOldKey;
FOUNDATION_EXPORT NSKeyValueChangeKey const NSKeyValueChangeIndexesKey;
FOUNDATION_EXPORT NSKeyValueChangeKey const NSKeyValueChangeNotificationIsPriorKey API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));

NSKeyValueChange

typedef NS_ENUM(NSUInteger, NSKeyValueChange) {

NSKeyValueChangeSetting = 1,

NSKeyValueChangeInsertion = 2,

NSKeyValueChangeRemoval = 3,

NSKeyValueChangeReplacement = 4,

};

NSKeyValueSetMutationKind

typedef NS_ENUM(NSUInteger, NSKeyValueSetMutationKind) {
    NSKeyValueUnionSetMutation = 1,
    NSKeyValueMinusSetMutation = 2,
    NSKeyValueIntersectSetMutation = 3,
    NSKeyValueSetSetMutation = 4
};

Registering for Key-Value Observing

参数Options

options参数(指定为选项常量的按位或)既会影响通知中提供的更改字典的内容,又会影响生成通知的方式。

参数context

context参数,上下文,void *

作用

一种更安全,更可扩展的方法是使用上下文确保您收到的通知是发给观察者的,而不是超类的。

实现

  1. 变量PersonNameContext

    static void *PersonNameContext = &PersonNameContext;
    
  2. 添加观察者时配置context

    [self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:PersonNameContext];
    
  3. 观察时判断

    static void *PersonNameContext = &PersonNameContext;
    
    - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
    {
        if (context == PersonNameContext) {
    
        } else {
            [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
        }
    }
    

Receiving Notification of a Change

  • 当对象的观察属性的值更改时,观察者将收到一条ObservValueForKeyPath:ofObject:change:context:消息。所有观察者都必须实现此方法。

  • NSKeyValueChangeKindKey提供有关发生的更改类型的信息。如果所观察对象的值已更改,则NSKeyValueChangeKindKey将返回NSKeyValueChangeSetting

  • 根据注册观察者时指定的选项,更改字典中的NSKeyValueChangeOldKeyNSKeyValueChangeNewKey包含更改前后的属性值。

  • 如果属性是对象,则直接提供值。如果属性是标量或C结构,则将值包装在NSValue对象中。

  • 如果观察到的属性是一对多关系,则NSKeyValueChangeKindKey还通过分别返回NSKeyValueChangeInsertionNSKeyValueChangeRemovalNSKeyValueChangeReplacement来插入,移除或替换关系中的对象。

  • 在任何情况下,观察者均应在无法识别上下文(或在简单情况下,是任何键路径)时始终调用父类的observeValueForKeyPath:ofObject:change:context的实现,因为这意味着父类已针对通知。

  • 在任何情况下,观察者均应在无法识别上下文(或在简单情况下,是任何键路径)时始终调用父类的observeValueForKeyPath:ofObject:change:context的实现,因为这意味着超类已针对通知。

    • 如果在注册观察者时指定了NULL上下文,则将通知的键路径与要观察的键路径进行比较,以确定发生了什么变化。
    • 如果为所有观察到的关键路径使用了单个上下文,则首先要根据通知的上下文进行测试,然后找到匹配项,然后使用关键路径字符串比较来确定具体更改的内容。
    • 如果为每个键路径提供了唯一的上下文,如此处所示,则一系列简单的指针比较会同时告诉您通知是否针对此观察者,如果是,则更改了哪个键路径。
  • 如果通知传播到类层次结构的顶部,则NSObject会引发NSInternalInconsistencyException,因为这是编程错误:子类无法使用为其注册的通知

    通常NSInternalInconsistencyException,是由于未移除观察者导致的

Removing an Object as an Observer

通过向被观察对象发送一条removeObserver:forKeyPath:context:消息来删除键值观察者,并指定观察对象,键路径和上下文。

 [self.person removeObserver:self forKeyPath:@"name" context:PersonNameContext];

删除观察者时,请记住以下几点:

  • 如果尚未注册,移除观察者会导致NSRangeException
  • removeObserver:forKeyPath:context:addObserver:forKeyPath:options:context:一一对应;
  • 请将removeObserver:forKeyPath:context:调用在try / catch块内处理潜在的异常。

KVO Compliance

Manual Change Notification

手动和自动通知不是互斥的。除了已经发布的自动通知之外,还可以发布手动通知。

通常,可能希望完全控制特定属性的通知。在这种情况下,将覆盖NSObject实现的automaticNotifyObserversForKey:。对于要排除其自动通知的属性,automaticNotifyObserversForKey的类实现应返回NO

automaticallyNotifiesObserversForKey:

+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)theKey {

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

Example accessor method implementing manual notification

要实现手动观察者通知,请在更改值之前调用willChangeValueForKey:,在更改值之后调用didChangeValueForKey:

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

Implementation of manual observer notification in a to-many relationship

有序对多关系的情况下,不仅必须指定已更改的键,还必须指定更改的类型和所涉及对象的索引。更改的类型是NSKeyValueChange,它指定NSKeyValueChangeInsertionNSKeyValueChangeRemovalNSKeyValueChangeReplacement。受影响的对象的索引作为NSIndexSet对象传递。

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

To-One Relationships

fullName依赖于firstNamelastName,当firstNamelastName属性更改时,必须通知观察fullName属性的应用程序,因为它们会影响该属性的值。

- (NSString *)fullName {
    return [NSString stringWithFormat:@"%@ %@",firstName, lastName];
}
路径观察
  • 重写keyPathsForValuesAffectingValueForKey:,设置依赖的从属键firstNamelastName

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

无法通过实现keyPathsForValuesAffectingValueForKey:来建立对多关系的依赖关系。必须观察“ to-many”集合中每个对象的适当属性,并通过自己更新相关键来响应其值的更改。

数组观察

mutableArrayValueForkey:

监听数组的变化

self.person.dataArray = [NSMutableArray array];
[self.person addObserver:self forKeyPath:@"dataArray" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:PersonNameContext];

用KVC的方式变更属性

  [[self.person mutableArrayValueForKey:@"dataArray"] addObject:@"1"];

监听结果

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
    if (context == PersonNameContext) {
        NSLog(@"change%@",change);
    } else {
        [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
    }
}

输出

change{
    indexes = "<_NSCachedIndexSet: 0x600003665d80>[number of indexes: 1 (in 1 ranges), indexes: (0)]";
    kind = 2;
    new =     (
        1
    );
}

KVO底层原理

KVO无法监听对类的成员变量赋值

KVO是使用isa-swizzling的技术实现的

  • 顾名思义,isa指针指向维护分配表的对象的类。该分派表实质上包含指向该类实现的方法的指针以及其他数据。
  • 在为对象的属性注册观察者时,将修改观察对象的isa指针,指向中间类而不是真实类。结果,isa指针的值不一定反映实例的实际类。
  • 永远不应依靠isa指针来确定类成员身份。相反,应该使用class方法来确定对象实例的类。

调试添加及移除观察者的前后

  • 在添加观察者前以及添加观察者后,分别使用object_getClassName()可得下图,NSKVONotifying_PersonPerson的子类(派生类)

    添加观察者前后调试.png

    在添加观察者后,查看NSKVONotifying_Person和Person的方法列表,NSKVONotifying_Person重写了监听属性的setter方法

    - (void)printAllMethodsWithClass:(Class)class{
        unsigned int count = 0;
        Method *list = class_copyMethodList(class, &count);
        for (int i = 0; i
    ![Person类与中间类.png](https://upload-images.jianshu.io/upload_images/2438680-ea3c794377779555.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
    
  • 在移除全部监听后,查看对象isa已被还原,但是中间类生成后不会被销毁


    移除观察者后.png

KVO监听赋值过程

  • 在初始化Person对象时,断点调试监听赋值过程

    watchpoint set variable self->_person->_name

    watchpoint.png
  • 查看栈信息,在监听属性做赋值时会按下图顺序执行

    1. _NSSetObjectValueAndNotify

    2. -[NSObject(NSKeyValueObservingPrivate) _changeValueForKey:key:key:usingBlock:]:

    3. -[NSObject(NSKeyValueObservingPrivate) _changeValueForKeys:count:maybeOldValuesDict:maybeNewValuesDict:usingBlock:

      方法内部

      1. NSKeyValueWillChange
      2. [super setName:]
      3. NSKeyValueDidChange
    监听属性赋值时的栈信息.png

你可能感兴趣的:(了解 Key-Value Observing)