在 Swift 开发中,循环引用(Strong Reference Cycles)通常出现在那些需要长期持有引用或者定义了双向关系的场景中。
以下是四大典型的循环引用场景及其底层成因分析:
1. 代理模式 (Delegate) —— 最经典的场景
这是 iOS 开发中最常见的内存泄漏点。
-
场景描述:
ViewController持有一个UITableView,而UITableView的delegate属性又指回了ViewController。 -
成因:如果
delegate属性在内部声明为strong(默认情况),则VC -> TableView -> VC形成闭环。 -
解决方案:将代理属性声明为
weak。Swift
protocol MyDelegate: AnyObject { } class MyView { weak var delegate: MyDelegate? // 必须使用 weak }
2. 逃逸闭包 (Escaping Closures) —— 最隐蔽的场景
闭包是引用类型,它会捕获并持有其内部使用的所有外部对象。
-
场景描述:类实例持有一个闭包作为属性(例如用于回调),而闭包内部使用了
self。 -
成因:
self强持有closure,closure强捕获self。 -
典型案例:网络请求封装、自定义 View 的点击事件回调、或者是存储在数组里的动画 Block。
-
解决方案:使用捕获列表
[weak self]。Swift
class NetworkManager { var onComplete: (() -> Void)? func fetchData() { onComplete = { [weak self] in // 避免强捕获 self self?.updateUI() } } }
3. 双向强引用 (Parent-Child Relationships) —— 逻辑层面的场景
当两个数据模型对象之间存在逻辑上的“所属”和“反向关联”时。
-
场景描述:
Teacher类有一个学生列表[Student]。Student类有一个老师属性var teacher: Teacher。
-
成因:老师强引用学生,学生也强引用老师。
-
解决方案:根据逻辑上的生命周期,将一方设为
weak或unowned。通常“子”对象对“父”对象的引用应为weak。
4. 计时器与观察者 (Timer & Observer) —— 系统层面的场景
这些场景的循环引用往往不是两个自定义对象之间,而是 系统组件 <-> 你的对象 之间。
-
Timer (NSTimer) :
- 问题:
RunLoop强持有Timer,而Timer默认强持有它的target(通常是self)。只要 Timer 不停止,self就永远无法释放。 - 解决:使用 Swift 5 之后提供的闭包版 Timer,并在闭包内使用
[weak self];或者在页面消失时手动调用invalidate()。
- 问题:
-
NotificationCenter (Block-based) :
- 问题:使用
addObserver(forName:...)返回的观察者对象会被强持有,且闭包内部如果强捕获了self,也会导致泄漏。 - 解决:存储返回的观察者对象,并在
deinit时移除,同时闭包内使用[weak self]。
- 问题:使用
5. 多层级容器 (Collection Types)
- 场景描述:将
self放入一个由self持有的数组或字典中。 - 成因:数组会强持有其内部的所有元素。如果数组是
self的属性,则形成了self -> Array -> self。
如何快速识别这些场景?
当你怀疑有循环引用时,可以问自己三个问题:
- 我是否在类的属性里存储了一个闭包? (如果是,检查闭包里有没有
self) - 我是否有两个类互相指向对方? (如果是,检查是否有一方是
weak) - 这个对象在页面退出后,
deinit方法执行了吗? (如果没有,说明上述某个连线断不掉)
英文版
8-18. [Memory Management] What are the Typical Strong Reference Cycle Scenarios? (e.g., Delegates, Closures, Bi-directional References)
In Swift development, strong reference cycles (Retain Cycles) typically appear in scenarios requiring long-term reference retention or where bi-directional relationships are defined.
Here are the four most typical scenarios and an analysis of their underlying causes:
1. Delegate Pattern — The Most Classic Scenario
This is the most common memory leak point in iOS development.
- Scenario: A
ViewControllerholds aUITableView, and theUITableView'sdelegateproperty points back to theViewController. - Cause: If the
delegateproperty is declared asstrong(the default), a closed loop ofVC -> TableView -> VCis formed. - Solution: Declare the delegate property as
weak.
Swift
protocol MyDelegate: AnyObject { }
class MyView {
weak var delegate: MyDelegate? // Must use weak to break the cycle
}
2. Escaping Closures — The Most Subtle Scenario
Closures are reference types; they capture and hold all external objects used within them.
- Scenario: A class instance holds a closure as a property (e.g., for a callback), and the closure uses
selfinternally. - Cause:
selfstrongly holds theclosure, and theclosurestrongly capturesself. - Typical Cases: Network request wrappers, custom View click event callbacks, or animation blocks stored in an array.
- Solution: Use a capture list
[weak self].
Swift
class NetworkManager {
var onComplete: (() -> Void)?
func fetchData() {
onComplete = { [weak self] in // Avoid strong capture of self
self?.updateUI()
}
}
}
3. Bi-directional Strong References (Parent-Child Relationships)
This occurs when two data model objects have a logical "ownership" and "reverse association."
-
Scenario:
- A
Teacherclass has a list of students:[Student]. - A
Studentclass has a teacher property:var teacher: Teacher.
- A
-
Cause: The teacher strongly references the students, and the students strongly reference the teacher.
-
Solution: Based on the logical lifecycle, set one side to
weakorunowned. Usually, the "child" object's reference to the "parent" should beweak.
4. Timers and Observers — System-Level Scenarios
In these cases, the cycle isn't between two custom objects, but between a System Component <-> Your Object.
-
Timer (NSTimer) :
- The Issue: The
RunLoopstrongly holds theTimer, and theTimerdefaults to strongly holding itstarget(usuallyself). As long as the Timer doesn't stop,selfcan never be released. - The Fix: Use the closure-based Timer provided since Swift 5 with
[weak self], or manually callinvalidate()when the page disappears.
- The Issue: The
-
NotificationCenter (Block-based) :
- The Issue: The observer object returned by
addObserver(forName:...)is strongly held. If the closure strongly capturesself, a leak occurs. - The Fix: Store the returned observer object, remove it in
deinit, and use[weak self]inside the closure.
- The Issue: The observer object returned by
5. Multi-level Containers (Collection Types)
- Scenario: Placing
selfinto an array or dictionary that is itself held byself. - Cause: Arrays strongly hold all their internal elements. If the array is a property of
self, it formsself -> Array -> self.
How to Quickly Identify These Scenarios?
When you suspect a strong reference cycle, ask yourself these three questions:
- Am I storing a closure in a class property? (If yes, check if the closure contains
self). - Do I have two classes pointing at each other? (If yes, check if one side is
weak). - Does the
deinitmethod execute after the page is dismissed? (If not, one of the connections above is failing to break).