「这是我参与2022首次更文挑战的第19天,活动详情查看:2022首次更文挑战」。
- 本文主要对
Swift协议原理的探究
1.协议证明表(PWT)
我们之前在探究方法调度的时候知道方法是存储在vtable里面,其中对于值类型来说是通过静态派发的方式进行方法调度,引用类型则是函数表派发。其中extension都是静态派发,详细可以看之前的文章。那么对于我们协议的方法是否也是存在vtable中呢?
1.1 witness_method
我们定义一个简单的协议,并实现它
编译成sil文件,在main函数中,test调用的是class_method,它是通过vtable进行调用的。
我们可以看下官方的定义SIL参考文档
说明对于class_method是通过vtable进行查找到该方法的实现
我们看下sil文件下面,确实在
vtable中。
我们可以发现下面的witness_table中存放的是我们协议中的test方法,那么它的作用是什么呢?我们在vtable中已经有了。我们把p的静态类型定义为协议,动态类型还是实际的Person
进行编译后发现,在main函数中对于test调用变为了witness_method
查看witness_method说明
查找受该协议约束的泛型类型变量的协议方法的实现。结果将在原始协议的 Self 原型上是通用的,并具有 witness_method 调用约定。如果引用的协议是 @objc 协议,则结果类型具有 objc 调用约定。
我们继续看下协议中test的实现
最后还是去遵循这个协议的类中vtable查找该方法的实现,进行调用。
- 小结:
- 对于实例对象的静态类型是类的话,直接通过
class_method调用,通过vtable进行查找到该方法的实现 - 对于实例对象的静态类型是协议的话,通过
witness_method调用,之后还是去遵循这个协议的类中vtable查找该方法的实现,进行调用。
- 对于实例对象的静态类型是类的话,直接通过
1.2 对于在协议extension中实现
我们在协议的extension中实现方法,之后指定不同的静态类型进行调用
结果还是在类方法中进行调用,我们查看下sil文件
对于静态类型是协议的走的是witness_method,是类的话走的是class_method。对于协议方法witness_method最后还是走的是查找这个类vtable中的实现,和上面分析一样。
- 对于协议中
没有定义方法的情况
此时打印的结果就是按照各自对应静态类型的实现了,我们查看下sil文件
对于类的方法还是不变,而对于协议的类型则没有了witness_method,直接进行静态调用,因为在extension中的方法在编译的时候已经确定了,不会在查找函数表进行派发了。
sil_witness_table 中也没有我们test方法,说明sil_witness_table中的方法和我们协议中声明的方法相关联。
1.3 值类型结构体调用协议方法
我们编译sil文件,可以发现对于值类型的结构体
实例对象是结构体的话直接静态调用function_ref,而对于静态类似是协议的话则先调用witness_method,我们继续看下协议的实现
也是结构体地址直接进行调用
协议没有定义的话和上面的分析类似。
1.4 witness_table的继承关系
对于我们类继承多个协议会有几个sil_witness_table呢?
我们编译sil文件进行查看
当一个类继承多个协议的话,就会有多个sil_witness_table
- 子类继承父类
查看sil代码
那么这个类的子类和父类是共用一份 sil_witness_table 的。
- 一对多
一个协议被
多个类遵循,那么sil_witness_table有几个
查看sil代码
多个sil_witness_table,一个类对应一个。
- 小结:
- 一个类遵循多个协议则会生成对应多个
sil_witness_table - 一个类父类遵循协议,子类和父类共用一个
sil_witness_table,子类无法再次遵循同样协议,只能重写父类实现协议的方法。 - 多个类遵循统一额协议,每个类对应一个
sil_witness_table。
- 一个类遵循多个协议则会生成对应多个
2. 协议证明表的组成和内存分布
上面我们知道witness_table用来存储协议中的方法,那么它是由什么组成的呢?
我们定义一个类,只是静态类型不同,最后分配的内存空间也不同,按理说都是引用类型,都应该是8字节,但是协议类型的话是40字节。
打印circle1的地址,指针正常指向内存地址,就是heapObject的结构体,我们打印存储的成员变量值
expression -f float -- 0x4024000000000000 也可以直接p/f 打印。
打印circle的地址,它是40字节我们打印5个连续的内存地址即可,我们第一个存储的还是实例对象的内存空间地址,打印内存地址,读取成员变量:10. 倒数第二个则是对象的metadata,可以看到和下面heapObject的第一个内存地址相同,这个就是metadata。最后一个则是我们上面一直说的witness_table地址。我们验证下:
因此我们可以得出一个大致的协议的结构体类型
struct KBProtocolBox {
var heapObject: UnsafeRawPointer
var unkown1: UnsafeRawPointer
var unkown2: UnsafeRawPointer
var metadata: UnsafeRawPointer
var witness_table: UnsafeRawPointer
}
2.1 witness_table 的内存结构
我们编译IR代码,可以看到和我们上面定义协议的结构体类似
我们还需要知道 witness_table 的内存结构,在 IR 中 witness_table 的结构如下:
可以看到存储了2个指针地址descriptor和witness,我们用结构体定义下
struct TargetWitnessTable{
var protocol_conformance_descriptor:UnsafeRawPointer
var witnessMethod: UnsafeRawPointer
}
综合下:
struct KBProtocolBox {
var heapObject: UnsafeRawPointer
var unkown1: UnsafeRawPointer
var unkown2: UnsafeRawPointer
var metadata: UnsafeRawPointer
var witness_table: UnsafeMutablePointer<TargetWitnessTable>
}
struct TargetWitnessTable{
var protocol_conformance_descriptor:UnsafeRawPointer
var witnessMethod: UnsafeRawPointer
}
2.2 源码分析
我们全局搜索 TargetWitnessTable,在 Metadata.h 文件中找到 TargetWitnessTable,如图:
TargetProtocolConformanceDescriptor找到这个结构的定义,发现它有以下成员,如图:
其类型的结构为 TargetProtocolDescriptor,为指针类型。现在需要还原 TargetProtocolDescriptor 的结构,TargetProtocolDescriptor 是继承自 TargetContextDescriptor 。Flags 和Parent两个成员变量,我们再看一下它自身有什么,如图:
因此会有
最终还原出来的结构
struct KBProtocolBox {
var heapObject: UnsafeRawPointer
var unkown1: UnsafeRawPointer
var unkown2: UnsafeRawPointer
var metadata: UnsafeRawPointer
var witness_table: UnsafeMutablePointer<TargetWitnessTable>
}
struct TargetWitnessTable{//协议见证表
var protocol_conformance_descriptor: UnsafeMutablePointer<TargetProtocolConformanceDescriptor>
var witnessMethod: UnsafeRawPointer
}
struct TargetProtocolConformanceDescriptor{
var protocolDesc: TargetRelativeDirectPointer<TargetProtocolDescriptor>
var typeRef: UnsafeRawPointer
var WitnessTablePattern: UnsafeRawPointer
var flags: UInt32
}
struct TargetProtocolDescriptor
{
var flags: UInt32
var parent: TargetRelativeDirectPointer<UnsafeRawPointer>
var Name: TargetRelativeDirectPointer<CChar>
var NumRequirementsInSignature: UInt32
var NumRequirements: UInt32
var AssociatedTypeNames: TargetRelativeDirectPointer<CChar>
}
//通过偏移获取指针指向的值
struct TargetRelativeDirectPointer<Pointee>{
var offset: Int32
mutating func getmeasureRelativeOffset() -> UnsafeMutablePointer<Pointee>{
let offset = self.offset
return withUnsafePointer(to: &self) { p in
return UnsafeMutablePointer(mutating: UnsafeRawPointer(p).advanced(by: numericCast(offset)).assumingMemoryBound(to: Pointee.self))
}
}
}
我们验证下
var circle: Shape = Circle(20)
withUnsafePointer(to: &circle) { c2_ptr in
c2_ptr.withMemoryRebound(to: KBProtocolBox.self, capacity: 1) { pis_ptr in
print(pis_ptr.pointee)
let protocolDesPtr = pis_ptr.pointee.witness_table.pointee.protocol_conformance_descriptor.pointee.protocolDesc.getmeasureRelativeOffset()
print("协议名称:\(String(cString: protocolDesPtr.pointee.Name.getmeasureRelativeOffset()))")
print("协议方法的数量:\(protocolDesPtr.pointee.NumRequirements)")
print("witnessMethod:\(pis_ptr.pointee.witness_table.pointee.protocol_conformance_descriptor)")
}
}
打印结果
2.3 Existential Container内存优化
我们把引用类型改为值类型
此时heapObject存储的就是20的值。
- 我们添加3个属性值
- 超过3个值的话
此时会在堆区生成一个heapObject对象,存放我们的成员变量。
Existential Container: 它是编译器生成的一种特殊的数据类型,用于管理遵守了相同协议的协议类型,因为这些类型的内存大小不一致,所以通过当前的 Existential Container 统一管理。
小结:
-
对于小容量的数据,直接存储在
Value Buffer。 -
对于大容量的数据,通过
堆区分配,存储堆空间的地址。 -
如果这个实例是
引用类型,那么第一个8字节存储的就是实例在堆空间的地址值。 -
如果这个实例是
值类型,当着24个字节可以完全存储值类型的内存(也就是值类型的属性值),那么它就直接存储在这24个字节里。如果超出了24个字节,会通过堆区分配,然后第一个 8 字节存储堆空间的地址。
3.总结
- 一个类遵循多个协议则会生成对应多个
sil_witness_table;一个类父类遵循协议,子类和父类共用一个sil_witness_table,子类无法再次遵循同样协议,只能重写父类实现协议的方法;多个类遵循统一额协议,每个类对应一个sil_witness_table。 - 通过上面的分析可知,协议的底层是一个
40字节大小的结构体,前24字节称为valueBuffer,用来存储值。后16字节则由metadata和witness_table组成。 - 对于
遵循协议是引用类型的指针 在valueBuffer首地址直接存储的引用类型的heapObjec对象地址。而值类型则分为2种,如果成员变量大小小于等于24字节,则直接存储在valueBuffer中,大于24字节则会开辟新的堆内存空间,首地址存放的为heapObject变量,里面在存储我们值类型的成员变量。 - 对于
witness_table相当于一个过渡层,编译的时候在协议见证表中获取方法,但是还是会去遵循协议的类中查找方法。