Swift中遵循协议的值类型是如何进行内存分配的?
Swift 协议类型值的内存分配原理
你之前的认知存在偏差:遵循同一协议的不同值类型本身的内存大小并不统一,Swift通过**存在容器(Existential Container)**的封装机制,实现了用统一大小的类型包装不同大小的值类型实例,从而支持协议类型的泛化使用。
存在容器的基础结构
常规的非@objc、无关联类型的协议作为类型使用时,每个实例对应的存在容器固定为5个机器字长度(64位系统下为40字节),内部结构如下:
- 前3个机器字为「内联值缓冲区」:如果实际值类型的实例大小 ≤ 3个机器字,实例会直接存在这个缓冲区中,完全不需要堆内存分配,访问效率和普通值类型一致
- 第4个机器字为「值见证表(Value Witness Table, VWT)」指针:每个具体类型独有一份值见证表,表中存储了该类型的内存大小、对齐规则、拷贝、销毁等生命周期相关操作的函数指针,Swift通过这个表处理不同大小类型的通用操作
- 第5个机器字为「协议见证表(Protocol Witness Table, PWT)」指针:存储了当前类型实现对应协议方法的函数指针,用于运行时的协议方法派发
如果实际值类型的实例大小超过3个机器字,Swift会自动在堆上分配内存存储这个实例,再把堆内存的地址存在前3个机器字的内联缓冲区中,此时访问实例需要一次间接寻址,开销和引用类型接近。
举个实际的代码示例方便理解:
protocol Shape { func area() -> Double } // 仅存一个Double,大小8字节 < 3*8=24字节,直接内联到存在容器中 struct Circle: Shape { let radius: Double func area() -> Double { .pi * radius * radius } } // 存4个Double,大小32字节 > 24字节,实例分配到堆上,容器中仅存指针 struct Rectangle: Shape { let originX, originY, width, height: Double func area() -> Double { width * height } }
特殊场景的差异
- 如果是类类型遵循协议,存在容器的内联缓冲区直接存储类的堆引用,不需要额外的堆分配,因为类实例本身就存储在堆上
- 标记为
@objc的协议作为类型使用时,存在容器仅需1个机器字大小,完全复用Objective-C的消息派发机制,不需要存储两份见证表 - 带关联类型的协议(Protocol with Associated Type, PAT)不能直接作为存在类型使用,编译期就会报错,因为这类协议的见证表结构无法在编译期确定,必须通过泛型约束使用
你之前的假设其实刚好是存在容器的设计目标:对外暴露的协议类型实例大小固定,内部自动处理不同大小值的存储逻辑,既保留了值类型的值语义,又实现了协议的动态泛化能力。
内容的提问来源于stack exchange,提问作者NepoSSEDA
相关产品推荐
相关产品推荐

