跳到主要内容

循环引用

问题

什么是循环引用?有哪些常见场景?如何解决?

答案

什么是循环引用

两个或多个对象相互持有强引用,导致引用计数永远无法归零,内存泄漏。

场景一: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

weakunowned
类型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: 如何检测循环引用?

答案

  1. Memory Graph Debugger:Xcode 调试时点击内存图按钮,紫色警告标记泄漏
  2. Instruments - Leaks:自动检测运行时内存泄漏
  3. 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。

相关链接