美文网首页OC面试相关
OC底层原理18 - KVO

OC底层原理18 - KVO

作者: 卡布奇诺_95d2 | 来源:发表于2021-03-19 18:54 被阅读0次

    简介

    KVO,全称为Key-Value observing,中文名为键值观察,KVO是一种机制,它允许将其他对象的指定属性的更改通知给对象

    Key-Value Observing Programming Guide官方文档中,又这么一句话:理解KVO之前,必须先理解KVC(即KVO是基于KVC基础之上)。

    KVC是键值编码,在对象创建完成后,可以动态的给对象属性赋值,而KVO是键值观察,提供了一种监听机制,当指定的对象的属性被修改后,则对象会收到通知,所以可以看出KVO是基于KVC的基础上对属性动态变化的监听。

    KVO与NSNotificatioCenter的区别

    • 相同点
      • 两者的设计模式都使用观察者模式
      • 都是用于对象的监听
      • 都能实现一对多的操作
    • 不同点
      • KVO只能用于监听对象属性的变化,并且属性名都是通过NSString来查找,编译器不会帮你检测对错和补全,纯手敲会比较容易出错
      • NSNotification的发送监听(post)的操作我们可以控制,KVO由系统控制
      • KVO可以记录新旧值变化

    KVO 使用

    基本操作

    • 注册观察者,addObserver:forKeyPath:options:context
      • observer参数:注册KVO通知的对象,即观察者;观察者必须实现key-value观察方法:observeValueForKeyPath:ofObject:change:context:
      • keyPath参数:被观察对象的键值路径,这个值不允许为nil
      • options参数,这是一个NSKeyValueObservingOptions枚举类型。
        • NSKeyValueObservingOptionNew:当告知观察者对象发生变化时,提供新的属性值
        • NSKeyValueObservingOptionOld:当告知观察者对象发生变化时,提供旧的属性值
        • NSKeyValueObservingOptionInitial:通知应该立即发送给观察者,在观察者注册方法甚至返回之前
        • NSKeyValueObservingOptionPrior:在每次更改之前之后分别向观察者发送通知,而不是在更改之后发送单个通知。这与-willChangeValueForKey:被触发的时间是相对应的。在每次修改属性时,实际上是会发送两条通知。
      • context参数:任意的额外数据,我们可以将这些数据作为上下文数据并传递至observeValueForKeyPath:ofObject:change:context方法中。这个参数的意义在于区分同一对象监听同一属性(从属于同一对象)的多个不同的监听。
    • 实现KVO回调,observeValueForKeyPath:ofObject:change:context
      • keyPath参数:被观察对象的属性的键值路径,这个值不允许为nil
      • object参数:被观察对象。
      • change参数:这是一个字典,它包含了属性被修改的一些信息。这个字典中包含的值会根据我们在添加观察者时设置的options参数的不同而有所不同。change中的内容可通过以下key进行获取。官方提供了以下五种Key。
        1. NSKeyValueChangeKindKey,提供了发生变化类型的信息。
          1. 返回NSKeyValueChangeSetting,表示观察对象被设置了一个新值
          2. 返回NSKeyValueChangeInsertion,表示一对多关系的对象中有值被插入
          3. 返回NSKeyValueChangeRemoval,表示一对多关系的对象中有值被移除
          4. 返回NSKeyValueChangeReplacement,表示一对多关系的对象中有值被替换
        2. NSKeyValueChangeNewKey:观察对象被设置的新值,需要将options设置成NSKeyValueObservingOptionNew
        3. NSKeyValueChangeOldKey:观察对象被设置新值之前的旧值,需要将options设置成NSKeyValueObservingOptionOld
        4. NSKeyValueChangeIndexesKey:当一对多关系的对象中有值被插入、移除或替换,则通过该获取插入、移除或替换索引
        5. NSKeyValueChangeNotificationIsPriorKey:当options设置成NSKeyValueObservingOptionPrior选项时,可以使用NSKeyValueChangeNotificationIsPriorKey来获取到通知是否是预先发送的,如果是,获取到的值总是@(YES)
      • context参数:这个值即是添加观察者时提供的上下文信息。
    • 移除观察者,removeObserver:forKeyPath:contextremoveObserver:forKeyPath
      • observer参数:注册KVO通知的对象,即观察者。
      • keyPath参数:被观察对象的键值路径,这个值不允许为nil
      • context参数:这个值即是添加观察者时提供的上下文信息。

    移除观察者注意点:

    • 如果还没有注册成观察者,就要求被移除会导致NSRangeException异常。正常情况,一次removeObserver:forKeyPath:context:对应于addObserver:forKeyPath:options:context:。或者在removeObserver:forKeyPath:context:中增加try/catch块处理异常。
    • 当对象被释放时,观察者不会自动移除。被观察对象继续发送通知,发送给已释放对象的更改通知会触发内存访问异常。因此,可以确保观察者在从内存中消失之前删除自己。
    • KVO协议并不提供接口获取当前对象是否是观察者或被观察者。在构建代码时为了避免相关错误,一个典型方法是在对象初始化的时候注册成观察者(例如在init或viewDidLoad中),在对象释放的时候移除观察者(通常在dealloc中)。

    KVO自动触发和手动触发

    • 自动触发
      当类方法automaticallyNotifiesObserversForKey返回YES时,表示自动触发KVO。当返回NO时,关闭自动触发KVO
      NSObject提供了一个基本的自动键值变化通知的实现。但不是所有的方法都能自动触发KVO,以下方法能自动触发KVO

      // 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返回NO时,可手动触发KVO。如下:属性的setter方法中,在改变value之前调用willChangeValueForKey,在改变value之后调用didChangeValueForKey

      - (void)setName:(NSString *)name{
          if (name != _name) {
              [self willChangeValueForKey:@"name"];
              _name = name;
              [self didChangeValueForKey:@"name"];
          }
      }
      

      如果单个操作导致多个属性发生更改,则必须嵌套更改通知。如:

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

      如果集合类型的属性,则在手动触发KVO时,不仅必须指定更改的键,还必须指定更改的类型和涉及的对象的索引。如:

      - (void)removeTransactionsAtIndexes:(NSIndexSet *)indexes {
          [self willChange:NSKeyValueChangeRemoval
              valuesAtIndexes:indexes forKey:@"transactions"];
          [self didChange:NSKeyValueChangeRemoval
              valuesAtIndexes:indexes forKey:@"transactions"];
      }
      

    注册 Dependent Keys

    在许多情况下,一个属性的值依赖于另一个对象中的一个或多个其他属性的值。如果一个属性的值发生了变化,那么派生属性的值也应该被标记为发生了变化。如何确保为这些依赖属性触发键-值观察通知,取决于关系的基数。

    • 一对一情况
    1. 当属性发生更改时,手动触发派生属性的KVO。
    2. 重写keyPathsForValuesAffectingValueForKey:方法,指明派生属性依赖于哪个属性。
    3. 实现类方法keyPathsForValuesAffecting<Key>,指明派生属性依赖于哪个属性。
    4. 实现类方法keyPathsForValuesAffectingValueForKey:,指明派生属性依赖于哪个属性。
    • 一对多情况
      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中的isa-swizzling

    KVO 的实现用了一种叫isa-swizzling的技术。isa就是指向类的指针,当一个对象的一个属性注册了观察者后,被观察对象的isa就指向了一个系统为我们生成的中间类,而不是我们自己创建的类。在这个类中,系统为我们重写了被观察属性setter方法。

    • 中间类
      注册KVO观察者后,观察对象的isa指向会发生改变,指向了一个NSKVONotifying_xxx的类。带着这个观点,接下来对代码进行调试分析。
      【1】验证注册观察者之后,观察对象的isa是否会发生改变,是否会指向一个NSKVONotifying_xxx的类。

      //注册观察者之前
      (lldb) po object_getClassName(self)
      "PersonSwizzling"
      
      //注册观察者之后
      (lldb) po object_getClassName(self)
      "NSKVONotifying_PersonSwizzling"
      
      //object_getClassName
      const char *object_getClassName(id obj) {
          return class_getName(obj ? obj->getIsa() : nil);
      }
      

      通过lldb打印信息可以验证,当注册观察者之后,观察对象的isa发生了变化。并且指向了NSKVONotifying_PersonSwizzling的类。

      【2】PersonSwizzling 类和 NSKVONotifying_PersonSwizzling 类的关系

      //1. 在注册观察者之前,查看当前类的isa以及supperclass
      //读取对象地址中的内容
      (lldb) x/4g self
      0x6000038f8410: 0x0000000106a23e80 0x0000000000000000
      0x6000038f8420: 0x00007f984be06dc0 0x0000000000000000
      //通过对象的isa找到当前实例对象所指向的类
      (lldb) p/x 0x0000000106a23e80 & 0x00007ffffffffff8ULL
      (unsigned long long) $1 = 0x0000000106a23e80
      //实例对象指向的类:Person类
      (lldb) po $1
      PersonSwizzling
      //读取类地址中的内容
      (lldb) x/4g $1
      0x106a23e80: 0x0000000106a23ea8 0x00007fff86d54660
      0x106a23e90: 0x0000600002ff4740 0x0001801c00000003
      //类结构中,第一个参数是类的isa指向,此时指向的是 PersonSwizzling 元类
      (lldb) po 0x0000000106a23ea8
      PersonSwizzling
      //类结构中,第二参数是类的父类,此时指向的是 NSObject 类
      (lldb) po 0x00007fff86d54660
      NSObject
      
      注册观察者之前
      //2. 在注册观察者之后,查看当前类的isa以及supperclass
      //读取对象地址中的内容
      (lldb) x/4g self
      0x6000038f8410: 0x00006000008f81b0 0x0000000000000000
      0x6000038f8420: 0x00007f984be06dc0 0x0000000000000000
      //通过对象的isa找到当前实例对象所指向的类
      (lldb) p/x 0x00006000008f81b0 & 0x00007ffffffffff8ULL
      (unsigned long long) $5 = 0x00006000008f81b0
      //当前实例对象指向的类:NSKVONotifying_PersonSwizzling 类
      (lldb) po $5
      NSKVONotifying_PersonSwizzling
      //读取类地址中的内容
      (lldb) x/4g $5
      0x6000008f81b0: 0x00006000008f8240 0x0000000106a23e80
      0x6000008f81c0: 0x00007fff20193d20 0x0000000400000000
      //类结构中,第一个参数是类的isa指向,此时指向的是 NSKVONotifying_PersonSwizzling 元类
      (lldb) po 0x00006000008f8240
      NSKVONotifying_PersonSwizzling
      //类结构中,第二参数是类的父类,此时指向的是 PersonSwizzling 类
      (lldb) po 0x0000000106a23e80
      PersonSwizzling
      
      注册观察者之后

      由lldb中可以看出,当注册观察后,原本实例对象的isa由指向PersonSwizzling类修改成指向NSKVONotifying_PersonSwizzling类,而NSKVONotifying_PersonSwizzling类的父类PersonSwizzling类。

      【3】NSKVONotifying_PersonSwizzling 类中有什么方法?
      1、通过class_copyMethodList方法获取类的方法列表
      2、遍历方法列表,获取每个方法的SEL和IMP

      - (void)printClassAllMethod:(Class)cls{
          unsigned int outCount = 0;
          Method* methodList = class_copyMethodList(cls, &outCount);
          for(int i = 0; i<outCount; i++){
              Method method = methodList[i];
              SEL sel = method_getName(method);
              IMP imp = method_getImplementation(method);
              NSLog(@"sel:%@, imp:%p", NSStringFromSelector(sel), imp);
          }
          free(methodList);
      }
      

      通过lldb打印输出可查看到NSKVONotifying_PersonSwizzling类中方法的SEL和IMP。

      2021-03-19 15:47:34.199551+0800 KVODemo[93742:5454700] sel:setName:, imp:0x7fff207bbb57(Foundation`_NSSetObjectValueAndNotify)
      2021-03-19 15:47:39.892224+0800 KVODemo[93742:5454700] sel:class, imp:0x7fff207ba662(Foundation`NSKVOClass)
      2021-03-19 15:47:45.109027+0800 KVODemo[93742:5454700] sel:dealloc, imp:0x7fff207ba40b(Foundation`NSKVODeallocate)
      2021-03-19 15:47:50.819430+0800 KVODemo[93742:5454700] sel:_isKVOA, imp:0x7fff207ba403(Foundation`NSKVOIsAutonotifying)
      
      • setName:观察属性的setter方法
      • class:获取类的方法
      • dealloc:对象释放的方法
      • _isKVOA:判断是否自动触发KVO的方法
    • 中间类总结

      • 在注册观察者之后,实例对象的isa会指向一个由系统生成的NSKVONotifying_PersonSwizzling类,NSKVONotifying_PersonSwizzling继承自NSKVONotifying_PersonSwizzling
      • 在移除观察者之后,实例对象的isa会修改回指向PersonSwizzling
      • NSKVONotifying_PersonSwizzling类中会重写观察属性的setter方法、class方法、dealloc方法、_isKVOA方法。
      • NSKVONotifying_PersonSwizzling类一旦注册到内存中,为了考虑后续的重用问题,中间类将一直存在内存中。
    • 自定义KVO
      大概了解了KVO的过程,接下来根据了解的KVO的三步骤自定义VKO流程。

      • 注册观察者:

        1. 判断对象属性是否有Setter方法,若没有的话,则不允许继续执行KVO
        2. 动态生成子类(重点
          2.1 获取当前类的类名,拼接当前类名,生成一个新的类名HQKVONotifying_xxx
          2.2 根据新的类名,通过NSClassFromString函数获取一个类结构的指针
          2.3 使用objc_allocateClassPair完善新的类
          2.4 通过objc_registerClassPair函数将类注册至内存
          2.5 重写新类的class方法,如果没有重映射,自定的KVO在注册前后的实例对象person的class就会看到是不一致的,返回的isa更改后的类,即中间类
          2.6 重写新类的setter方法
          2.7 重写新类的delloc方法
        3. 修改实例对象的isa,使它指向新生成的类
        4. 保存当前观察者信息
      • 当观察属性被设值时

        1. 将设值的信息先发送给父类,让父类完全属性值的设置
        2. 发送通知hq_observeValueForKeyPath:ofObject:change:context:,通知观察者当前属性的值发生了变化。
      • 移除观察者

        1. 将关联对象中保存的观察者信息删掉
        2. 将实例对象的isa重新指回原来的类

    自定义KVO Demo

    本文中的示例,及自定义KVO 请见Demo地址

    相关文章

      网友评论

        本文标题:OC底层原理18 - KVO

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