Skip to content

arc-memory-management-sim ​

Swift Playground 版:单文件 Swift,macOS 直接 swift ArcMemorySim.swift 运行,Foundation 层模拟 iOS ARC 行为。

1. 实验目标与工程要点 ​

  • 验证 ARC「引用计数归零 → 同步立即 deinit」的确定性析构时机,与 JVM/Dart 追踪式 GC 的延迟回收形成对照。
  • 制造并打破 Retain Cycle:双向强引用泄漏、weak 破环、unowned 无主引用语义。
  • 复现 Swift 最常见的隐式成环路径:闭包强捕获 self,以及 [weak self] 修法。
  • 关键观察点:deinit 输出顺序完全确定——这正是 ARC 与 GC 语言在可验证性上的本质差异(GC 语言无法用输出断言回收时机)。

2. 深入原理与生活浅显比喻 ​

2.1 生活浅显比喻 ​

ARC 像会议室签到表:进屋签一次(retain),出门划掉(release),最后一个人划掉时灯立即熄灭(deinit);循环引用像两个人互相等对方先关门,谁也不动,灯永远亮着。GC 像保洁定时巡场:没人(不可达)也不马上清,等下一轮巡场统一收拾。

2.2 底层原理分析 ​

编译器在编译期自动插入 retain / release / release(对象侧 dealloc),SideTable 记录计数与 weak 引用表;计数归零同步走 deinit 并释放内存,无守护线程、无 STW。weak 引用不增加计数,所指对象释放时经 SideTable 自动置 nil;unowned 同样不加计数但非可选,访问悬空对象直接 crash。JVM/Dart 则从 GC Root 做可达性扫描,「互引但根不可达」的环会被整组回收——所以同样的双向持有在 Kotlin/Dart 不是 bug。

2.3 方案对比矩阵 ​

方案通俗比喻底层原理保证特性吞吐性能运行开销适用场景
iOS/Swift ARC会议室签到表,最后一个划掉即关灯编译期插桩 retain/release,计数归零即时析构析构时机确定、无 STW分配快、释放即时每次赋值/传递有计数读写开销实时性敏感的 UI 层、iOS 全栈默认
JVM/Dart Tracing GC保洁定时巡场统一收拾从 GC Root 可达性标记-清除/分代回收循环引用自动清除批量分配吞吐高STW 或并发暂停、时机不可预测高并发分配的 Android/Dart 主战场

3. 移动端实战场景与误配事故 ​

3.1 实战场景 ​

  • iOS 侧 Flutter plugin 的 MethodChannel handler 存进单例,闭包强捕获 ViewController → 页面退出后 Native 内存只涨不降。
  • delegate、parent 回指必须一侧 weak;生命周期严格嵌套(Loan 必然短于 Owner)才可用 unowned。

3.2 常见误配与事故注意事项 ​

  • 把 Java/Dart「互引不用管」心智搬到 Swift → 永久泄漏。
  • [weak self] 后忘写 guard let self → 回调静默失效;该 strong-ify 时(如网络请求期间想保活)反而要权衡。
  • 高频循环内创建 autorelease 对象未包 @autoreleasepool → 内存尖峰 OOM(见对应 docs 页复习题)。

3.3 排障思路 ​

页面往返 N 次内存不回落 → Memory Graph Debugger 看紫色环警报 / Instruments Leaks;先查单例、长生命周期容器持有的闭包与回调,再查 delegate 是否漏 weak。

4. 实验源码与运行验证 ​

关键源码 ​

  • ArcMemorySim.swift —— 场景1 强引用即析构;场景2 双向强引用泄漏;场景3 weak 破环 + unowned;场景4 闭包强捕获 vs [weak self];末尾自检汇总由 deinit 计数器推导。

关键参数 / 开关 ​

  • static var deinitCount:每类对象的析构计数器,自检结论的数据来源。
  • weak var screen vs var screen:一字之差决定是否成环。
  • [weak self]:闭包捕获列表,打破 client ↔ closure 环。

运行方式 ​

bash
cd labs/swift-playground/arc-memory-management-sim
swift ArcMemorySim.swift        # 直接运行演示
./verify.sh                     # 运行 + 断言校验

预期控制台运行输出(节选) ​

text
[ARC] 置 nil 后:deinit 已同步完成(deinit 次数 = 1)
[ARC] 两个局部变量置 nil 后:Screen.deinit=0, Presenter.deinit=0
[ARC] -> 无任何输出 deinit,两个对象永久泄漏(Memory Graph Debugger 里会看到紫色环)
[CHECK] scene4 强捕获泄漏 / [weak self] 正常释放:PASS
PASS: 全部检查通过

断言脚本说明 ​

verify.sh 校验三点:① Swift 内置 5 项自检全部 PASS;② 场景3 中 Screen 先于 FixedPresenter 析构(weak 不阻止释放);③ 泄漏对象全程无任何 deinit 输出。任一失败退出码为 1。

5. 对应知识库文档 ​

复习检查题 ​

  1. 为什么本 demo 能用「输出顺序断言」验证释放,而 Dart/JVM 版做不到?

    答:ARC 是编译期插桩的确定性同步析构,conn = nil 执行完 deinit 必然已完成;GC 语言对象只是先变「不可达」,真正回收要等收集器运行,时机不可预测,只能断言可达性而不能断言回收时机。

  2. weak 和 unowned 怎么选?

    答:可能比对方先死(或不确定)用 weak(可选、悬空自动置 nil);确定自己活得比对方短且生命周期严格嵌套用 unowned(非可选省解包,但访问悬空直接 crash)。

  3. 为什么 Kotlin 里 Presenter ↔ Screen 双向持有不算 bug?

    答:追踪式 GC 只看 GC Root 可达性,互引但根不可达的整环会被一起回收;ARC 数的是计数,环内每个对象计数恒 ≥1,永远到不了 0。

站点构建时间:2026/8/24 23:43:17