7-9.【高级特性】ARC 会对 stack-allocated 引用类型做哪些优化?

4 阅读2分钟

当编译器能证明一个 class 实例不逃逸其作用域时,
ARC 会被“静态消除”,对象甚至可以被栈分配,retain/release 直接不存在。


一、先澄清一个关键误区

误区

“class 一定在堆上,由 ARC 管理”

真实情况

class 语义上是引用类型,但 物理上不一定真的堆分配,也不一定真的走 ARC。

Swift 编译器在 SIL 层有非常激进的 ARC 优化。


二、ARC 能对“栈分配 class”做什么优化?

1️⃣ 核心前提:Escape Analysis(逃逸分析)

编译器会判断:

  • 对象是否:

    • 被返回?
    • 被赋给全局 / heap?
    • 被闭包捕获并逃逸?
    • 被作为 AnyObject 传出?
    • 被动态派发调用?

如果 都没有 👉 对象是 non-escaping


三、优化 1:完全消除 retain / release(ARC 消失)

class C {
    var x: Int = 0
}

func foo() {
    let c = C()
    c.x += 1
}

在这种情况下:

  • c

    • 不返回
    • 不传出
    • 不捕获
  • 👉 引用计数恒为 1

  • 👉 retain / release 全部被移除

ARC 在这里退化成了“静态生命周期管理”。


四、优化 2:对象直接栈分配(stack promotion)

在 ARC 被消除后,下一步优化是:

heap → stack promotion

func foo() {
    let c = C()
    use(c.x)
}

内存模型变成:

Stack:
[C instance memory]

而不是:

Stack: pointer
Heap:  object

📌 注意:

  • 这是编译器优化
  • 不改变 class 的语义
  • 调试器 / Instruments 未必能直观看到

五、优化 3:对象“拆解”(scalar replacement)

再进一步,编译器可能会做:

Scalar Replacement of Aggregates (SROA)

class Pair {
    var a: Int
    var b: Int
}

在极端情况下:

  • Pair 对象 根本不存在
  • ab 直接变成两个局部 Int 变量
Stack:
[a]
[b]

👉 连“对象”这个概念都被抹掉了。


六、为什么你平时几乎感知不到?

因为 这些优化非常容易失效

❌ 常见“破坏优化”的操作

行为结果
返回对象必须堆分配
赋给 AnyObject逃逸
作为参数传给未知函数逃逸
存入 Array / Dictionary逃逸
被 escaping closure 捕获逃逸
动态派发(@objc / protocol)逃逸

一旦逃逸:

  • 必须堆分配
  • 必须完整 ARC

七、ARC 优化在 SwiftUI / 性能中的意义

为什么 SwiftUI 强调 struct?

  • struct:

    • 天然无 ARC
  • class:

    • 只有在“极端理想条件”下才能优化到 struct 的性能

SwiftUI 的 View 构建:

  • 高度动态
  • 大量闭包
  • protocol / existential

👉 几乎不满足 class 栈分配的条件


八、面试官真正想听的那段话(建议原话)

Swift 编译器会通过逃逸分析消除不必要的 ARC,
对不逃逸的 class 实例,retain/release 可被完全移除,
甚至将对象从堆提升到栈,或直接拆解为标量。
但这些优化非常容易因逃逸而失效。


九、一个现实判断标准(工程向)

如果你“指望 ARC 优化 class 的性能”,
那你已经输了;
性能敏感路径应主动使用值类型或显式 COW 设计。