iOS 底层原理:KVO (Key-Value Observing)

1. 实现机制:ISA Swizzling

KVO 是通过 Runtime 动态生成子类实现的。

[obj addObserver:...] 被调用时:

  1. 动态子类化:Foundation 通常为原类动态生成一个类似 NSKVONotifying_OriginalClass 的子类。这个命名和具体实现属于私有细节,只适合解释现象,不应作为业务逻辑依赖。
  2. 修改 ISA:将 objisa 指针指向这个新生成的中间类。
  3. 重写 Setter:在子类中重写被观察属性的 setter 方法。

2. Setter 重写逻辑

重写后的 setter 伪代码大致如下:

- (void)setName:(NSString *)name {
    [self willChangeValueForKey:@"name"];
    
    // 调用父类(原类)的 setter 实现
    // 或者是通过 handle 获取原 setter imp
    [super setName:name]; 
    
    [self didChangeValueForKey:@"name"];
}

didChangeValueForKey 内部会触发 observeValueForKeyPath 回调。

3. 其它覆盖的方法

除了 setter,NSKVONotifying_xxx 类还重写了:

  • class:返回父类的 Class 对象。

    • 目的:欺骗开发者。调用 [obj class] 依然返回 OriginalClass,隐藏 KVO 的底层实现。
  • dealloc:参与 KVO 状态清理;具体覆盖集合会随系统实现变化。

  • _isKVOA:一些系统版本中可见的私有标记方法。它不是公开 API,不要在业务代码中调用。

4. 面试题:手动触发 KVO?

可以。

  • 关闭自动触发:+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key 返回 NO。

  • 手动调用:

[self willChangeValueForKey:@"name"];
_name = name;
[self didChangeValueForKey:@"name"];

willChange...didChange... 必须成对、正确嵌套。异常路径漏掉 didChange... 会破坏通知状态,因此手写通知时要尽量缩小两者包围的代码范围。

5. 哪些修改能够自动触发

KVO 依赖符合 KVC 约定的变更入口。通过属性 setter 修改值通常可以自动触发;直接写 ivar 会绕过 setter,一般不会自动通知。

self.name = @"new"; // 通常触发
_name = @"new";     // 直接写 ivar,通常不触发自动 KVO

对 to-many 关系,不要直接取出可变数组后随意修改并期待完整的增删语义通知。可以使用 KVC 提供的 mutable proxy,例如:

[[self mutableArrayValueForKey:@"items"] addObject:item];

如果一个计算属性依赖其他 key,可以实现 +keyPathsForValuesAffectingValueForKey:,或为固定关系提供 +keyPathsForValuesAffectingFullName 这一类方法,让依赖属性变化时也产生通知。

6. Observer 管理与 Context

传统 KVO 回调会把多个观察关系汇聚到同一个方法。判断时优先使用唯一 context,而不是只比较字符串 keyPath:

static void *UserNameContext = &UserNameContext;

[user addObserver:self
       forKeyPath:@"name"
          options:NSKeyValueObservingOptionInitial |
                  NSKeyValueObservingOptionNew |
                  NSKeyValueObservingOptionOld
          context:UserNameContext];

回调中先判断 context;不是自己处理的观察关系,应交给 super。观察注册和移除要保持对称。较新的系统 API 与 Swift 的 observation token 能降低管理成本,但仍要理解 token 由谁持有、何时失效。

7. 常见误区

  • KVO 不是 Method Swizzling 的同义词:两者都利用 Runtime 动态能力,但经典 KVO 的关键是动态子类化和 isa 指向变化。
  • object_getClass(obj)[obj class] 结果可能不同:前者能看到对象实际 isa 指向的动态子类,后者通常被覆写成返回原类。
  • 不是所有属性都天然 KVO compliant:自定义访问器、直接 ivar 写入、集合内部变化、非 KVC 兼容模型都需要单独判断。
  • 不要依赖私有类名和私有方法:理解它们是为了建立模型,不是为了在线上代码里检测或操纵 KVO。