5-6.【性能分析与优化】值类型“看起来在栈上”,但实际上什么时候会逃逸到堆上?

1 阅读2分钟

1️⃣ 栈分配 vs 堆分配

  • 栈(stack)

    • 生命周期短,跟随作用域。
    • 访问快,自动释放。
    • 常见:局部变量、函数参数(非闭包捕获)。
  • 堆(heap)

    • 生命周期由引用计数控制(ARC)。
    • 访问慢,需动态分配/释放。
    • 常见:class 对象,闭包捕获的值类型,数组 buffer。

2️⃣ 值类型为什么“看起来在栈上”

  • struct、enum 在局部作用域中 直接存储在栈,这是优化手段。
  • 例如:
struct Point { var x, y: Int }
func foo() {
    var p = Point(x: 1, y: 2)  // 栈上
}
  • 这时 p 生命周期完全在 foo 内,不需要堆分配。

3️⃣ 什么时候值类型会逃逸到堆上

(1)被闭包捕获并逃逸

var arr: [() -> Void] = []

func test() {
    var p = Point(x: 1, y: 2)
    arr.append { print(p.x) }  // 闭包逃逸到 arr
}
  • 这里 p 被捕获在 逃逸闭包 中。
  • 为了保证闭包访问的有效性,Swift 会把 p 搬到堆上
  • 如果闭包没有逃逸(non-escaping),值可以留在栈上。

(2)值类型作为引用类型的存储

class Wrapper {
    var point: Point
    init(point: Point) { self.point = point }
}

let w = Wrapper(point: Point(x:1, y:2))
  • Point 被存储在 Wrapper 的 heap 对象里 → Point 在堆上
  • 原理:class 对象永远在堆,struct 的成员随着 class 对象在堆上。

(3)数组 / 字典 / Set 等 CoW 容器

var data = [Point](repeating: Point(x:1,y:2), count: 1_000_000)
  • Swift 数组是 struct + buffer(heap)
  • 数组本身(data)是值类型,可以在栈上,但 元素 buffer 在堆上
  • 大数组必须堆分配,否则栈不够。

(4)被 @escaping 参数捕获

func f(closure: @escaping () -> Void) {}

func g() {
    var p = Point(x: 1, y: 2)
    f { print(p.x) }  // p 逃逸堆
}
  • 类似闭包逃逸情况。

4️⃣ 总结表格

情况分配位置
局部变量,非闭包捕获,small struct
大数组 / 字符串 / CoW 容器 buffer
值类型被 class 成员持有
值类型被闭包捕获且闭包逃逸
返回值被赋给外部变量堆(优化情况下可能栈上)

💡 核心原则:

值类型会尽量栈上分配,但一旦需要 引用延长生命周期或共享内存,就必须堆分配。