KVO(Key-Value Observing)的实现是 Objective-C Runtime 动态特性的巅峰之作。它的核心机制可以概括为:运行时派生子类、方法重写(Swizzling)以及消息转发。
1. 核心原理:中间类模式 (Isa-swizzling)
当你第一次观察对象 A 的属性 name 时,Runtime 会在幕后执行以下“掉包”操作:
-
动态派生子类: 系统会利用
objc_allocateClassPair动态创建一个名为NSKVONotifying_A的新类,该类继承自原类A。 -
修改 isa 指针: 将对象
A的isa指针指向这个动态生成的子类。此时,对象
A表面上还是类A的实例(因为class方法被重写了),但实质上它已经变成了子类的实例。 -
重写 Setter 方法: 在子类中重写被观察属性的
setter方法。 -
重写辅助方法: 重写
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 调用 will/didChange 回调观察者。 |
| 移除观察者 | 如果不再有监听者,对象的 isa 指针可能会被改回原类。 |
| 销毁 | dealloc 中必须移除观察者,否则会引发野指针崩溃。 |
⚠️ 面试高频:手动触发 KVO
如果某个属性的变化不是通过 setter 实现的(比如是一个根据其他属性计算出来的只读属性),你可以通过以下方式手动触发:
- 重写
+automaticallyNotifiesObserversForKey:并返回NO。 - 在值变化前后手动调用
willChangeValueForKey:和didChangeValueForKey:。