5-15.【OC】【Block】为什么 Swift 可以在很多场景下“不写 weak”,而 OC 不行?这是语言能力差异,还是 Runtime 设计差异?

2 阅读3分钟

答案是:既是语言能力的进化(编译器更聪明),也是 Runtime 管理机制的代差(更先进的元数据管理)。

在 Objective-C 中,如果你不写 weakSelf,循环引用几乎是“必死”的;但在 Swift 中,很多看似会循环引用的地方,编译器和 Runtime 都在背后默默为你“松了绑”。

以下是导致这种差异的四个核心维度:


1. 语言语义:非逃逸闭包 (Non-escaping Closures)

这是 Swift 最强大的护城河。

  • Swift 的默认行为: 在 Swift 中,闭包默认是 @nonescaping(非逃逸) 的。这意味着闭包的生命周期被严格限制在函数内部,执行完函数,闭包就销毁了。
  • 为什么不需要 weak? 既然闭包不会在函数结束后继续存在,它就不可能长期持有 self 导致环。
  • OC 的尴尬: OC 的 Block 没有“非逃逸”的编译期强制约束。编译器无法保证一个 Block 不会被异步存储,所以它只能默认采取最保守的强引用策略。

结论: 在 Swift 的 mapfiltersort 等常用方法里不写 weak 是绝对安全的,因为它们是非逃逸的。


2. 内存布局:Side Tables 与 Inline 引用计数

Swift 的引用计数管理比 OC 更加精细。

  • OC 的 SideTable: OC 所有的弱引用都存在一个全局的散列表中。由于性能开销,OC 倾向于让 Block 尽可能简单地“抓住”对象。
  • Swift 的对象头 (Object Header): Swift 对象在内存中有一个非常强大的头部。它不仅存储强引用计数,还存储 弱引用(Weak)未拥有引用(Unowned) 计数。
  • 能力差异: Swift 编译器在编译时能更精确地分析闭包的逃逸路径。如果它发现闭包虽然逃逸了,但并没有构成物理上的环(比如 DispatchQueue 这种系统持有的场景),它会优化引用逻辑,减少不必要的内存开销。

3. 语法糖:值的捕获 (Value Capture)

正如我们之前聊过的,Swift 允许你捕获而不是变量

当你在 Swift 中写 [count = self.count] in ... 时,你捕获的是那一刻的 Int 值,而不是 self 整个对象。但在 OC 中,如果你写 _count(成员变量),底层会强制捕获整个 self

这种细粒度的捕获控制,让 Swift 程序员可以轻松避开“为了一个小数据而捕获大对象”的陷阱。


4. 自动解环:逃逸分析与优化 (Escape Analysis)

Swift 编译器拥有强大的 Escape Analysis(逃逸分析) 能力:

  1. 自动提升: 如果变量只在闭包内用,且闭包不逃逸,Swift 会直接把变量留在栈上,不触发任何引用计数。
  2. 系统级解环: Swift 社区和框架(如 SwiftUI, Combine)在设计 API 时,利用了 Value Type(结构体) 占主导的特性。因为结构体没有引用计数,也就根本不存在循环引用的可能。

5. 总结:语言能力 vs Runtime 设计

维度结论说明
语言能力主导因素nonescaping 机制是 Swift 敢不写 weak 的最大底气。
Runtime 设计关键辅助更高效的 SideTable 和对象头设计,让弱引用成本更低、处理更安全。
类型系统核心差异结构体(Value Type)的普及从根源上消灭了大量引用循环。

最后的提醒

虽然 Swift 很聪明,但在以下场景你依然必须weak self

  • Delegate 闭包: 你持有这个闭包,闭包里又用了你。
  • 存储属性闭包: 闭包被你存成了变量,且内部调用了你的方法。