Appearance
10. iOS ARC 自动引用计数与 Java/Dart GC 跨端对照
一句话定位:iOS/macOS 采用 ARC (Automatic Reference Counting) 编译期确定性销毁对象,与 Android (ART) / Flutter (Dart VM,AOT 编译后仍由自研 GC 管理) 基于 GC 追踪式回收 (Tracing GC) 构成跨端两大内存流派;理解 ARC 循环引用 (Retain Cycle) 与 Dart 强弱引用/对象池 是攻克跨端内存泄露的核心。
代码索引
| 主题 | Lab 说明 | 源码 |
|---|---|---|
| ARC 析构时机 / Retain Cycle / weak·unowned / 闭包捕获 | arc-memory-management-sim(Swift Playground 版,macOS 直接运行,deinit 输出顺序可断言) | ArcMemorySim.swift |
核心概念:ARC 引用计数 vs Tracing GC (Java / Dart)
图注补充:异步批量回收 / STW 或并发暂停。
1. 为什么 iOS 不用 GC,而 Android / Dart 采用 GC?
- iOS (ARC):Apple 追求极致的实时性与可预测性(Deterministic UI) 。对象一旦
refCount == 0会在当前汇编指令周期立即触发dealloc,无需开辟守护线程进行全局扫描,完全避免 GC 停顿 (STW)。 - Android/Dart (GC):Java/Dart 为了降低开发者编码心智负担,支持高速高并发分配。对象通过指针碰撞快速在年轻代分配;GC 引擎会自动解决复杂的互相强引用图,避免 ARC 中的手动弱引用打补丁。
2. ARC 最致命的痛点:Retain Cycle(循环引用)
对应 Lab:arc-memory-management-sim —— 场景2 复现双向强引用泄漏(全程无 deinit 输出),场景3/4 验证
weak破环、unowned与闭包[weak self]修法。
在 Swift/Objective-C 中,若两个对象(或对象与闭包/Block)相互持有强引用:
swift
class ViewController {
var onAction: (() -> Void)?
func setup() {
// 错误:self -> onAction 闭包 -> 捕获了 self (Retain Cycle)
onAction = { self.doSomething() }
}
}此时因为双方 refCount 永远不会降为 0,程序中即使没有任何变量指向它们,这两个对象也终身无法被释放,造成永久性内存泄露。
- 解法:在闭包中使用
[weak self](弱引用,置 nil)或[unowned self](无主引用,非可选但引用悬空时报 Crash)。
为什么需要(框架)
- 为什么在 iOS / Swift 开发中闭包(Closure / Block)特别容易产生内存泄露,而在 Dart / Kotlin 闭包里捕获
this/self很少永久泄漏?- 一句话答:iOS 采用 ARC 编译期引用计数,对象与闭包若形成相互强引用环,它们的引用计数永远大于 0 无法释放;而 Dart/Kotlin 采用追踪式 GC,只要整个环对根节点(GC Root)不可达,GC 就会把整环一起安全销毁。(详见下文 §1、§2及本章复习题)
- 为什么 Flutter 接入 Native (iOS) Plugin 时,原生层的内存没有被回收?
- 一句话答:Dart 侧对象通过 GC 管理,iOS 端通过 ARC 管理;如果 MethodChannel 监听或回调闭包在 Native 端未解除被单例/长生命周期 Controller 持有,会导致 Native 宿主页面出现 ARC 内存泄露。(详见下文 §2及跨端内存架构对照表)
跨端内存架构对齐表
| 比较维度 | iOS / Swift (ARC) | Android Java/Kotlin (ART GC) | Flutter / Dart (Generational GC) |
|---|---|---|---|
| 回收核心时机 | 编译期插入 retain/release,计数为 0 即时析构 | 内存不足或阈值触发,运行时分代扫描 | 分代并发收集器,极低停顿(Dart VM 自研 GC,非 V8) |
| 循环引用 | 会永久泄露,必须显式用 weak / unowned | 自动清除(不再可达即被回收) | 自动清除(不可达对象组统统回收) |
| 内存峰值控制 | 平缓、无尖峰、极其顺滑 | 有 GC 爆发期、容易在大量临时对象时突变 | 新生代 (Nursery) 清理极快(几微秒) |
| 手动干预手段 | @autoreleasepool 手动释放海量临时局部对象 | System.gc() (不推荐) / 弱引用缓存 | Finalizer / WeakReference |
常见误配、事故后果与排障
1. 事故:Flutter 与 iOS 混编时,Native 侧闭包被单例持有导致 ARC 泄漏
- 误配原因:在 iOS 侧实现 Flutter plugin / MethodChannel handler 时,把回调闭包存进单例或长生命周期 Controller,闭包内又强捕获了 ViewController / FlutterViewController。
- 后果:形成 Retain Cycle,页面退出后对象引用计数恒大于 0,Native 内存只涨不降(Flutter 侧看不到,泄漏发生在 ARC 层)。
- 排障与修法:优先
[weak self]捕获;通道 handler 在页面dispose/onCancel时注销;用 Memory Graph Debugger 看紫色循环引用警报,或用 Instruments Leaks 验证页面往返 N 次后内存是否回落。
2. 事故:高频循环内创建大对象,忽略 @autoreleasepool 导致内存尖峰
- 误配原因:在
for循环内反复创建UIImage/CIImage等自动释放对象,未包@autoreleasepool。 - 后果:临时对象在
autoreleasepool兜底释放前持续累积,内存峰值瞬间爆仓,直接 OOM 崩溃(复现自测 §复习题 1)。 - 排障与修法:循环体包
@autoreleasepool { ... }让单次迭代立即释放;大图用imageWithContentsOfFile/downsampling降低解码峰值。
3. 事故:把 Java/Dart GC 的"循环引用不用管"心智直接套到 iOS
- 误配原因:在 Swift/ObjC 里写出 A ↔ B 互相强引用(或闭包捕获 self),以为"反正系统会回收"。
- 后果:ARC 对循环引用无能为力,对象永久泄漏——这正是跨端工程师最容易踩的坑:同一段"闭包捕获"代码,在 Dart/Kotlin 里没问题,搬到 Swift 就泄漏。
- 排障与修法:跨端编码时对 iOS 侧引用关系单独审视:双向持有必须有一侧用
weak/unowned;建立"闭包捕获清单"走评审。
对应实验
| Lab | 说明 | 源码 |
|---|---|---|
| arc-memory-management-sim | Foundation 层单文件模拟 ARC:强引用归零同步 deinit、双向强引用泄漏、weak 破环 + unowned、闭包强捕获 vs [weak self];deinit 输出顺序完全确定,可用断言校验(与文首「代码索引」一致) | ArcMemorySim.swift · verify.sh |
本章复习题
- 在 iOS 和 Flutter 混合栈开发时,为什么有时需要通过
@autoreleasepool优化高频循环内的图片加载?- 答:在
for循环里高频解压和处理大图(如 UIImage / CIImage)时,方法返回值由自动释放池统一挂起,必须通过局部@autoreleasepool { ... }让池内对象在单次迭代末尾立刻减计为 0 并回收,避免内存峰值瞬间爆仓引出 OOM。
- 答:在
- 如何排查 Swift/ObjC 中的 Retain Cycle?
- 答:利用 Xcode DevTools 工具链中的 Memory Graph Debugger(内存图断点分析) 查看对象引用拓扑树上的紫线(循环依赖警报),或在 Instruments 中运行 Leaks / Allocations 分析。
对应实验室与拓展
- 实战指引:关于 Android 平台 GC 模型与 Dart 新生代分配的更深细节,请参考 §02-memory-lifecycle/01-jvm-dart-memory-model.md。
- 关联拓展:如果想通过弱引用在 Android/Dart 中优雅管理缓存,可复习 §02-memory-lifecycle/08-reference-types-and-weak-reference.md。