当你在 Objective-C 中调用 addObserver:forKeyPath:options:context: 时,Runtime 并不仅仅是做个登记,而是通过 isa-swizzling 技术对被观察对象的类结构进行了一场“动态手术”。
以下是 Runtime 内部执行的精密步骤:
1. 动态派生 KVO 子类 (NSKVONotifying_...)
Runtime 首先检查被观察对象 obj 的类 A。如果该对象是第一次被观察,Runtime 会利用 objc_allocateClassPair 动态创建一个名为 NSKVONotifying_A 的新类。
- 继承关系: 这个新类继承自原类
A。 - 命名规律: 始终以
NSKVONotifying_作为前缀。
2. 修改对象的 isa 指针
这是 KVO 最核心的“调包”动作。Runtime 会将对象 obj 的 isa 指针指向这个新创建的子类。
- 结果: 该对象从此变成了一个派生类的实例。当你对这个对象调用方法时,系统会优先在派生类中查找。
3. 在子类中重写被观察属性的 Setter
Runtime 会在新类中重写对应属性的 setter 方法(例如 setName:)。这个重写的 setter 是通知分发的关键,其内部实现通常调用了 _NSSetObjectValueAndNotify 等函数,逻辑如下:
- 调用
willChangeValueForKey:。 - 调用父类(原类)的真正
setter实现。 - 调用
didChangeValueForKey:(该方法内部会触发观察者的回调)。
4. 重写辅助方法以“瞒天过海”
为了让开发者感觉不到对象已经变了,Runtime 会在子类中重写几个关键方法:
-class: 内部实现返回原类的Class对象。这让你在调试时调用[obj class]依然看到的是A。-dealloc: 负责在对象销毁时进行一些内部清理工作。-_isKVOA: 这是一个私有标志位方法,返回YES,用于 Runtime 内部快速判断一个对象是否正在被监听。
5. 存储观察信息
Runtime 会将被观察的 KeyPath、观察者(Observer)指针、配置项(Options)以及 Context 存储在对象的一个私有内部存储区域(通常是关联对象或全局的哈希表)。
总结对照:注册前后的变化
| 特性 | 注册 KVO 前 | 注册 KVO 后 |
|---|---|---|
| isa 指针 | 指向原类 A | 指向动态子类 NSKVONotifying_A |
| Setter 方法 | 原类实现(直接修改内存/变量) | 子类重写实现(包装了通知发送逻辑) |
| class 方法 | 返回 A | 返回 A (被重写以隐藏子类) |
| 运行时类名 | A | NSKVONotifying_A (可通过 object_getClass 发现) |