循环引用
问题
什么是循环引用?有哪些常见场景?如何解决?
答案
什么是循环引用
两个或多个对象相互持有强引用,导致引用计数永远无法归零,内存泄漏。
场景一:delegate 循环
// ❌ delegate 强引用 → 循环
class ViewController: UIViewController {
let tableView = UITableView()
override func viewDidLoad() {
tableView.delegate = self // tableView → strong → self
// self → strong → tableView(已经通过 view 持有)
}
}
// ✅ UIKit 的 delegate 声明为 weak
weak var delegate: UITableViewDelegate?
场景二:闭包捕获 self
class ViewModel {
var name = "Alice"
var onUpdate: (() -> Void)?
func setup() {
// ❌ 闭包强捕获 self → self 持有 onUpdate → 循环
onUpdate = {
print(self.name)
}
// ✅ 使用 capture list
onUpdate = { [weak self] in
guard let self else { return }
print(self.name)
}
}
deinit { print("ViewModel deinit") }
}
场景三:Timer 循环
class ViewController: UIViewController {
var timer: Timer?
override func viewDidLoad() {
// ❌ Timer 强持有 target(self)
timer = Timer.scheduledTimer(timeInterval: 1, target: self,
selector: #selector(tick), userInfo: nil, repeats: true)
}
// ✅ 方案1:使用 block API + [weak self]
func setupTimer() {
timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] _ in
self?.tick()
}
}
// ✅ 方案2:viewWillDisappear 中 invalidate
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
timer?.invalidate()
}
deinit { timer?.invalidate() }
}
weak vs unowned
| weak | unowned | |
|---|---|---|
| 类型 | Optional(自动置 nil) | 非 Optional |
| 释放后访问 | 安全(nil) | 崩溃(EXC_BAD_ACCESS) |
| 性能 | 略低(Side Table 管理) | 略高 |
| 使用场景 | 生命周期不确定 | 确定不会先于自身释放 |
// unowned 适合:parent-child 关系,child 不可能比 parent 活得久
class Customer {
var card: CreditCard?
}
class CreditCard {
unowned let customer: Customer // 信用卡不可能脱离客户存在
init(customer: Customer) { self.customer = customer }
}
使用 unowned 的风险
如果被引用对象先于当前对象释放,访问 unowned 引用会直接崩溃。不确定时,使用 weak 更安全。
闭包中 [weak self] 的常见模式
// 模式 1:guard let(推荐)
closure = { [weak self] in
guard let self else { return }
self.doSomething()
}
// 模式 2:可选链
closure = { [weak self] in
self?.doSomething()
}
// 模式 3:withExtendedLifetime(防止执行中途释放)
closure = { [weak self] in
guard let self else { return }
withExtendedLifetime(self) {
self.step1()
self.step2() // 确保 step1 和 step2 期间 self 不被释放
}
}
常见面试问题
Q1: 所有闭包都需要 [weak self] 吗?
答案:不是。只有当闭包被 self 长期持有(存储属性、全局变量)时才会循环引用。以下情况不需要 [weak self]:
UIView.animate闭包(系统临时持有)DispatchQueue.main.async闭包(短暂持有)- 非逃逸闭包
Q2: 如何检测循环引用?
答案:
- Memory Graph Debugger:Xcode 调试时点击内存图按钮,紫色警告标记泄漏
- Instruments - Leaks:自动检测运行时内存泄漏
- deinit 打印:在
deinit中添加 print,确认对象正常释放
Q3: NotificationCenter 需要 [weak self] 吗?
答案:iOS 9+ 的 addObserver(forName:) block API 中建议使用 [weak self],因为返回的 observer token 如果被 self 持有就可能循环。addObserver(_:selector:) 在 iOS 9+ 虽然不再强持有 observer,但最好仍在 deinit 中 removeObserver。