在 Swift 运行时(Runtime),any 类型(即 Existential Type)的存储和分发是由一个名为 Existential Container(存在容器) 的结构体负责的。
为了让你在运行时能对一个“抹去了具体类型”的对象进行操作,容器必须不仅保存数据,还要保存如何处理这些数据的指令。
以下是其在内存中的详细布局和工作机制:
1. 内存布局:5-Word 结构
在 64 位系统下,一个 any P(P 是一个协议)通常占用 40 字节,分为三部分:
| 组成部分 | 长度 | 作用 |
|---|---|---|
| Value Buffer | 3 Words (24 字节) | 存储值本身或指向堆内存的指针。 |
| Metadata Pointer | 1 Word (8 字节) | 指向具体类型的 Metadata(元数据)。 |
| PWT Pointer | 1 Word (8 字节) | 指向 Protocol Witness Table(协议见证表)。 |
注意:如果
any组合了多个协议(如any P1 & P2),容器会变长,增加更多的 PWT 指针。
2. Metadata 与 VWT:管理生命周期
Metadata Pointer 指向具体类型(如 String 或 MyClass)的描述信息。通过这个指针,运行时可以找到该类型的 VWT (Value Witness Table) 。
-
VWT 的作用:它包含了一组通用的函数指针,处理与协议逻辑无关的底层操作:
allocate: 如何为这个类型分配内存?copy: 如何拷贝这个值(是按位拷贝还是增加引用计数)?destruct: 如何销毁这个值?
-
运行时决策:当你把一个
any变量赋值给另一个变量时,Runtime 会查找 VWT 中的copy函数。由于 VWT 是根据具体类型生成的,它知道该执行深拷贝还是简单的指针赋值。
3. PWT (Protocol Witness Table):实现动态分发
这是 any 类型能够调用协议方法的关键。PWT 是一个存储了函数指针的数组。
-
内部映射:如果你定义了协议
protocol Drawable { func draw() },那么 PWT 里就会存着一个指向具体实现类(比如Circle)中draw函数的指针。 -
调用逻辑:当你执行
anyValue.draw()时,发生了以下伪指令操作:- 从 Existential Container 中读出 PWT 指针。
- 在 PWT 表中根据偏移量找到
draw函数的地址。 - 从 Value Buffer 中取出数据(作为
self参数)。 - 执行函数。
4. Value Buffer 的两种形态(运行时转换)
Runtime 会根据具体类型的大小动态决定 Buffer 的用法:
- Inline (内联) :如果具体类型 字节,VWT 的
copy指令会将数据直接写入这 24 字节中。 - Outline (堆分配) :如果具体类型 字节,Runtime 会在堆上分配空间。此时 Value Buffer 的前 8 字节存储堆地址。VWT 会负责管理这块堆内存的引用计数和释放。
5. 举个例子:当你调用 any 的方法时
假设有 let a: any Drawable = Circle():
-
存储阶段:容器装入了
Circle的实例(或指针),存入了Circle的 Metadata 地址,以及Circle遵循Drawable协议的 PWT 地址。 -
调用阶段 (
a.draw()):- 编译器生成的代码不直接查找
Circle.draw。 - 它跳转到容器中的 PWT。
- PWT 说:“对于这个
Circle实例,draw函数在地址0x1234”。 - 程序跳转到
0x1234执行代码。
- 编译器生成的代码不直接查找
6. 性能影响总结
相比 some,any 在运行时的额外开销体现在:
- 间接寻址:必须经过 Metadata 和 PWT 两层指针查找才能找到函数地址(无法内联)。
- 内存压力:大型结构体会触发动态内存分配(Heap Allocation)。
- Witness 表查找:每次调用协议方法都是一次动态分发。
这就是为什么 Swift 团队在 Swift 5.7+ 中极力推崇使用 some(Opaque types),除非你确实需要在一个集合里存储不同类型的对象(Heterogeneous collection)。