Swift 的 ARC (自动引用计数) 机制是如何工作的?
Swift 的 ARC (Automatic Reference Counting,自动引用计数) 是 Swift 用于管理应用程序内存使用情况的机制。
简单来说,ARC 的工作原理是:跟踪并计算有多少个变量、常量或属性指向(引用)某个类的实例。当引用计数变为 0 时,ARC 会自动释放该实例所占用的内存。
以下是 ARC 机制的详细工作流程和核心概念:
1. 基本原理:计数器
ARC 仅适用于 引用类型 (Reference Types),即 Class(类)和闭包。结构体 (Struct) 和枚举 (Enum) 是值类型,不通过 ARC 管理。
- 创建时 (+1):当你创建一个类的实例,并将其赋值给一个变量、常量或属性时,ARC 会为该实例分配内存,并将引用计数设为 1。
- 赋值时 (+1):如果你将该实例赋值给另一个变量,引用计数 +1。
- 断开时 (-1):当变量超出作用域、被赋值为
nil或指向了其他对象时,原实例的引用计数 -1。 - 销毁时 (0):当引用计数归零时,ARC 判定该实例不再被需要,立即调用该实例的
deinit方法并释放内存。
代码示例:
plaintext
class Person {
let name: String
init(name: String) { self.name = name; print("\(name) 初始化") }
deinit { print("\(name) 被销毁") }
}
var reference1: Person?
var reference2: Person?
var reference3: Person?
reference1 = Person(name: "John")
// 输出: "John 初始化"
// 引用计数: 1
reference2 = reference1
// 引用计数: 2
reference3 = reference1
// 引用计数: 3
reference1 = nil
reference2 = nil
// 引用计数: 1 (reference3 还在引用)
reference3 = nil
// 引用计数: 0
// 输出: "John 被销毁"
2. 强引用 (Strong Reference)
默认情况下,Swift 中的引用都是强引用。
- 只要强引用存在,对象就不会被销毁。
- 上面的例子中,
reference1等都是强引用。
3. 循环引用 (Retain Cycles) —— ARC 的主要挑战
如果两个对象互相持有对方的强引用,就会产生循环引用。
- 对象 A 持有对象 B。
- 对象 B 持有对象 A。
- 即使外部没有变量指向它们,它们的引用计数永远不会降为 0(至少是 1)。
- 结果:内存泄漏 (Memory Leak)。
解决办法:weak 和 unowned
为了打破循环引用,Swift 提供了两种特殊的引用类型,它们不会增加引用计数。
A. 弱引用 (weak)
- 特点:不会增加引用计数。
- 类型:必须是 Optional (可选类型) 的
var。因为对象被释放后,ARC 会自动将弱引用赋值为nil。 - 适用场景:生命周期中,被引用的对象可能会比持有者先销毁(例如:Delegate 模式)。
plaintext
class Person {
var apartment: Apartment?
}
class Apartment {
// 使用 weak 打破循环
weak var tenant: Person?
}
B. 无主引用 (unowned)
- 特点:不会增加引用计数。
- 类型:非 Optional,必须始终有值。
- 适用场景:被引用的对象生命周期 大于或等于 持有者。也就是说,当持有者存在时,被引用的对象一定存在。
- 风险:如果试图访问一个已经被释放的无主引用,程序会崩溃(类似野指针)。
plaintext
class Customer {
var card: CreditCard?
}
class CreditCard {
// 信用卡必须归属于某人,且人销毁了卡也没用了
unowned let customer: Customer
init(customer: Customer) { self.customer = customer }
}
4. 闭包中的循环引用
闭包 (Closure) 也是引用类型。如果你在闭包内部访问了 self(例如 self.someProperty),闭包会捕获 self,从而持有 self 的强引用。如果 self 也持有这个闭包,就形成了循环引用。
解决办法:捕获列表 (Capture List)
plaintext
class HTMLElement {
let name: String
let text: String?
lazy var asHTML: () -> String = { [weak self] in
// 使用 [weak self] 将 self 变为弱引用
// 在闭包内 self 变成了 Optional
guard let self = self else { return "" }
return "<\(self.name)>\(self.text ?? "")</\(self.name)>"
}
init(name: String, text: String? = nil) {
self.name = name
self.text = text
}
}
5. ARC 与 垃圾回收 (GC) 的区别
这是面试中常见的问题。Swift 的 ARC 和 Java/C# 的 Garbage Collection (GC) 不同:
| 特性 | ARC (Swift/Obj-C) | Tracing GC (Java/C#) |
|---|---|---|
| 执行时机 | 编译时 (Compile-time) 插入代码,运行时立即执行。 | 运行时 (Runtime) 不定期间歇性运行。 |
| 确定性 | 高。对象不再被使用时立即销毁。 | 低。你不知道对象何时会被回收。 |
| 性能开销 | 引用计数操作微小的 CPU 开销,平摊在运行过程中。 | GC 触发时可能会导致程序短暂停顿 (Stop-the-world)。 |
| 循环引用 | 无法自动处理,需要开发者手动使用 weak/unowned。 |
可以自动处理,GC 算法能检测到孤立的循环引用岛。 |
总结
- ARC 自动管理内存:通过计算引用数来决定何时释放对象。
- 引用计数为 0 即销毁。
- 强引用 (Strong):默认,增加计数,保持对象存活。
- 弱引用 (Weak):不增加计数,对象销毁后自动变
nil,用于打破循环引用。 - 无主引用 (Unowned):不增加计数,假定对象始终存在,用于生命周期确定的循环引用。
- 闭包捕获列表:
[weak self]或[unowned self]用于解决闭包引起的循环引用。