6-8.【OC】【KVC/KVO】KVO 底层是如何实现的?

2 阅读2分钟

KVO(Key-Value Observing)的实现是 Objective-C Runtime 动态特性的巅峰之作。它的核心机制可以概括为:运行时派生子类、方法重写(Swizzling)以及消息转发。


1. 核心原理:中间类模式 (Isa-swizzling)

当你第一次观察对象 A 的属性 name 时,Runtime 会在幕后执行以下“掉包”操作:

  1. 动态派生子类: 系统会利用 objc_allocateClassPair 动态创建一个名为 NSKVONotifying_A 的新类,该类继承自原类 A

  2. 修改 isa 指针: 将对象 Aisa 指针指向这个动态生成的子类。

    此时,对象 A 表面上还是类 A 的实例(因为 class 方法被重写了),但实质上它已经变成了子类的实例。

  3. 重写 Setter 方法: 在子类中重写被观察属性的 setter 方法。

  4. 重写辅助方法: 重写 class(隐藏子类存在)、dealloc(清理工作)和 _isKVOA(标识这是一个 KVO 代理类)。


2. 被重写的 Setter 内部逻辑

子类重写的 setter 方法并不是简单地修改变量,而是负责触发通知逻辑。以 setName: 为例,其内部实现伪代码如下:

Objective-C

- (void)setName:(NSString *)name {
    // 1. 发送 willChange 通知
    [self willChangeValueForKey:@"name"];
    
    // 2. 调用原类(父类)的实现,真正修改变量
    [super setName:name];
    
    // 3. 发送 didChange 通知
    [self didChangeValueForKey:@"name"];
}

didChangeValueForKey: 内部,系统会最终调用观察者的 -observeValueForKeyPath:ofObject:change:context: 方法。


3. KVO 的查找与触发链路

KVO 的触发并不一定非要调用 setter。如果你通过 KVC(如 setValue:forKey:)修改属性,即使没有 setter 方法,KVC 内部也会手动调用 willChangeValueForKey:didChangeValueForKey:,从而触发 KVO。

但如果你直接修改成员变量(如 _name = @"New"),KVO 是不会触发的,除非你手动调用那两个 Change 方法。


4. 关键细节:如何瞒天过海?

为了让开发者感觉不到对象的类发生了变化,NSKVONotifying_A 重写了 class 方法:

Objective-C

- (Class)class {
    // 故意返回原类的 Class 对象,隐藏动态子类的细节
    return [A class];
}

这也是为什么我们在调试时,通过 [obj class] 看到的还是原类,但通过底层的 object_getClass(obj) 却能看到 NSKVONotifying_ 前缀的真正类名。


5. KVO 的生命周期总结

阶段动作
注册观察者派生 NSKVONotifying_ 子类,修改对象 isa 指针。
属性修改触发子类 Setter \rightarrow 调用 will/didChange \rightarrow 回调观察者。
移除观察者如果不再有监听者,对象的 isa 指针可能会被改回原类。
销毁dealloc 中必须移除观察者,否则会引发野指针崩溃。

⚠️ 面试高频:手动触发 KVO

如果某个属性的变化不是通过 setter 实现的(比如是一个根据其他属性计算出来的只读属性),你可以通过以下方式手动触发:

  1. 重写 +automaticallyNotifiesObserversForKey: 并返回 NO
  2. 在值变化前后手动调用 willChangeValueForKey:didChangeValueForKey: