在 Swift 底层,Any 类型的变量并不是直接存储数据的,而是通过一个被称为 Existential Container(存在容器) 的结构来管理的。
由于 Any 可以代表任何类型(从小型的 Int 到大型的 Struct,再到指针形式的 Class),它们的内存大小各不相同。为了让编译器能以统一的方式处理这些大小不一的类型,Swift 引入了 Existential Container。
一个标准的 Existential Container 占用 5个内存字(Words) ,在 64 位架构下共 40 字节。其内部结构如下:
1. 内存结构布局
Existential Container 由三个部分组成:
- Value Buffer(值缓冲区) :占 3 个字(24 字节)。用于直接存储值或存储指向堆内存的指针。
- VWT (Value Witness Table,值见证表) :占 1 个字(8 字节)。负责管理值的生命周期(拷贝、销毁、分配)。
- 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 个 Double 的 Struct):
- 堆分配:Swift 会在堆(Heap)上开辟一块空间来存放这个结构体。
- 存储指针:Value Buffer 的第一个字(First Word)会存储指向该堆内存地址的指针。
- 性能损耗:由于涉及堆分配和引用计数管理(尽管是值类型,但容器需要管理这块内存的生命周期),性能会比直接存储低。
3. 存储引用类型 (Class)
当你在 Any 中存储类实例时,情况相对简单:
- 存储指针:因为 Class 本身就是引用类型,其本质就是一个指针。
- Value Buffer:Value Buffer 的第一个字会存储指向该类实例在堆中地址的指针。
- 引用计数:存入和取出时,VWT 会负责调用
swift_retain和swift_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。