KVO(Key-Value Observing)看起来很简单。使用 addObserver 注册后,每当属性发生变化,就会收到通知。
相关文章 Objective-C 方法 Swizzling 完全解析 将介绍背景概念和相关应用案例。
但这不是很奇怪吗?明明没有在属性 setter 中添加通知代码,运行时怎么知道值发生了变化?
答案很大胆。添加观察者的瞬间,运行时会悄悄替换该对象的类。
这种技术称为 isa-swizzling。如果上一篇文章中的 method swizzling 是修改分发表中的“行”,这次讨论的则是替换对象所属的“类”本身。
调用 addObserver 时发生了什么
Person 为对象添加观察者后,运行时会执行以下操作。
- 动态创建名为
NSKVONotifying_Person的 子类 - 重写被观察属性的 setter,替换为在变更前后插入通知代码的版本
- 将对象的 isa 指针替换为新的子类
之后执行 person.name = @"Kim" 时,调用流程如下。
// NSKVONotifying_Person重写 setter的伪代码
- (void)setName:(NSString *)name {
[self willChangeValueForKey:@"name"];
[super setName:name]; // 执行原始 setter
[self didChangeValueForKey:@"name"]; // 在此通知观察者
}
对象的内存内容没有变化,只是所属类变了,因此现有代码完全察觉不到。移除最后一个观察者后,isa 会恢复为原始类。
类在说谎:class vs object_getClass
这里会出现一个有趣的细节。询问已添加观察者的对象的类时,可能得到两种答案。
[person class]; // Person — 谎言
object_getClass(person); // NSKVONotifying_Person — 真相
运行时还会在动态子类中重写 -class 方法,让它返回 原始类。这是为了避免开发者困惑“我的对象类为什么有这么奇怪的名字”,也是隐藏实现细节的设计。
object_getClass()会直接读取 isa 指针,因此能看到真实的类。调试时如果两个值不同,说明该对象当前正被某个观察者观察。
不经过 setter,就不会有通知
理解 isa-swizzling 的原理后,KVO 广为人知的限制就很自然了。
person.name = @"Kim"; // 产生通知 — 通过重写的 setter
person->_name = @"Kim"; // 无通知 — ivar 直接修改, setter未经过
KVO 通知的来源是重写的 setter。直接修改 ivar 会绕过这条路径,因此什么也不会发生。这也是 Objective-C 推荐使用点语法访问属性的实际原因之一,因为它内部会调用 setter。
如果必须不通过 setter 修改值,可以用手动通知包住这次操作。
[self willChangeValueForKey:@"name"];
_name = @"Kim";
[self didChangeValueForKey:@"name"];
反过来,如果想关闭某个属性的自动通知,只需在 automaticallyNotifiesObserversForKey: 中返回 NO。
实际开发中的注意事项
观察生命周期的管理仍然很重要。 在需要观察期间,必须保留基于 block 的 API 返回的 NSKeyValueObservation token(Apple KVO 文档)。
结束观察时,使 token 失效或释放它,注册也会随之清理。如果继续使用基于字符串的 API,就必须自行协调 addObserver 和 removeObserver 的生命周期(Apple KVO 文档)。
在同一个类中混用 KVO 和 method swizzling,会产生顺序问题。 如果先 swizzling 再添加 KVO,动态子类会包装已 swizzling 的 setter;反过来则可能导致双方的假设互相冲突。
两种操作运行时的技术在同一个对象上相遇时,调试难度会成倍增加。
在 Swift 中,@objc dynamic是必需的。
class Person: NSObject {
@objc dynamic var name: String = ""
}
let observation = person.observe(\.name, options: [.new]) { _, change in
print(change.newValue ?? "")
}
因为 isa-swizzling 要正常工作,setter 调用必须经过 Objective-C 消息路径。
从 Swift 4 开始使用基于 block 的 observe(_:options:changeHandler:) API。返回的 NSKeyValueObservation token 释放时,也会自动取消观察注册。Using Key-Value Observing in Swift
因此,这种结构可以避免字符串 keyPath 时代的大多数问题。
总结
- 注册观察者时,KVO 会动态创建 NSKVONotifying_ 子类并替换 isa
- 通知的来源是 重写的 setter——插入了 willChange/didChange 的版本
-class被处理为返回原始类,而真实类会显示为object_getClass()- 直接修改 ivar 会绕过 setter,因此不会发出通知——需要时使用 willChange/didChange 手动通知
- 在 Swift 中,
@objc dynamic加上基于 block 的 observe API 是标准做法,释放 token 就等同于取消注册
把 method swizzling 和 isa-swizzling 放在一起看,就能理解 Objective-C 运行时的整体图景。方法分发表和对象所属的类,都是可以在运行时替换的数据;这种灵活性正是 Cocoa 框架大多数魔法的来源。
延伸阅读
- Objective-C 方法 Swizzling 完全解析
- +load vs +initialize:调用时机与继承陷阱
- Objective-C Category 与 Swift extension 的区别(概念与注意事项总结)
来源与验证
- Using Key-Value Observing in SwiftApple · 官方文档 · 核查 2026年8月17日依据: Swift KVO 注册、变更通知与 observation token 生命周期

