内存管理是所有应用程序的基石,而在 Swift 中,理解 ARC(自动引用计数)不仅仅是为了通过面试,更是为了防止 App 在用户不知情的情况下“吃掉”所有内存并最终崩溃。
很多开发者以为 ARC 就是“自动挡”,挂上 D 挡(写代码)就不管了。其实不然,ARC 更像是一套严密的交通规则,如果你不懂红绿灯(引用类型),还是会发生车祸(内存泄漏)。
今天,我们就深入引擎盖下方,看看 ARC 到底是如何工作的,以及如何优雅地解开那些“死结”。
Swift 使用 ARC (Automatic Reference Counting) 来追踪和管理应用的内存。在大多数情况下,这对我们是透明的——你创建对象,用完它,Swift 自动回收它。完美,对吧?
但是,ARC 不是 Java 或 C# 中的“垃圾回收器 (Garbage Collector)”。它不会定期扫描内存清理垃圾,而是实时工作的。一旦引用计数归零,对象立刻销毁。
这种确定性带来了高性能,但也带来了一个致命弱点:循环引用 (Retain Cycles)。如果两个对象互相抓着对方不放,ARC 就永远无法回收它们。
今天我们就来拆解这个机制,并掌握三种“解法模式”。
1. ARC 的核心:强引用 (Strong Reference)
默认情况下,Swift 中的任何引用都是强引用。
class Person {
let name: String
init(name: String) { self.name = name }
deinit { print("\(name) 被释放了") }
}
var reference1: Person? = Person(name: "John")
// 此时: John 的引用计数 = 1
只要 reference1 还指向这个 Person 实例,它就永远不会被销毁。这就像你牵着一个气球的绳子,只要你手(强引用)不松开,气球(对象)就不会飞走(释放)。
2. 致命拥抱:循环引用 (Retain Cycle)
当两个对象互相持有对方的强引用时,问题就来了。
想象一下:Person(人)拥有 Apartment(公寓),而 Apartment 也有一个 tenant(租客)。
class Person {
let name: String
var apartment: Apartment? // 默认强引用
init(name: String) { self.name = name }
deinit { print("\(name) 释放") }
}
class Apartment {
let unit: String
var tenant: Person? // 默认强引用
init(unit: String) { self.unit = unit }
deinit { print("公寓 \(unit) 释放") }
}
var john: Person? = Person(name: "John")
var unit4A: Apartment? = Apartment(unit: "4A")
// 建立循环
john?.apartment = unit4A // John 强引用 4A
unit4A?.tenant = john // 4A 强引用 John
此时的内存图谱变成了这样:

当我们尝试释放外部变量时:
john = nil
unit4A = nil
// 控制台一片死寂... 没有打印任何 deinit 信息
虽然外部没有变量指向它们了,但它们互相指着对方。引用计数都不为 0。这就是内存泄漏。
3. 解法模式一:弱引用 (Weak Reference)
这是解决循环引用最常用的武器。
规则:
- 使用
weak关键字。 - 不增加引用计数。
- 必须是
Optional(var) 类型。因为当引用的对象被释放时,ARC 会自动把这个变量设为nil。
场景: 典型的“父子关系”或“委托模式”。父持有子(强),子持有父(弱)。
我们修改上面的 Apartment:
class Apartment {
let unit: String
// ✅ 破局关键:使用 weak
weak var tenant: Person?
init(unit: String) { self.unit = unit }
deinit { print("公寓 \(unit) 释放") }
}
现在,Apartment 对 tenant 的引用是弱引用。
当 john = nil 时,John 对象不再有强引用(Apartment 的引用不计数),John 被释放。
John 释放后,他持有的 Apartment 强引用也随之断开,于是 Apartment 也被释放。完美解套。
4. 解法模式二:无主引用 (Unowned Reference)
这是另一种更“激进”的解法。
规则:
- 使用
unowned关键字。 - 不增加引用计数。
- 不是 Optional。它假设引用的对象永远存在。
场景: 两个对象生命周期绑定,且“后者”不能独立于“前者”存在。比如:信用卡(CreditCard)必须属于一个客户(Customer),没有客户就没有卡。
class Customer {
let name: String
var card: CreditCard?
init(name: String) { self.name = name }
deinit { print("\(name) 释放") }
}
class CreditCard {
let number: UInt64
// ✅ 破局关键:使用 unowned
// 卡片存在时,客户一定存在,所以不需要是 Optional
unowned let customer: Customer
init(number: UInt64, customer: Customer) {
self.number = number
self.customer = customer
}
deinit { print("卡片 \(number) 释放") }
}
⚠️ 风险提示: 如果你在 customer 被释放后,试图访问 card.customer,程序会直接 Crash。这有点像“悬垂指针”。所以,除非你非常确定生命周期,否则优先用 weak。
5. 解法模式三:闭包捕获列表 (Capture List)
这是 Swift 开发中最容易踩的坑。闭包(Closure)是引用类型,如果闭包赋值给了类的属性,而闭包内部又捕获了 self,就会形成 self -> closure -> self 的循环引用。
错误示范:
class HTMLElement {
let name: String
let text: String?
lazy var asHTML: () -> String = {
// ❌ 闭包强引用了 self
if let text = self.text {
return "<\(self.name)>\(text)</\(self.name)>"
} else {
return "<\(self.name) />"
}
}
// ... init ...
}
**正确解法:[weak self] + guard**
我们在闭包的定义处使用捕获列表来打破循环。
lazy var asHTML: () -> String = { [weak self] in
// 此时 self 是 weak 的,变成了 Optional
// ✅ 标准写法:Weak-Strong Dance
guard let self = self else { return "" }
return "<\(self.name)>\(self.text ?? "")</\(self.name)>"
}
面试题:什么时候在闭包里用 unowned self?
只有当闭包和 self 的生命周期完全一致,且闭包不可能在 self 销毁后被调用时(比如某些非逃逸闭包,或者特定的 lazy 属性初始化)。但为了安全起见,工程实践中 99% 的情况建议使用 weak self。
总结
Swift 的 ARC 是一套优雅的内存管理机制,但它需要开发者的配合。
| 引用类型 | 关键字 | 引用计数 | 是否 Optional | 变为空时行为 | 适用场景 |
|---|---|---|---|---|---|
| 强引用 | (默认) | +1 | 是/否 | 保持对象存活 | 所有的所有权关系 |
| 弱引用 | weak |
+0 | 必须是 | 自动变为 nil | Delegate, 闭包, 可空反向引用 |
| 无主引用 | unowned |
+0 | 否 | 导致 Crash | 相互依赖且生命周期绑定的非空引用 |
记住这句口诀:“层级向下用 Strong,层级向上用 Weak,生死与共用 Unowned。”
感谢阅读!你在项目中遇到过最难调试的内存泄漏是什么样的?是用 Instruments 查出来的,还是看代码看出来的?欢迎在评论区分享你的“捉虫”经历!