美文网首页
了解 Key-Value Observing

了解 Key-Value Observing

作者: _涼城 | 来源:发表于2020-12-28 16:01 被阅读0次

    为了理解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 <Key>的类方法来获得相同的结果,其中<Key>是依赖于值的属性名称(首字母大写)。

      添加到类别时可以使用此实现方式

      + (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<count; i++) {
              Method method = list[I];
              SEL sel = method_getName(method);
              IMP imp = class_getMethodImplementation(class, sel);
              NSLog(@"%@-%p",NSStringFromSelector(sel),imp);
          }
          free(list);
      }
      
      ![Person类与中间类.png](https://img.haomeiwen.com/i2438680/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

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