7-12.【高级特性】Existential Container 在使用 Any 类型时如何存储值和引用?

3 阅读3分钟

在 Swift 底层,Any 类型的变量并不是直接存储数据的,而是通过一个被称为 Existential Container(存在容器) 的结构来管理的。

由于 Any 可以代表任何类型(从小型的 Int 到大型的 Struct,再到指针形式的 Class),它们的内存大小各不相同。为了让编译器能以统一的方式处理这些大小不一的类型,Swift 引入了 Existential Container。

一个标准的 Existential Container 占用 5个内存字(Words) ,在 64 位架构下共 40 字节。其内部结构如下:


1. 内存结构布局

Existential Container 由三个部分组成:

  1. Value Buffer(值缓冲区) :占 3 个字(24 字节)。用于直接存储值或存储指向堆内存的指针。
  2. VWT (Value Witness Table,值见证表) :占 1 个字(8 字节)。负责管理值的生命周期(拷贝、销毁、分配)。
  3. PWT (Protocol Witness Table,协议见证表) :占 1 个字(8 字节)。如果 Any 实际上是某个协议类型,这里存储该协议的方法映射;对于纯 Any,这部分逻辑稍有不同,但结构是相似的。

2. 存储值类型 (Struct / Enum)

当你在 Any 中存储值类型时,Swift 会根据其大小采取不同的策略:

A. 小型值类型(≤ 24 字节)

如果值的大小(如 Int, Double, 或包含少量属性的 Struct)小于或等于 24 字节:

  • 直接存储:值会直接填充在 Value Buffer 的 3 个字空间内。
  • 无需堆分配:这种情况下性能很高,没有额外的内存分配开销。

B. 大型值类型(> 24 字节)

如果结构体非常大(比如一个包含 10 个 DoubleStruct):

  • 堆分配:Swift 会在堆(Heap)上开辟一块空间来存放这个结构体。
  • 存储指针Value Buffer 的第一个字(First Word)会存储指向该堆内存地址的指针。
  • 性能损耗:由于涉及堆分配和引用计数管理(尽管是值类型,但容器需要管理这块内存的生命周期),性能会比直接存储低。

3. 存储引用类型 (Class)

当你在 Any 中存储类实例时,情况相对简单:

  • 存储指针:因为 Class 本身就是引用类型,其本质就是一个指针。
  • Value BufferValue Buffer 的第一个字会存储指向该类实例在堆中地址的指针。
  • 引用计数:存入和取出时,VWT 会负责调用 swift_retainswift_release 来维护该对象的引用计数。

4. VWT 的核心作用

无论存储的是什么,Any 容器都必须知道如何处理它。VWT (Value Witness Table) 是关键:

  • 当你把 Any 赋值给另一个 Any 时,容器会查看 VWT,如果是小型值则直接按位拷贝 Buffer,如果是大型值或引用类型,则通过 VWT 进行深拷贝或增加引用计数。
  • Any 变量作用域结束时,VWT 负责释放内存。

总结对比

存储类型存储位置Value Buffer 内容性能
小 Struct (≤24B)栈(Container 内)实际数据内容高(无堆开销)
大 Struct (>24B)堆(Heap)指向堆的指针中(有堆开销)
Class (引用类型)堆(Heap)指向堆对象的指针中(有 ARC 开销)

为什么这很重要?

理解这一点能帮你优化 Swift 性能:频繁在 Any 或协议类型中传递大型结构体会导致意外的堆分配开销。 如果对性能有极致要求,建议使用泛型(Generics),因为泛型在编译期会进行“特化(Specialization)”,从而避免使用这种昂贵的 Existential Container。