一、循环引用是如何形成的?
1️⃣ ARC 的工作前提
- ARC 只做一件事:统计强引用数量
- 引用计数 > 0 → 对象存活
- 引用计数 = 0 → 对象释放
ARC 不会分析引用关系图,也不会自动打破环
2️⃣ 最简单的循环引用
class A {
var b: B?
}
class B {
var a: A?
}
let a = A()
let b = B()
a.b = b
b.a = a
引用关系:
A ──strong──▶ B
▲ │
└──strong─────┘
a和b作用域结束- 但 A、B 仍然互相强引用
- 引用计数永远 ≠ 0
- 👉 内存泄漏
二、为什么 ARC 无法处理循环引用?
因为 ARC 的模型是:
- 局部的
- 基于计数的
- 不做全局图分析
相比之下:
- GC(垃圾回收)会扫描整张对象图
- ARC 不会,也不能(性能 & 确定性)
三、如何用 weak / unowned 打破循环?
1️⃣ weak:最安全、最常用
class B {
weak var a: A?
}
特点
- 不增加引用计数
- 指向对象释放后 → 自动置
nil - 必须是 optional
📌 使用场景:
- delegate
- 父 → 子 / 子 → 父中的“反向引用”
- 生命周期不完全确定
2️⃣ unowned:性能更好,但有风险
class Child {
unowned var parent: Parent
}
特点
- 不增加引用计数
- 不会置 nil
- 对象释放后再访问 → 直接崩溃
📌 使用前提:
你 100% 确定被引用对象的生命周期 ≥ 自己
常见场景:
- child 的生命周期严格受 parent 控制
- 闭包捕获
self,且 self 生命周期必然更长
四、weak vs unowned 对比表(面试常问)
| 维度 | weak | unowned |
|---|---|---|
| 是否增加引用计数 | ❌ | ❌ |
| 是否可为 nil | ✅ | ❌ |
| 对象释放后 | 自动置 nil | 野指针 → crash |
| 是否 optional | 必须 | 不需要 |
| 性能 | 略慢 | 略快 |
| 安全性 | 高 | 低 |
五、闭包中的循环引用(高频翻车点)
1️⃣ 闭包为什么容易造成循环?
class ViewModel {
var callback: (() -> Void)?
func setup() {
callback = {
self.doSomething()
}
}
}
引用链:
ViewModel
│
▼
callback (closure)
│
▼
self (ViewModel)
👉 闭环完成,泄漏成立。
2️⃣ 用捕获列表打破
✅ weak self(推荐)
callback = { [weak self] in
self?.doSomething()
}
- 不保证 self 存在
- 安全
✅ unowned self(需谨慎)
callback = { [unowned self] in
doSomething()
}
- 性能更好
- self 被释放后调用 → crash
六、什么时候用 weak?什么时候用 unowned?
一个实用判断法:
“这个引用在对象释放后,还可能被访问吗?”
- 可能 →
weak - 绝不可能 →
unowned
面试官爱听的版本:
delegate / callback / async → weak
parent-child 强约束 → unowned(谨慎)
七、真实项目里的经典场景
1️⃣ delegate 模式
weak var delegate: UITableViewDelegate?
2️⃣ Timer
Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] _ in
self?.tick()
}
3️⃣ Combine / async Task
Task { [weak self] in
await self?.load()
}
八、一句话终极总结(建议背)
循环引用是对象之间相互强引用形成的闭环,ARC 无法自动回收;
使用 weak 或 unowned 打破强引用链,其中 weak 安全、unowned 高效但危险。