Appearance
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 screenvsvar 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. 对应知识库文档
复习检查题
为什么本 demo 能用「输出顺序断言」验证释放,而 Dart/JVM 版做不到?
答:ARC 是编译期插桩的确定性同步析构,
conn = nil执行完 deinit 必然已完成;GC 语言对象只是先变「不可达」,真正回收要等收集器运行,时机不可预测,只能断言可达性而不能断言回收时机。weak和unowned怎么选?答:可能比对方先死(或不确定)用
weak(可选、悬空自动置 nil);确定自己活得比对方短且生命周期严格嵌套用unowned(非可选省解包,但访问悬空直接 crash)。为什么 Kotlin 里 Presenter ↔ Screen 双向持有不算 bug?
答:追踪式 GC 只看 GC Root 可达性,互引但根不可达的整环会被一起回收;ARC 数的是计数,环内每个对象计数恒 ≥1,永远到不了 0。