1. 闭包是引用类型
在 Swift 中,闭包是引用类型(Reference Type) 。
当你将一个闭包赋值给变量或将其传递给另一个函数时,你实际上是在传递一个指向该闭包实体的指针,而不是拷贝闭包的代码或其捕获的状态。
- 证明方法:如果你将同一个闭包赋值给两个不同的变量,并让其中一个变量执行修改捕获变量的操作,另一个变量在执行时也会看到同样的变化。
2. 闭包捕获变量的底层原理
闭包捕获(Capture)的底层核心在于:提升变量的生命周期。
通常局部变量存储在**栈(Stack)上,函数执行完就会被销毁。但如果闭包捕获了它,为了保证闭包在未来任何时候执行时变量依然有效,Swift 会将该变量从栈“提升”到堆(Heap)**上。
核心结构:HeapContext
在底层,闭包实际上是一个结构体(通常称为 ClosureBox 或 Thunk),它包含两个部分:
- 函数指针:指向闭包内执行的代码逻辑。
- 捕获上下文(Capture Context) :一个指向堆内存的指针,里面存储了所有被捕获的变量。
捕获方式:引用捕获
默认情况下,Swift 闭包捕获的是变量的引用,而不是变量当时的值。
- 对于值类型(如 Int, Struct) :Swift 会在堆上创建一个包装容器(Box)。闭包内部和原始函数作用域实际上共享这个堆上的 Box 指针。这就是为什么闭包内部修改变量,外部也会同步改变的原因。
- 对于引用类型(如 Class 实例) :闭包会捕获该实例的指针,并触发 ARC (Automatic Reference Counting) 操作,使该对象的引用计数加 1。
3. 逃逸闭包 vs 非逃逸闭包
捕获的底层开销与闭包是否“逃逸”密切相关:
| 闭包类型 | 存储位置 | 性能成本 |
|---|---|---|
| 非逃逸闭包 (@noescape) | 栈 (Stack) | 低。编译器确定闭包不会在函数结束后存在,因此无需将变量提升到堆。 |
| 逃逸闭包 (@escaping) | 堆 (Heap) | 高。必须在堆上分配 Context,并由 ARC 管理捕获的变量。 |
4. 捕获列表(Capture List)的作用
如果你不想使用默认的“引用捕获”,可以使用捕获列表 [variable]。
- 值拷贝:如果在捕获列表中写
[count],闭包会在定义那一刻拷贝count的当前值,存储在自己的 Context 中。后续外部对count的修改,闭包内部感知不到。 - 弱引用/无主引用:
[weak self]或[unowned self]也是通过捕获列表实现的。它告诉编译器在 Context 中存储一个弱引用指针,而不是强引用,从而打破循环引用。
5. 性能影响
- 堆分配开销:由于逃逸闭包需要分配堆内存,频繁创建大量捕获变量的闭包会增加内存压力。
- ARC 开销:引用捕获涉及原子性的引用计数加减,在高并发场景下可能存在微小的锁竞争。
总结
闭包是持有代码指针和堆上数据上下文的引用类型。它通过将栈变量“提升”到堆上,解决了函数返回后局部变量失效的问题。