8-16.【内存管理机制】如何分析闭包捕获造成的内存泄漏问题?

32 阅读5分钟

分析闭包引起的内存泄漏(循环强引用),本质上是寻找内存中的闭合环路。在 Swift 中,闭包是引用类型,当它捕获了持有它的对象时,就会形成“对象 \rightarrow 闭包 \rightarrow 对象”的死结。

以下是结构化的分析方法和工具链:


1. 逻辑审计:寻找“持有链”

首先通过代码审查寻找满足以下两个条件的场景:

  1. 对象持有闭包:对象有一个属性是闭包类型,或者闭包被存储在对象持有的集合(如数组、字典)中。
  2. 闭包捕获对象:闭包内部使用了 self,或者使用了对象持有的某个强引用属性。

警惕陷阱:即使没写 self,访问对象的成员变量(如 _name 或调用方法)也会隐式捕获 self


2. 使用 Xcode Memory Graph Debugger(最直观)

这是最强大的日常分析工具。

  1. 运行 App 并进入怀疑有泄漏的界面,然后退出该界面。

  2. 点击 Xcode 调试栏底部的 Debug Memory Graph 按钮(三个小圆圈连线的图标)。

  3. 查看左侧面板:寻找那些本该被销毁但依然存在的类实例。

  4. 分析引用线:点击实例,右侧会显示引用关系图。

    • 如果看到一个 Swift closure context 节点指向你的对象,而你的对象又通过某个属性指回这个闭包,这就是明确的循环引用。

3. 使用 Instruments (Leaks)

如果你无法确定具体是哪个对象泄漏,Instruments 可以帮你全局扫描。

  1. 按下 Command + I 打开 Instruments,选择 Leaks
  2. 点击左上角红色按钮开始录制。
  3. 在 App 中反复执行“打开-关闭”页面的操作。
  4. Leaks 栏:如果出现红色叉号,说明发生了泄漏。
  5. Cycles & Roots:在下方的详细面板切换到这个视图,Instruments 会自动为你绘制出导致泄漏的引用环。

4. 析构探测法(最简单暴力)

在你的类中添加 deinit 日志:

Swift

deinit {
    print("(Self.self) 得到了释放")
}

如果你退出页面后控制台没有打印这条消息,那么 100% 发生了内存泄漏。此时,你可以通过注释掉闭包内的代码来快速回溯,定位具体是哪一行捕获导致了问题。


5. 闭包捕获的典型结构分析

分析时,请注意区分以下两种引用环:

A. 直接循环

self \rightarrow Property \rightarrow Closure \rightarrow self

  • 例子:网络请求回调被存储在 ViewController 的变量中。

B. 间接循环

self \rightarrow View \rightarrow SubView \rightarrow Closure \rightarrow self

  • 例子:闭包被传给了子视图,子视图强持有了这个闭包,而闭包又捕获了父视图。这种环路更隐蔽,需要沿着 UI 树层级向上查找。

6. 预防与修复

一旦确认是闭包捕获导致的问题,修复方案非常统一:

  • 使用捕获列表[weak self] 是最通用的做法。
  • 解除持有:在闭包执行完毕后,手动将对象持有的闭包变量置为 nil(这会打破“对象 \rightarrow 闭包”这一环)。
  • 非逃逸优化:如果闭包不需要在函数结束后执行,确保它没有被标记为 @escaping,非逃逸闭包不会产生循环引用。

总结建议

分析的第一步永远是 确认对象是否调用了 deinit。如果没有,再通过 Memory Graph 观察是谁在“拽着”这个对象不放。

英文版

8-16. [Memory Management] How to Analyze Memory Leaks Caused by Closure Capture?

Analyzing memory leaks (strong reference cycles) caused by closures is essentially a search for closed loops in memory. In Swift, closures are reference types; when a closure captures the object that holds it, a "Deadlock" of Object \rightarrow Closure \rightarrow Object is formed.

Here is a structured analysis methodology and toolchain:


1. Logical Audit: Searching for the "Holding Chain"

First, perform a code review to find scenarios that satisfy these two conditions:

  1. Object Holds Closure: The object has a property of a closure type, or the closure is stored in a collection (like an array or dictionary) held by the object.
  2. Closure Captures Object: The closure uses self internally, or accesses a strong reference property held by the object.

Warning Trap: Even if you don't explicitly write self, accessing an object's member variables (like _name) or calling its methods will implicitly capture self.


2. Using Xcode Memory Graph Debugger (Most Intuitive)

This is the most powerful tool for daily analysis.

  1. Run the App and enter the interface suspected of leaking, then exit that interface.

  2. Click the Debug Memory Graph button at the bottom of the Xcode debug bar (the icon with three small circles connected by lines).

  3. Check the Left Panel: Look for class instances that should have been destroyed but still exist.

  4. Analyze Reference Lines: Click an instance; the right panel will display a reference relationship graph.

    • If you see a Swift closure context node pointing to your object, while your object points back to that closure via a property, this is a definitive retain cycle.

3. Using Instruments (Leaks)

If you cannot determine exactly which object is leaking, Instruments can help you perform a global scan.

  1. Press Command + I to open Instruments and select Leaks.
  2. Click the red record button in the top left.
  3. Repeatedly perform "Open-Close" actions on the page within the App.
  4. Leaks Lane: If a red 'X' appears, a leak has occurred.
  5. Cycles & Roots: Switch to this view in the detail panel below. Instruments will automatically draw the reference ring causing the leak for you.

4. Deinit Detection (Simplest & Most Direct)

Add a deinit log to your class:

Swift

deinit {
    print("(Self.self) has been deinitialized")
}

If this message is not printed in the console after you exit the page, a memory leak has 100% occurred. At this point, you can quickly backtrack by commenting out code inside the closure to locate exactly which line of capture is causing the issue.


5. Typical Structure Analysis of Closure Captures

When analyzing, distinguish between these two types of reference rings:

A. Direct Cycle

self \rightarrow Property \rightarrow Closure \rightarrow self

  • Example: A network request callback is stored in a variable within a ViewController.

B. Indirect Cycle

self \rightarrow View \rightarrow SubView \rightarrow Closure \rightarrow self

  • Example: A closure is passed to a subview, which holds a strong reference to it, while the closure captures the parent view. This type of ring is more hidden and requires searching upwards through the UI tree hierarchy.

6. Prevention and Fixes

Once a problem caused by closure capture is confirmed, the solutions are consistent:

  • Use Capture Lists: [weak self] is the most common practice.
  • Break the Hold: Manually set the closure property held by the object to nil after the closure finishes executing (this breaks the "Object \rightarrow Closure" link).
  • Non-escaping Optimization: If the closure does not need to execute after the function ends, ensure it is not marked as @escaping. Non-escaping closures do not create retain cycles.

Summary & Recommendation

The first step of analysis is always to confirm whether the object called deinit. If it didn't, use the Memory Graph to observe who is "tugging" on that object and refusing to let go.