iOS KVO KVC 知识点

0 阅读3分钟

🎯 KVO:动态子类化与 ISA 交换

KVO (Key-Value Observing) 的本质,是系统在运行时动态创建子类,并通过 ISA 交换(ISA-Swizzling)  技术,让被观察对象“偷偷”变成这个动态子类的实例,从而在属性 setter 方法中插入通知逻辑

  1. 动态创建子类:当为对象 person 的属性 name 注册第一个观察者时,系统会在运行时动态创建一个名为 NSKVONotifying_Person 的子类,并让它继承自 Person 类

  2. 重写核心方法:在这个动态子类中,系统会重写一系列关键方法

    • 重写被观察属性的 Setter:这是最核心的一步。重写后的 setName: 方法,其内部实现逻辑如下

      objc

      // 伪代码示例
      - (void)setName:(NSString *)newName {
          [self willChangeValueForKey:@"name"]; // 1. 通知观察者:值即将改变
          [super setName:newName];              // 2. 调用父类(原类)的setter,真正修改值
          [self didChangeValueForKey:@"name"];  // 3. 通知观察者:值已经改变
      }
      
    • 重写 class 方法:为了让外部代码(如调试器)以为对象还是原来的 Person 类,动态子类会重写 class 方法,返回 [Person class],从而隐藏其真实类型

    • 重写 dealloc 方法:用于在对象销毁时清理 KVO 相关的资源

  3. 执行 ISA 交换:将原 person 对象的 isa 指针,从指向 Person 类,修改为指向这个新创建的 NSKVONotifying_Person 类

  4. 触发通知流程:当后续修改 person.name 时,会调用动态子类被重写的 setName: 方法,从而自动触发 willChangeValueForKey: 和 didChangeValueForKey:。在 didChangeValueForKey: 内部,会调用观察者的 observeValueForKeyPath:ofObject:change:context: 方法,将新旧值传递给观察者

重要注意事项:KVO 的本质是监听 setter 方法,因此直接修改成员变量(即使用 _name = newName不会触发 KVO

🔑 KVC:按模式搜索的访问机制

KVC (Key-Value Coding) 的核心原理是标准的搜索模式。当调用 setValue:forKey: 或 valueForKey: 时,系统会按照一套预定义的、可预测的规则来搜索并完成操作。

setValue:forKey: 的底层执行逻辑

  1. 查找 Setter 方法
    优先查找 set<Key>:,其次查找 _set<Key>:。一旦找到,直接调用该方法并结束流程。

  2. 检查访问权限
    若未找到 Setter 方法,KVC 会检查 accessInstanceVariablesDirectively 方法的返回值(默认为 YES)。

    • 若为 YES,允许直接访问成员变量(Ivar)。
    • 若为 NO,则直接进入第4步。
  3. 查找成员变量
    按以下顺序查找成员变量,一旦找到,直接赋值并结束流程

    1. _<key>
    2. _is<Key>
    3. <key>
    4. is<Key>
  4. 异常处理
    若上述步骤均未找到,最后会调用 setValue:forUndefinedKey:,该方法默认会抛出 NSUndefinedKeyException 异常

valueForKey: 的底层执行逻辑

  1. 查找 Getter 方法
    按以下顺序查找方法,找到则调用并返回值。

    1. get<Key>
    2. <key>
    3. is<Key>
    4. _<key>
  2. 查找集合方法
    如果第一步未找到,KVC 会检查属性是否是集合类型。它会查找 countOf<Key> 和 objectIn<Key>AtIndex: 等方法,如果找到,则返回一个可以响应 NSArray 方法的代理对象。

  3. 查找成员变量
    若前两步均未找到,KVC 会直接查找成员变量(如 _<key><key>),找到则直接返回其值。

  4. 异常处理
    若所有步骤都失败,最后会调用 valueForUndefinedKey:,该方法默认抛出异常。

🗺️ KVO 与 KVC 的关系

KVO 和 KVC 紧密协作。KVC 提供了通过字符串 key 访问属性的能力,而 KVO 正是依赖于此。最直接的证明就是:通过 KVC 修改属性(如 [person setValue:@"New Name" forKey:@"name"]),会完整地触发 KVO 通知。因为 KVC 在内部执行时,依然会经过 setter 方法的查找和调用流程。