首先我们来看一段代码
protocol Drawable { func draw() }
struct Point :Drawable {
var x, y:Double
func draw() {
print(x, y)
}
}
struct Line :Drawable {
var x1, y1, x2, y2:Double
func draw() {
print(x1, y1, x2, y2)
}
}
let point = Point(x: 0, y: 0)
let line = Line(x1: 0, y1: 0, x2: 0, y2: 0)
var drawables:[Drawable] = [point, line]
for d in drawables {
d.draw()
}
我们知道,在数组当中存放的数据,它在内存中占据的大小都是一样的,但是Swift中可以将遵循同一个协议的类型存放在数组当中,这些类型的实际大小不同,但是可以存放在一起,那么 Protocol 类型的变量按什么进行内存布局?
这就要引入 Existential Container ,这个就是Swift用来管理 Protocol 变量布局内存
在swift的官方文档中, Existential Container被分为两类
Opaque Existential Containers :如果对协议或协议组合类型没有类约束,则存在容器必须容纳任意大小和对齐的值。它使用固定大小的缓冲区来执行此操作,该缓冲区大小为三个指针并且指针对齐
struct OpaqueExistentialContainer { void *fixedSizeBuffer[3]; Metadata *type; WitnessTable *witnessTables[NUM_WITNESS_TABLES]; };
fixedSizeBuffer
— 3 个指针大小的 buffer 空间,当真实类型的 size (内存对齐后的大小) 小于 3 个字时则其内容直接存储在 fixedSizeBuffer 中,否则在 heap 上另辟空间存储,并将指针存储在 fixedSizeBuffer 中;type
— 因为Protocol Type
的类型不同,内存空间,初始化方法等都不相同,为了对Protocol Type
生命周期进行专项管理,用到了Value Witness Table
(实例的初始化、拷贝、内存销毁)witnessTables
— 指向协议函数表 (Protocol Witness Table, PWT),协议函数表中存储的是真实类型中对应函数的地址
Class Existential Container :用于有类约束的 Protocol,该协议背后真实的类型只能是类,而类的实例都是在 Heap 上分配内存的
struct ClassExistentialContainer { HeapObject *value; WitnessTable *witnessTables[NUM_WITNESS_TABLES]; };
value
— 指向堆内存的指针witnessTables
— PWT 指针
下面我们来看上面那段代码中到底发生了什么
由于 protocol Drawable
没有 class constraint,故其对应的 Existential Container 是 OpaqueExistentialContainer
:
- 由于
Point
实例占用 2 个字的内存空间 (小于 3),故对于Drawable
协议类型的变量point
直接使用OpaqueExistentialContainer#buffer
来存储其内容(x
、y
); - 而
Line
的实例要占用 4 个字的内存空间,故line
需要在 heap 上分配内存用于存储其内容(x0
、y0
、x1
、y1
); - Existential Container 中的
type
分别指向了其背后真实类型的 Metadata - PWT 中的函数指针则指向真实类型中的函数
对应产生的伪代码如下:
point.draw()
// 编译器生成的(伪)代码
let _point: Point = Point(x: 0, y: 0)
var point: OpaqueExistentialContainer = OpaqueExistentialContainer()//初始化 container
let metadata = _point.type
let vwt = metadata.vwt//获取VWT管理生命周期
vwt.copy(&(point.fixedSizeBuffer), _point) //拷贝
point.type = metadata
point.witnessTables = PWT(Point.draw)//获取PWT用于方法派发
point.pwt.draw() //查找方法时是通过当前对象的地址,通过一定的位移去查找方法地址
vwt.dealloc(point) //vwt进行生命周期管理,销毁内存
由此可见对于协议类型的变量,编译器在背后实现了大量的工作
写这篇文章也是因为在喵神关于面向协议编程提到的一个例子,想要深入了解一下背后的内容,也算是提升了自己阅读英文文档的能力吧
参考资料
https://github.com/apple/swift/blob/main/docs/ABI/TypeLayout.rst