Swift 的 KVO 让人感到“别扭”,本质是因为 KVO 是 Objective-C Runtime 的强行产物,而 Swift 是一门强类型、静态且追求内存安全的现代语言。
这种冲突体现在机制底层的截然不同。以下是 Swift KVO 让人感到难用的几个核心深层原因:
1. 静态语言对动态特性的“排斥”
KVO 的底层核心是 isa-swizzling(动态派生子类)。
-
OC 的做法: 只要有属性,Runtime 就能动态地在运行时给你塞一个子类。
-
Swift 的别扭点: Swift 默认是静态分发的(Static Dispatch),为了让 KVO 起作用,你必须强制把 Swift 属性“降级”为 OC 风格。你必须同时使用:
@objc:暴露给 Runtime。dynamic:禁用 Swift 的静态优化,强制走消息分发。
结果: 你写的是 Swift,但为了 KVO,你必须手动拆掉 Swift 所有的性能优化和安全检查。
2. “引用类型”的软肋
KVO 只能作用于 Class(类) 。
- Swift 的现状: Swift 的灵魂是 Value Types (Structs) 。你会发现你最常用的数据模型(Struct)完全无法使用 KVO。
- 别扭点: 当你想观察一个 Struct 模型的变化时,你会发现这条路被堵死了。这种“二等公民”的待遇让你在构建响应式代码时感到严重的断层。
3. 类型安全与 API 的进化史
在 Swift 4 之前,API 几乎完全照搬 OC,体验极差。现在的 API 虽然好了一些,但依然留下了“嫁接”的痕迹。
曾经的“阴影”(老版本):
Swift
// 必须重写 observeValue(forKeyPath:of:change:context:)
// 然后用一堆 if-else 和强制类型转换来处理不同的 keyPath
现在的“妥协”(新版本):
虽然有了基于 KeyPath 的闭包 API,但它依赖于特殊的 \Sub.property 语法。
Swift
// 虽然好用了,但背后依然是复杂的泛型包装
observation = observed.observe(.name, options: [.new]) { object, change in
print(change.newValue)
}
4. 观察者的生命周期管理
在 OC 中,KVO 的 removeObserver 是手动管理的,一旦忘了就会崩溃(Crash)。
- Swift 的做法: Swift 通过返回一个
NSKeyValueObservation令牌对象来管理生命周期。当这个对象销毁时,自动注销观察。 - 别扭点: 这意味着你必须在你的类里专门开辟一个属性来“存”这个令牌。如果你只是想简单监听一下,却得额外管理一个变量的生命周期,这显得非常繁琐。
5. Swift 有更好的替代方案
开发者觉得别扭,是因为 Swift 提供了更符合其直觉的工具,显得 KVO 像个“老古董”:
-
Property Observers (
didSet/willSet) :这是 Swift 原生的。简单、安全、不依赖 Runtime,直接写在属性定义里。
-
Combine / Swift Data:
@Published或Observable宏。这些是编译器级别的支持,完全不需要@objc dynamic,且性能更高。
总结:为什么要留着它?
KVO 这么别扭,为什么不删了?
因为 Cocoa 框架(如 UIKit)依然是基于 OC 构建的。 当你需要监听 UITextField 的文字变化、UIScrollView 的 contentOffset 或 AVPlayer 的状态时,你避不开 KVO。
| 方案 | 推荐程度 | 适用场景 |
|---|---|---|
didSet | ⭐⭐⭐⭐⭐ | 内部属性逻辑触发 |
| Combine / Observable | ⭐⭐⭐⭐ | 现代 UI 状态绑定 |
| Swift KVO | ⭐⭐ | 仅用于监听系统框架(UIKit/AVFoundation)的对象 |
Swift 5.9 之后推出的新一代“观察者”宏 @Observable 是如何彻底终结这种别扭感的吗?
1. 为什么它能终结“别扭感”?
它解决了 KVO 在 Swift 中的三大痛点:
- 不再需要
NSObject: 你的类不需要继承自任何父类,就是一个纯粹的 Swift Class。 - 不再需要
@objc dynamic: 它不再依赖 Objective-C 派生子类,而是通过代码静态生成来实现。 - 属性感知更自然: 你不需要手动管理
observation令牌。只要在视图里用了这个属性,系统就自动知道要“观察”它。
2. 底层实现原理:编译器“魔法”
当你写下 @Observable 时,Swift 编译器在编译阶段会帮你把代码重写。
你的原始代码:
Swift
@Observable class User {
var name = "Gemini"
}
编译器展开后的样子(简化逻辑):
编译器会自动为你生成一个特殊的 _$observationRegistrar 对象,并重写属性的 get 和 set:
- Get 拦截: 当你读取
name时,它会调用access(self, .name),悄悄把当前正在运行的 UI 闭包记录下来。 - Set 拦截: 当你修改
name时,它会调用withMutation(self, .name),然后通知之前记录的所有 UI 闭包进行刷新。
3. KVO 与 @Observable 的代差对比
| 特性 | Swift KVO (@objc dynamic) | 现代 Observation 宏 |
|---|---|---|
| 基础依赖 | 必须继承 NSObject | 纯 Swift 类即可 |
| 属性要求 | 必须加修饰符,性能损耗大 | 无需额外修饰符,性能极高 |
| 观察范围 | 显式注册 KeyPath | 自动追踪(只要读了就自动监听到) |
| 解绑逻辑 | 必须存个 observation 对象 | 全自动(基于作用域,无需管理生命周期) |
4. 真正强大的地方:依赖自动追踪
KVO 最让人头疼的是“级联监听”(监听 A 变化导致 B 刷新)。
在 @Observable 下,你甚至可以写出这样的代码:
Swift
@Observable class Order {
var items: [String] = []
var itemCount: Int { items.count } // 计算属性也能自动被观察!
}
原理: 因为 itemCount 的 get 方法里读取了 items,编译器会自动建立它们之间的依赖链。只要 items 变了,监听 itemCount 的 UI 就会自动刷新。这在 KVO 时代需要写大量冗长的 keyPathsForValuesAffecting...。
5. 什么时候该用哪个?
- 监听系统对象(如
AVPlayer、UIScrollView): 别无选择,继续用 KVO。 - 自己的业务模型(SwiftUI 交互): 永远优先选择
@Observable。 - 非 UI 的简单回调: 使用
didSet或普通的闭包。
最后的“槽点”
虽然 @Observable 极度优雅,但它有一个硬伤:它目前仅支持 iOS 17+。 如果你的项目还需要兼容老版本,你可能还得在“别扭”的旧式响应式代码中再坚持一段时间。