8-17.【内存管理机制】什么是强引用循环(strong reference cycle)?如何在对象图中识别?

32 阅读5分钟

在 Swift 的 ARC(自动引用计数)机制中,强引用循环(Strong Reference Cycle) ,也常被称为循环引用(Retain Cycle) ,是内存泄漏最主要的原因。

1. 什么是强引用循环?

当两个或多个对象互相持有对方的强引用,导致它们的引用计数永远无法归零时,就发生了强引用循环。

  • ARC 的逻辑:只有当一个对象的强引用计数减为 0 时,它才会被销毁。
  • 循环的后果:由于对象 A 强引用了对象 B,而对象 B 也强引用了对象 A,即使外部没有任何变量再指向它们,这两个对象也会在堆内存中“孤立地”互相拉扯。此时,内存无法回收,直到程序进程结束。

2. 强引用循环的典型模型

A. 类实例之间的循环

最直接的情况:两个类互相定义了强引用属性。

Swift

class Apartment {
    var tenant: Person? // 默认是强引用
}

class Person {
    var apartment: Apartment? // 默认是强引用
}

// 即使将外部变量设为 nil,这两个实例依然在内存中
B. 闭包与类实例之间的循环

这是最隐蔽的情况。由于闭包是引用类型,如果对象持有了闭包,而闭包又捕获了 self,就会形成环路。

  • Object \rightarrow (强持有) \rightarrow Closure
  • Closure \rightarrow (强捕获) \rightarrow Object (self)

3. 如何在对象图中识别?

识别循环引用的核心在于寻找“有向图”中的闭合环路

第一步:静态代码审计(找连接线)

在阅读代码时,观察属性的声明:

  1. 寻找持有者:谁拥有这个属性?(例如:ViewController 拥有一个 Service)。
  2. 寻找反向引用:被拥有的对象是否又指回了持有者?(例如:Service 是否有一个 delegate 指向 ViewController)。
  3. 检查修饰符:如果这两条线都是默认的强引用,那么这就是一个循环。
第二步:使用工具进行动态可视化

Xcode 提供了非常直观的工具来展示对象图(Object Graph):

  1. Xcode Memory Graph Debugger

    • 在运行时,点击调试栏的 "Debug Memory Graph" 图标。
    • 识别图标:Xcode 会在左侧导航栏用紫色的小叹号标记出疑似泄漏的对象。
    • 分析连线:点击对象,查看右侧的引用图。如果看到一条加粗的黑线从对象 A 指向 B,另一条又指回 A,这就是一个明显的环。
  2. Instruments (Leaks)

    • 使用 Command + I 打开 Instruments 里的 Leaks
    • 它会自动扫描堆内存,并生成一个 Cycles & Roots 图表,直接把形成环路的节点勾勒出来。
第三步:析构验证法(最简单的信号)

在对象图中,每一个节点都应该有清晰的“生”与“死”。

  • 在类中实现 deinit { print("销毁了") }
  • 如果你预期某个操作(如关闭页面)应该让对象消失,但控制台没有打印日志,说明在对象图中,有一根隐形的强引用线牵住了它。

4. 对象图中的“破环”方案

一旦在图中识别出循环,你需要手动将其中一根连线改为“弱连接”:

  1. 使用 weak:在对象图中,这会将实线变为虚线。对象销毁后,虚线自动断开(变为 nil)。
  2. 使用 unowned:类似于虚线,但不允许为 nil
  3. 手动断开:在不需要时将属性设为 nil

总结:识别强引用循环就像是在看一张地铁路线图。如果你发现从 A 站出发,顺着强引用箭头走,最终能回到 A 站,那么这就产生了一个内存黑洞。

英文版

8-17. [Memory Management] What is a Strong Reference Cycle? How to Identify it in an Object Graph?

In Swift's ARC (Automatic Reference Counting) mechanism, a Strong Reference Cycle, also commonly known as a Retain Cycle, is the primary cause of memory leaks.


1. What is a Strong Reference Cycle?

A strong reference cycle occurs when two or more objects hold strong references to each other, causing their reference counts to never reach zero.

  • ARC Logic: An object is only deallocated when its strong reference count drops to 0.
  • Consequences of a Cycle: Because Object A holds a strong reference to Object B, and Object B holds a strong reference to Object A, they will "pull" on each other in heap memory even if no external variables point to them anymore. Consequently, the memory cannot be reclaimed until the program process ends.

2. Typical Models of Strong Reference Cycles

A. Cycles Between Class Instances

The most straightforward case: two classes define strong reference properties pointing to each other.

Swift

class Apartment {
    var tenant: Person? // Default is a strong reference
}

class Person {
    var apartment: Apartment? // Default is a strong reference
}

// Even if external variables are set to nil, these two instances remain in memory.
B. Cycles Between Closures and Class Instances

This is the most subtle case. Since closures are reference types, if an object holds a closure and that closure captures self, a loop is formed.

  • Object \rightarrow (Strongly Holds) \rightarrow Closure
  • Closure \rightarrow (Strongly Captures) \rightarrow Object (self)

3. How to Identify Cycles in an Object Graph?

Identifying a retain cycle centers on finding closed loops within a "Directed Graph."

Step 1: Static Code Audit (Finding Connection Lines)

When reading code, observe the property declarations:

  1. Find the Owner: Who owns this property? (e.g., a ViewController owns a Service).
  2. Find the Reverse Reference: Does the owned object point back to the owner? (e.g., Does the Service have a delegate pointing to the ViewController?).
  3. Check Modifiers: If both of these lines are default strong references, you have a cycle.
Step 2: Dynamic Visualization with Tools

Xcode provides intuitive tools to display the Object Graph:

  1. Xcode Memory Graph Debugger:

    • While running, click the "Debug Memory Graph" icon in the debug bar.
    • Identify Icons: Xcode marks suspected leaking objects with a small purple exclamation mark in the left navigator.
    • Analyze Lines: Click an object to see the reference graph on the right. If you see a bold black line from Object A to B, and another pointing back from B to A, it’s a clear cycle.
  2. Instruments (Leaks) :

    • Open Leaks via Command + I.
    • It automatically scans heap memory and generates a Cycles & Roots chart, outlining the nodes forming the loop.
Step 3: Deinit Verification (The Simplest Signal)

In an object graph, every node should have a clear "birth" and "death."

  • Implement deinit { print("Deinitialized") } in your classes.
  • If you expect an action (like closing a page) to make an object disappear, but the console does not print the log, an invisible strong reference line in the object graph is holding onto it.

4. "Breaking the Loop" in the Object Graph

Once a cycle is identified in the graph, you must manually change one of the connection lines into a "weak connection":

  1. Use weak: In the object graph, this turns a solid line into a dashed line. After the object is destroyed, the dashed line automatically breaks (becomes nil).
  2. Use unowned: Similar to a dashed line, but does not allow the reference to be nil.
  3. Manual Disconnection: Set the property to nil when it is no longer needed.

Summary: Identifying a strong reference cycle is like looking at a subway map. If you start at Station A, follow the strong reference arrows, and eventually find yourself back at Station A, you have discovered a memory "black hole."