8-12.【内存管理机制】补充:闭包是值类型还是引用类型?闭包捕获变量的底层原理是什么?

1 阅读3分钟

1. 闭包是引用类型

在 Swift 中,闭包是引用类型(Reference Type)

当你将一个闭包赋值给变量或将其传递给另一个函数时,你实际上是在传递一个指向该闭包实体的指针,而不是拷贝闭包的代码或其捕获的状态。

  • 证明方法:如果你将同一个闭包赋值给两个不同的变量,并让其中一个变量执行修改捕获变量的操作,另一个变量在执行时也会看到同样的变化。

2. 闭包捕获变量的底层原理

闭包捕获(Capture)的底层核心在于:提升变量的生命周期

通常局部变量存储在**栈(Stack)上,函数执行完就会被销毁。但如果闭包捕获了它,为了保证闭包在未来任何时候执行时变量依然有效,Swift 会将该变量从栈“提升”到堆(Heap)**上。

核心结构:HeapContext

在底层,闭包实际上是一个结构体(通常称为 ClosureBoxThunk),它包含两个部分:

  1. 函数指针:指向闭包内执行的代码逻辑。
  2. 捕获上下文(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. 性能影响

  1. 堆分配开销:由于逃逸闭包需要分配堆内存,频繁创建大量捕获变量的闭包会增加内存压力。
  2. ARC 开销:引用捕获涉及原子性的引用计数加减,在高并发场景下可能存在微小的锁竞争。

总结

闭包是持有代码指针和堆上数据上下文的引用类型。它通过将栈变量“提升”到堆上,解决了函数返回后局部变量失效的问题。