Existential Container

首页 / iOS / 正文

首先我们来看一段代码

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 指针

下面我们来看上面那段代码中到底发生了什么

截屏2022-03-13 下午4.52.31.png

由于 protocol Drawable 没有 class constraint,故其对应的 Existential Container 是 OpaqueExistentialContainer

  • 由于 Point 实例占用 2 个字的内存空间 (小于 3),故对于 Drawable 协议类型的变量 point 直接使用 OpaqueExistentialContainer#buffer 来存储其内容(xy);
  • Line 的实例要占用 4 个字的内存空间,故 line 需要在 heap 上分配内存用于存储其内容(x0y0x1y1);
  • 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

https://www.jianshu.com/p/c93d7a7d6771

https://mp.weixin.qq.com/s/U95QmOOjeXkk-yC23cuZCQ

无标签
评论区
头像
文章目录