6-13.【OC】【KVC/KVO】Swift 的 KVO 为什么这么别扭?

5 阅读5分钟

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 像个“老古董”:

  1. Property Observers (didSet / willSet)

    这是 Swift 原生的。简单、安全、不依赖 Runtime,直接写在属性定义里。

  2. Combine / Swift Data

    @PublishedObservable 宏。这些是编译器级别的支持,完全不需要 @objc dynamic,且性能更高。


总结:为什么要留着它?

KVO 这么别扭,为什么不删了?

因为 Cocoa 框架(如 UIKit)依然是基于 OC 构建的。 当你需要监听 UITextField 的文字变化、UIScrollViewcontentOffsetAVPlayer 的状态时,你避不开 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 对象,并重写属性的 getset

  1. Get 拦截: 当你读取 name 时,它会调用 access(self, .name),悄悄把当前正在运行的 UI 闭包记录下来。
  2. 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 } // 计算属性也能自动被观察!
}

原理: 因为 itemCountget 方法里读取了 items,编译器会自动建立它们之间的依赖链。只要 items 变了,监听 itemCount 的 UI 就会自动刷新。这在 KVO 时代需要写大量冗长的 keyPathsForValuesAffecting...


5. 什么时候该用哪个?

  • 监听系统对象(如 AVPlayerUIScrollView): 别无选择,继续用 KVO
  • 自己的业务模型(SwiftUI 交互): 永远优先选择 @Observable
  • 非 UI 的简单回调: 使用 didSet 或普通的闭包。

最后的“槽点”

虽然 @Observable 极度优雅,但它有一个硬伤:它目前仅支持 iOS 17+。 如果你的项目还需要兼容老版本,你可能还得在“别扭”的旧式响应式代码中再坚持一段时间。