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 成员持有 | 堆 |
| 值类型被闭包捕获且闭包逃逸 | 堆 |
| 返回值被赋给外部变量 | 堆(优化情况下可能栈上) |
💡 核心原则:
值类型会尽量栈上分配,但一旦需要 引用延长生命周期或共享内存,就必须堆分配。