5-25.【性能分析与优化】ARC 的 retain/release 在什么情况下会成为热点?

4 阅读3分钟

一句话结论(先给你记忆锚点)

ARC 会成为热点,几乎只在这三种情况下:

  1. 短生命周期对象在热路径中反复出现
  2. 值类型被迫逃逸到 heap(boxing)
  3. 引用被频繁跨边界传递(closure / 协议 / 并发)

如果你看到 swift_retain / swift_release 排到 Time Profiler 前几名,一定不是“ARC 太慢” ,而是代码结构在逼 ARC 工作


二、ARC 成为热点的典型触发条件(非常具体)

1️⃣ 热路径 + 短命对象(最常见)

结构特征

for item in items {
    process(item)
}

看起来无害,但如果 itemprocess 内部涉及:

  • class
  • closure
  • existential container
  • escaping value

👉 每一轮循环都是 retain + release

Instruments 特征

  • Time Profiler:

    • swift_retain
    • swift_release
  • Allocations:

    • 生命周期极短
    • 分配速率高

关键点

ARC 不怕“多”,怕“频繁 + 短命”


2️⃣ 值类型被 boxing 到 heap(非常隐蔽)

触发场景

let p: P = MyStruct()   // struct 被装箱

或者:

func f(_ x: P) { ... } // P 是 protocol existential
  • struct → existential container
  • container 在 heap
  • 每次传递都 retain/release

为什么危险?

  • 你以为你在用 struct(值类型)
  • 实际在操作 heap object + ARC

Instruments 信号

  • swift_allocObject
  • swift_retain / release
  • witness table 调用

3️⃣ closure 捕获引用(尤其在 map / forEach)

items.map { item in
    self.update(item)
}

发生了什么:

  • closure 是 heap object
  • 捕获 self
  • 每次调用 retain/release self

高危变体

  • closure 在循环中创建
  • closure 逃逸(async / stored)

4️⃣ 跨并发边界(Dispatch / Task / Actor)

DispatchQueue.global().async {
    self.work()
}
  • self 被 retain
  • 跨线程 → ARC 需要原子操作
  • retain/release 成本更高

特别容易炸的情况

  • 大量短任务
  • 频繁调度
  • TaskGroup / async let 热路径

5️⃣ SwiftUI / Combine 的隐式 ARC 风暴

SwiftUI

  • body 频繁计算
  • closure + View struct
  • state 变化粒度太细

Combine

  • map / flatMap / sink
  • closure 链式调用
  • AnyPublisher(类型擦除)

👉 表现为:

  • retain/release 密集
  • CPU 不高但帧率掉

三、什么时候 ARC 不会 成为热点?

这是判断的重要反面例子。

✅ 不会成为热点的情况

场景原因
长生命周期对象retain/release 被摊销
单次 retain,多次使用amortized
struct + 泛型 + inout无 ARC
final class 内部调用retain 可被消除
non-escaping closure编译期优化

很重要的一点:
ARC 开销不是线性的,而是“边界触发型”


四、ARC 成为热点时的“黄金诊断流程”

Step 1:Time Profiler

看三个名字:

  • swift_retain
  • swift_release
  • swift_allocObject

Step 2:展开调用栈,问三个问题

  1. 是不是在循环里?
  2. 是不是 protocol / closure / async?
  3. 是不是 struct 看起来很“值”,但实际在 heap?

只要三个里中一个,ARC 就有嫌疑。


五、结构级解决方式(不是微调)

🔧 1️⃣ 减少 heap 对象

  • 用 struct
  • 用泛型
  • 避免 existential

🔧 2️⃣ 减少生命周期抖动

// ❌
for x in xs {
    let tmp = Obj(x)
    use(tmp)
}

// ✅
let pool = ObjPool()
for x in xs {
    pool.with(x) { use($0) }
}

🔧 3️⃣ 减少跨边界 retain

  • inout
  • local copy
  • 合并 async

🔧 4️⃣ 用 final / private 帮助 ARC 消除

final class C {
    func work() { ... }
}
  • 编译器可静态分析
  • retain/release 可消除或下沉

六、最终总结(非常重要)

ARC 成为热点,从来不是“引用计数机制不行”
而是:

  • 你在热路径中制造了大量短命引用
  • 你以为是值,其实是 heap
  • 你频繁跨抽象边界

当你在 Instruments 里看到 ARC 热点时,不要去“优化 ARC”
而要问:

“我这个 API / 数据流,为什么会制造这么多短命引用?”