Retain Cycle — 本质、产生原因及在应用开发中的消除方法

作者: IT Sectr 发布日期: 2026-03-29 阅读时间: 8 分钟

Retain Cycle(循环引用)—— ARC 中两个或更多对象通过强引用相互引用,形成闭环的情况。根据 Apple Memory Management Guide, 2026,retain cycle 阻止了循环中所有对象的释放,因为每个对象都有 ≥ 1 的 retain count。与 GC 中的内存泄漏不同,retain cycle 保证只要循环中至少有一个外部参与者存活,对象就会保持存活——甚至在所有外部引用丢失之后,如果循环是隔离的也是如此。

要点

  • Retain Cycle——强引用的闭环,导致对象无法被 ARC 释放
  • 原因——两个(或更多)对象彼此持有强引用,retain count 无法归零
  • 后果——内存泄漏:对象永远留在内存中,RAM 消耗增加
  • 解决方案——将循环中的一个强引用替换为 weak 或 unowned
  • 诊断工具——Xcode Memory Debugger、Instruments Leaks、Debug Memory Graph

什么是 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 示例

让我们看看每个 iOS 开发者都会遇到的经典 retain cycle 场景。理解这些模式是编写安全代码与 ARC 的基础。

带有委托的 Parent-Child

经典场景:父对象(例如 UIViewController)创建一个子对象并成为其委托。如果两者都使用强引用,就会产生 retain cycle。解决方案——委托应该是 weak。

swift
// 错误:通过 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

NSTimer——经典的 retain cycle 来源。定时器持有 target(通常是 self),而 target 通过属性持有定时器。即使定时器是一次性的,它也不会被释放直到 invalidate。解决方案:总是在 deinit 或 viewDidDisappear 中调用 timer.invalidate()。

层次架构

在具有级联所有权的架构(协调器、路由器)中经常出现多步循环:Coordinator → ViewController → ViewModel → Coordinator(通过回调)。链中的每个强引用必须有意识地选择——一个弱引用在任何环节都能打破循环。

Swift 闭包中的 Retain Cycle

Swift 中的闭包通过强引用捕获外部变量。如果闭包作为对象的属性存储(例如 completion handler)并捕获了 self,就会形成 retain cycle:self → closure → self

这是现代 Swift 开发中最常见的 retain cycle 来源。它是隐式产生的——开发者可能没有注意到闭包中对 self 的捕获,特别是在使用没有显式 self 的简化语法时。

swift
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

unowned self——weak self 的替代方案,当 self 保证比闭包存活得更久时使用。示例:立即执行的同步闭包(sorted、filter)。在这种情况下,self 肯定存活,unowned 是安全的。然而,unowned 在访问已释放的对象时会崩溃——因此 weak 默认被认为是安全的选择

如何检测 retain cycle:诊断工具

早期检测 retain cycle 对应用性能至关重要。让我们了解在 iOS 开发中识别循环引用的主要工具和方法。

Xcode Memory Debugger

Xcode Memory Debugger(Debug Memory Graph)——显示内存中对象及其引用关系的可视化工具。Retain cycle 显示为强箭头的闭合链。启动方法:在应用运行期间,点击 Debug area 面板中的 Debug Memory Graph 按钮。每个对象都显示类型、地址和引用列表。

Instruments Leaks

Instruments Leaks——用于自动检测泄漏的分析器。记录内存分配并实时分析引用图。不仅检测 retain cycle,还检测被遗忘的引用、未释放的 ViewController 和其他泄漏。Leaks 指示确切的对象和持有链

deinit 日志记录

最简单的方法——在每个关键类的 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 和最佳实践

预防 retain cycle 比在生产环境中修复更容易。一些将循环引用风险降至最低的规则。

Weak 委托规则

所有委托和 dataSource 都应该是 weak 的。这个规则内置于 UIKit 中:Apple SDK 中的所有委托协议都使用 weak 属性声明(UITableView.delegate、UICollectionView.dataSource)。对于你自己的协议,使用 weak var delegate: MyDelegate? 并使协议继承自 AnyObject。

闭包中的 Capture list

每个作为属性存储(completion handler、callback)并捕获 self 的闭包都应在 capture list 中使用 [weak self]。例外——立即执行且不存储的闭包(sorted、map、filter)。对于它们,unowned self 是安全的。

检查架构

在复杂架构(VIPER、Coordinators、Redux)中跟踪强引用的方向。所有者持有对下属的强引用,但下属只能通过 weak 或 unowned 引用所有者。单向数据流(unidirectional data flow)简化了引用控制。

swift
// 示例:通过 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 与 GC 中的内存泄漏有何不同?

Retain cycle——ARC 的特有问题,强引用的闭合环阻止了释放。在 GC 中,收集器从根(root set)分析可达性,而不是引用计数器——因此循环不是泄漏。但在 ARC 中,任何隔离的循环都是确定的内存泄漏。

Weak 引用如何打破 retain cycle?

Weak 引用不会增加对象的 retain count。如果将循环中的一个强引用替换为 weak,每个对象的 retain count 都可以归零。对象释放后,weak 引用自动设置为 nil,防止访问已释放的内存。

Retain cycle 可以由三个或更多对象组成吗?

可以,retain cycle 可以包含任意数量的对象:A → B → C → A。要释放,只需打破循环中的一个环节——将任意强引用替换为 weak 或 unowned。工具显示整个图,而不仅仅是对象对。

为什么 GCD DispatchWorkItem 不会创建 retain cycle?

GCD(Grand Central Dispatch)不会在执行后存储闭包。DispatchWorkItem 执行后即被释放,即使闭包捕获了 self。Retain cycle 仅在闭包作为属性存储(类中的 completion handler)时才会产生,而不是在传递给队列时。

哪些类型的 retain cycle 不会被 Instruments 检测到?

Instruments Leaks 并非总能找到临时的 retain cycle(存在几秒钟)和通过 bridge 在 C/C++ 对象中的循环引用。要彻底检查,请手动使用 Memory Debugger + 场景中所有关键对象的 deinit 日志记录。

总结

  • Retain Cycle——阻塞 ARC 中对象释放的强引用闭环
  • 原因——具有强引用的委托、捕获 self 的闭包、双向的父子关系
  • 解决方案——将一个强引用替换为 weak 或 unowned 可以打破循环
  • 闭包——存储的 completion handler 应始终使用 [weak self]
  • 委托——始终是 weak;委托协议应继承自 AnyObject
  • 诊断——Xcode Memory Debugger、Instruments Leaks、deinit 日志记录
  • 预防——单向数据流、weak delegate、capture list、带 deinit 的基类

我们将开发一款交钥匙移动应用程序

IT Sectr自2017年以来为初创企业和企业打造iOS和Android应用程序。我们将为您提供咨询并提出最佳解决方案。

讨论项目

另请阅读