Strong、Weak、Unowned 引用在 Swift 中的具体实现和区别?
在 Swift 中,Strong、Weak 和 Unowned 是自动引用计数(ARC)管理内存的三种主要方式。理解它们的区别以及底层的实现机制(特别是 Side Table 的概念)是掌握 Swift 内存管理的关键。
以下是详细的深度解析:
1. Strong 引用 (强引用)
概念
这是 Swift 的默认引用类型。只要有一个强引用指向一个对象,该对象就不会被销毁。
具体实现
- 引用计数 (Reference Counting):
- Swift 的对象在堆(Heap)上的内存布局通常以
HeapObject结构开始,其中包含Metadata(元数据指针)和RefCounts(引用计数)。 - 当创建一个强引用时,运行时会调用
swift_retain,将该对象的refCounts中的强引用计数 +1。 - 当强引用离开作用域或被置为
nil时,调用swift_release,计数 -1。 - 当强引用计数降为 0 时,对象会被销毁(调用
deinit)。
- Swift 的对象在堆(Heap)上的内存布局通常以
区别与问题
- 默认行为: 不需要特殊关键字。
- 循环引用 (Retain Cycle): 如果对象 A 强引用 B,B 也强引用 A,两者的计数永远不会归零,导致内存泄漏。
2. Weak 引用 (弱引用)
概念
弱引用不会增加对象的“强引用计数”。如果一个对象只剩下弱引用,它会被销毁,并且所有的弱引用会自动变为 nil。
语法特征
- 必须是
Optional类型 (var object: MyClass?)。 - 必须是
var,因为它的值会在运行时改变。
具体实现 (核心机制:Side Table)
这是面试中的高频考点。Swift 的弱引用实现经历过优化,现代 Swift (4.0+) 使用了 Side Table (散列表/辅助表) 机制。
RefCounts 的变化:
- 对象的
RefCounts字段是一个 64 位的位域。 - 当一个对象第一次被
weak引用指向时,运行时会创建一个 Side Table。 - 原本存储在对象头部的引用计数,会被替换为一个指向 Side Table 的指针(同时设置一个标志位,表示现在使用了 Side Table)。
- 对象的
Side Table 结构:
- Side Table 独立于对象内存存在。它存储了:
- Strong Count (强引用计数)
- Weak Count (弱引用计数)
- Unowned Count (无主引用计数)
- 指向原对象的指针
- Side Table 独立于对象内存存在。它存储了:
弱引用的指向:
- Weak 引用并不直接指向对象内存,而是指向这个 Side Table。
置 nil 的过程:
- 当对象的强引用计数归零,对象调用
deinit并销毁内存。 - 但是,Side Table 不会立即销毁,因为弱引用还指向它。
- 当我们在代码中访问这个
weak变量时,运行时会通过 Side Table 检查对象是否存活。 - 如果对象已销毁(Strong Count 为 0),运行时会将这个弱引用变量写入
nil。
- 当对象的强引用计数归零,对象调用
3. Unowned 引用 (无主引用)
概念
无主引用也不增加强引用计数。但它假定对象在引用期间一直存在。如果对象被销毁后,你尝试访问 unowned 引用,程序会崩溃(Crash)。
语法特征
- 通常是非 Optional 类型 (
unowned let/var object: MyClass)。 - 不需要解包。
具体实现 (Safe vs Unsafe)
Swift 中的 unowned 其实分为两种:unowned(safe)(默认)和 unowned(unsafe)。
Unowned (Safe) - 默认:
- 实现: 同样利用了引用计数机制(在 Side Table 或 Inline RefCounts 中维护一个
Unowned Count)。 - 生命周期: 当强引用归零,对象销毁(
deinit),但对象的内存可能还没完全释放(变为“僵尸”状态),直到 Unowned 引用计数也归零。 - 检查: 当你访问
unowned引用时,Swift 运行时会检查对象是否已被销毁。如果已销毁,运行时会触发swift_abortRetainUnowned,导致程序崩溃(Trap)。这是一种安全机制。
- 实现: 同样利用了引用计数机制(在 Side Table 或 Inline RefCounts 中维护一个
Unowned (Unsafe):
- 实现: 类似于 C/C++ 的原始指针(Raw Pointer)或 Objective-C 的
__unsafe_unretained。 - 行为: 它直接指向内存地址,不进行任何引用计数检查。
- 风险: 如果对象销毁了,它就变成了悬垂指针 (Dangling Pointer)。访问它可能读到垃圾数据,也可能崩溃,行为不可预测。性能极高,但极不安全。
- 实现: 类似于 C/C++ 的原始指针(Raw Pointer)或 Objective-C 的
总结与对比
| 特性 | Strong | Weak | Unowned (Safe) |
|---|---|---|---|
| 引用计数 | 强引用计数 +1 | 弱引用计数 +1 (在 Side Table) | 无主引用计数 +1 |
| 对象销毁 | 阻止销毁 | 不阻止销毁 | 不阻止销毁 |
| 变为 nil | 否 | 是 (自动变为 nil) | 否 (保持原有值) |
| 类型要求 | 任意 | 必须是 Optional var | 通常是非 Optional |
| 访问已销毁对象 | 不可能 (对象活着) | 返回 nil | 运行时崩溃 (Crash) |
| 底层指向 | 直接指向对象 | 指向 Side Table | 指向对象 (但在访问时检查状态) |
| 适用场景 | 默认引用,拥有关系 | 代理 (Delegate),可能为空的引用 | 相互引用且生命周期一致 (如信用卡与客户) |
对象的生命周期 (结合 Side Table)
理解 Side Table 后,一个 Swift 对象的完整生命周期如下:
- Live (存活): Strong Count > 0。
- Deiniting (正在析构): Strong Count = 0,
deinit开始执行。 - Deinited (已析构):
deinit完成。此时weak引用访问会得到nil。但如果还有unowned引用,对象的内存头(包含引用计数的部分)还保留着。 - Freed (内存释放): Unowned Count = 0。对象本身的内存被完全释放。但如果还有
weak引用,Side Table 依然存在。 - Dead (彻底死亡): Weak Count = 0。Side Table 被释放。
面试回答建议 (Key Takeaways)
- Strong 是默认,造成循环引用。
- Weak 必须是 Optional,不持有对象,底层通过 Side Table 实现,对象销毁后自动置 nil。
- Unowned 不持有对象,不置 nil,访问已销毁对象会 Crash。
- 选择建议:
- 如果引用对象的生命周期比当前对象短(可能先死),用
weak。 - 如果引用对象的生命周期 >= 当前对象(同生共死),用
unowned(例如:闭包捕获self,且确定闭包执行时self一定还在)。
- 如果引用对象的生命周期比当前对象短(可能先死),用