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

41 阅读5分钟

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 开销:引用捕获涉及原子性的引用计数加减,在高并发场景下可能存在微小的锁竞争。

总结

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

英文版

8-12. [Memory Management] Are closures value types or reference types? What is the underlying principle of variable capture?


1. Closures are Reference Types

In Swift, closures are reference types.

When you assign a closure to a variable or pass it to another function, you are actually passing a pointer to the closure entity, rather than copying the closure’s code or its captured state.

  • Proof: If you assign the same closure to two different variables and have one variable perform an operation that modifies a captured variable, the other variable will see the same change when executed.

2. The Underlying Principle of Variable Capture

The core of closure capture lies in extending the lifetime of variables.

Normally, local variables are stored on the Stack and are destroyed once the function finishes executing. However, if a closure captures them, Swift "promotes" those variables from the Stack to the Heap to ensure they remain valid whenever the closure is executed in the future.

Core Structure: The Closure Box

At the low level, a closure is actually a structure (often referred to as a ClosureBox or Thunk) that contains two main parts:

  1. Function Pointer: Points to the compiled code logic inside the closure.
  2. Capture Context: A pointer to a heap-allocated memory block where all captured variables are stored.
Capture Method: Capture by Reference

By default, Swift closures capture a reference to the variable, not the value at that specific moment.

  • For Value Types (Int, Struct) : Swift creates a "Box" on the heap. Both the closure's internal logic and the original function scope share the pointer to this heap-resident Box. This explains why modifying a variable inside a closure reflects that change externally.
  • For Reference Types (Class instances) : The closure captures the pointer to the instance and triggers an ARC (Automatic Reference Counting) operation, incrementing the object's reference count by 1.

3. Escaping vs. Non-Escaping Closures

The underlying overhead of capture is closely tied to whether the closure is "escaping":

Closure TypeStorage LocationPerformance Cost
Non-escaping (@noescape)StackLow. The compiler knows the closure won't exist after the function ends, so no heap promotion is needed.
Escaping (@escaping)HeapHigh. A Context must be allocated on the heap, and captured variables are managed by ARC.

4. The Role of the Capture List

If you do not want to use the default "capture by reference," you can use a Capture List [variable].

  • Value Copy: If you write [count] in the capture list, the closure copies the current value of count at the moment of definition and stores it in its Context. Subsequent changes to count outside the closure will not be perceived inside.
  • Weak/Unowned References: [weak self] or [unowned self] are implemented via the capture list. They instruct the compiler to store a weak pointer in the Context instead of a strong one, breaking retain cycles.

5. Performance Implications

  1. Heap Allocation Overhead: Because escaping closures require heap memory, frequently creating closures that capture many variables can increase memory pressure.
  2. ARC Overhead: Capturing by reference involves atomic reference counting increments and decrements, which can cause minor lock contention in high-concurrency scenarios.

Summary

A closure is a reference type that holds a function pointer and a data context on the heap. It solves the problem of local variables becoming invalid after a function returns by "promoting" stack variables to the heap.