Retain Cycle(循环引用)—— ARC 中两个或更多对象通过强引用相互引用,形成闭环的情况。根据 Apple Memory Management Guide, 2026,retain cycle 阻止了循环中所有对象的释放,因为每个对象都有 ≥ 1 的 retain count。与 GC 中的内存泄漏不同,retain cycle 保证只要循环中至少有一个外部参与者存活,对象就会保持存活——甚至在所有外部引用丢失之后,如果循环是隔离的也是如此。
要点
Retain Cycle——是两个或更多对象通过强引用相互持有,形成封闭依赖图的情况。ARC 无法释放这些对象中的任何一个,因为每个对象的 retain count 始终 ≥ 1:对象 A 持有 B,B 持有 A,它们的计数器永远不会归零。
这个问题只出现在具有引用计数的系统(ARC、MRR)中。在垃圾回收(GC)中,收集器根据从根集合(root set)出发的引用图来确定不可达性——循环不是障碍。然而在 ARC 中,循环等同于泄漏,因为基于计数器的确定性释放无法解决循环依赖。
根据 WWDC 2012 Session 406,retain cycle 是 Objective-C 和 Swift 应用中最常见的内存泄漏原因。典型场景:带有委托的父子关系、捕获 self 的闭包以及具有双向连接的层次架构。
让我们看看每个 iOS 开发者都会遇到的经典 retain cycle 场景。理解这些模式是编写安全代码与 ARC 的基础。
经典场景:父对象(例如 UIViewController)创建一个子对象并成为其委托。如果两者都使用强引用,就会产生 retain cycle。解决方案——委托应该是 weak。
// 错误:通过 strong delegate 导致的 retain cycle
protocol ChildDelegate: AnyObject { }
class ParentVC: UIViewController, ChildDelegate {
var child: ChildVC?
func showChild() {
child = ChildVC()
child?.delegate = self // Parent → Child (strong)
} // Child → Parent (通过 delegate 的 strong)
} // ⚠️ Retain cycle!
class ChildVC: UIViewController {
var delegate: ChildDelegate? // ❌ 默认为 strong
}
// 修复:weak delegate
class ChildVC: UIViewController {
weak var delegate: ChildDelegate? // ✅ weak — 不持有
}
在示例中,ParentVC 通过 child 属性持有对 ChildVC 的强引用。ChildVC 通过 delegate 持有对 ParentVC 的强引用。循环已闭合。修正:weak var delegate——引用不增加 retain count,ParentVC 可以被释放。
NSTimer——经典的 retain cycle 来源。定时器持有 target(通常是 self),而 target 通过属性持有定时器。即使定时器是一次性的,它也不会被释放直到 invalidate。解决方案:总是在 deinit 或 viewDidDisappear 中调用 timer.invalidate()。
在具有级联所有权的架构(协调器、路由器)中经常出现多步循环:Coordinator → ViewController → ViewModel → Coordinator(通过回调)。链中的每个强引用必须有意识地选择——一个弱引用在任何环节都能打破循环。
Swift 中的闭包通过强引用捕获外部变量。如果闭包作为对象的属性存储(例如 completion handler)并捕获了 self,就会形成 retain cycle:self → closure → self。
这是现代 Swift 开发中最常见的 retain cycle 来源。它是隐式产生的——开发者可能没有注意到闭包中对 self 的捕获,特别是在使用没有显式 self 的简化语法时。
class DownloadService {
var onComplete: ((Data) -> Void)?
var result: Data?
func startDownload() {
// ❌ Retain cycle: self → onComplete → self
onComplete = { data in
self.result = data
self.notifyUI()
}
// ✅ 修复:带有 weak self 的 capture list
onComplete = { [weak self] data in
guard let self else { return }
self.result = data
self.notifyUI()
}
}
func notifyUI() { }
}
Capture list [weak self] 在闭包内部创建对 self 的弱引用。如果 DownloadService 在闭包执行之前被释放,self 变为 nil,代码通过 guard 安全退出。这是 Swift 中异步闭包的标准模式——当闭包作为属性存储时应始终应用。
unowned self——weak self 的替代方案,当 self 保证比闭包存活得更久时使用。示例:立即执行的同步闭包(sorted、filter)。在这种情况下,self 肯定存活,unowned 是安全的。然而,unowned 在访问已释放的对象时会崩溃——因此 weak 默认被认为是安全的选择。
早期检测 retain cycle 对应用性能至关重要。让我们了解在 iOS 开发中识别循环引用的主要工具和方法。
Xcode Memory Debugger(Debug Memory Graph)——显示内存中对象及其引用关系的可视化工具。Retain cycle 显示为强箭头的闭合链。启动方法:在应用运行期间,点击 Debug area 面板中的 Debug Memory Graph 按钮。每个对象都显示类型、地址和引用列表。
Instruments Leaks——用于自动检测泄漏的分析器。记录内存分配并实时分析引用图。不仅检测 retain cycle,还检测被遗忘的引用、未释放的 ViewController 和其他泄漏。Leaks 指示确切的对象和持有链。
最简单的方法——在每个关键类的 deinit 中添加 print。如果在预期的对象销毁时 deinit 没有被调用——存在 retain cycle。这种方法不需要工具,对于初始诊断很有效。
| 工具 | 类型 | 何时使用 |
|---|---|---|
| Memory Debugger | 可视化图形 | 导航后手动检查 |
| Instruments Leaks | 自动分析 | 回归测试、CI |
| deinit print | 手动日志 | 开发、代码审查 |
| Malloc Scribble | 运行时标志 | 调试 use-after-free |
推荐方法:在开发阶段使用 deinit 日志记录,手动测试时使用 Memory Debugger,在 CI/CD 流水线中使用 Instruments Leaks 进行自动回归泄漏检测。
预防 retain cycle 比在生产环境中修复更容易。一些将循环引用风险降至最低的规则。
所有委托和 dataSource 都应该是 weak 的。这个规则内置于 UIKit 中:Apple SDK 中的所有委托协议都使用 weak 属性声明(UITableView.delegate、UICollectionView.dataSource)。对于你自己的协议,使用 weak var delegate: MyDelegate? 并使协议继承自 AnyObject。
每个作为属性存储(completion handler、callback)并捕获 self 的闭包都应在 capture list 中使用 [weak self]。例外——立即执行且不存储的闭包(sorted、map、filter)。对于它们,unowned self 是安全的。
在复杂架构(VIPER、Coordinators、Redux)中跟踪强引用的方向。所有者持有对下属的强引用,但下属只能通过 weak 或 unowned 引用所有者。单向数据流(unidirectional data flow)简化了引用控制。
// 示例:通过 deinit 日志记录检查
class BaseViewController: UIViewController {
deinit {
print("✅ \(type(of: self)) deallocated")
}
}
// 用法:所有 ViewController 继承自 BaseViewController
class ProfileVC: BaseViewController {
var viewModel: ProfileViewModel?
var onLogout: (() -> Void)?
override func viewDidLoad() {
super.viewDidLoad()
onLogout = { [weak self] in
self?.dismiss(animated: true)
}
}
}
// 关闭 ProfileVC 时,控制台中预期看到 "✅ ProfileVC deallocated"
带有 deinit 日志记录的基类提供即时反馈。如果在预期的屏幕关闭时消息没有出现——这个类中存在 retain cycle。将此实践添加到项目中所有 ViewController 的模板中。
常见问题
Retain cycle——ARC 的特有问题,强引用的闭合环阻止了释放。在 GC 中,收集器从根(root set)分析可达性,而不是引用计数器——因此循环不是泄漏。但在 ARC 中,任何隔离的循环都是确定的内存泄漏。
Weak 引用不会增加对象的 retain count。如果将循环中的一个强引用替换为 weak,每个对象的 retain count 都可以归零。对象释放后,weak 引用自动设置为 nil,防止访问已释放的内存。
可以,retain cycle 可以包含任意数量的对象:A → B → C → A。要释放,只需打破循环中的一个环节——将任意强引用替换为 weak 或 unowned。工具显示整个图,而不仅仅是对象对。
GCD(Grand Central Dispatch)不会在执行后存储闭包。DispatchWorkItem 执行后即被释放,即使闭包捕获了 self。Retain cycle 仅在闭包作为属性存储(类中的 completion handler)时才会产生,而不是在传递给队列时。
Instruments Leaks 并非总能找到临时的 retain cycle(存在几秒钟)和通过 bridge 在 C/C++ 对象中的循环引用。要彻底检查,请手动使用 Memory Debugger + 场景中所有关键对象的 deinit 日志记录。
总结
我们将开发一款交钥匙移动应用程序
IT Sectr自2017年以来为初创企业和企业打造iOS和Android应用程序。我们将为您提供咨询并提出最佳解决方案。