Swfit进阶-18-Swift协议的原理

241 阅读8分钟

「这是我参与2022首次更文挑战的第19天,活动详情查看:2022首次更文挑战」。

  • 本文主要对Swift协议原理的探究

1.协议证明表(PWT)

我们之前在探究方法调度的时候知道方法是存储在vtable里面,其中对于值类型来说是通过静态派发的方式进行方法调度,引用类型则是函数表派发。其中extension都是静态派发详细可以看之前的文章。那么对于我们协议的方法是否也是存在vtable中呢?

1.1 witness_method

我们定义一个简单的协议,并实现它 image.png

编译成sil文件,在main函数中,test调用的是class_method,它是通过vtable进行调用的。

image.png

我们可以看下官方的定义SIL参考文档

说明对于class_method是通过vtable进行查找到该方法的实现

image.png 我们看下sil文件下面,确实在vtable中。

image.png

我们可以发现下面的witness_table中存放的是我们协议中的test方法,那么它的作用是什么呢?我们在vtable中已经有了。我们把p的静态类型定义为协议,动态类型还是实际的Person

image.png

进行编译后发现,在main函数中对于test调用变为了witness_method

image.png

查看witness_method说明

image.png

查找受该协议约束的泛型类型变量的协议方法的实现。结果将在原始协议的 Self 原型上是通用的,并具有 witness_method 调用约定。如果引用的协议是 @objc 协议,则结果类型具有 objc 调用约定。
我们继续看下协议中test的实现

image.png

最后还是去遵循这个协议的类中vtable查找该方法的实现,进行调用。

  • 小结:
    • 对于实例对象的静态类型是类的话,直接通过class_method调用,通过vtable进行查找到该方法的实现
    • 对于实例对象的静态类型是协议的话,通过witness_method调用,之后还是去遵循这个协议的类中vtable查找该方法的实现,进行调用。

1.2 对于在协议extension中实现

我们在协议的extension中实现方法,之后指定不同的静态类型进行调用

image.png

结果还是在类方法中进行调用,我们查看下sil文件

image.png

对于静态类型是协议的走的是witness_method,是类的话走的是class_method。对于协议方法witness_method最后还是走的是查找这个类vtable中的实现,和上面分析一样。

image.png

  • 对于协议中没有定义方法的情况

image.png

此时打印的结果就是按照各自对应静态类型的实现了,我们查看下sil文件

image.png

对于类的方法还是不变,而对于协议的类型则没有了witness_method,直接进行静态调用,因为在extension中的方法在编译的时候已经确定了,不会在查找函数表进行派发了。

image.png

sil_witness_table 中也没有我们test方法,说明sil_witness_table中的方法和我们协议中声明的方法相关联

1.3 值类型结构体调用协议方法

image.png

我们编译sil文件,可以发现对于值类型的结构体

image.png

实例对象是结构体的话直接静态调用function_ref,而对于静态类似是协议的话则先调用witness_method,我们继续看下协议的实现

image.png

也是结构体地址直接进行调用

image.png

协议没有定义的话和上面的分析类似

1.4 witness_table的继承关系

对于我们类继承多个协议会有几个sil_witness_table呢?

image.png

我们编译sil文件进行查看

image.png

当一个类继承多个协议的话,就会有多个sil_witness_table

  • 子类继承父类

image.png

查看sil代码

image.png

那么这个类的子类和父类是共用一份 sil_witness_table 的。

  • 一对多 一个协议被多个类遵循,那么sil_witness_table有几个

image.png

查看sil代码

image.png

多个sil_witness_table,一个类对应一个。

  • 小结:
    • 一个类遵循多个协议则会生成对应多个sil_witness_table
    • 一个类父类遵循协议,子类和父类共用一个sil_witness_table,子类无法再次遵循同样协议,只能重写父类实现协议的方法。
    • 多个类遵循统一额协议,每个类对应一个sil_witness_table

2. 协议证明表的组成和内存分布

上面我们知道witness_table用来存储协议中的方法,那么它是由什么组成的呢?

image.png

我们定义一个类,只是静态类型不同,最后分配的内存空间也不同,按理说都是引用类型,都应该是8字节,但是协议类型的话是40字节。

image.png

打印circle1的地址,指针正常指向内存地址,就是heapObject的结构体,我们打印存储的成员变量值 expression -f float -- 0x4024000000000000 也可以直接p/f 打印。

image.png

打印circle的地址,它是40字节我们打印5个连续的内存地址即可,我们第一个存储的还是实例对象的内存空间地址,打印内存地址,读取成员变量:10. 倒数第二个则是对象的metadata,可以看到和下面heapObject第一个内存地址相同,这个就是metadata。最后一个则是我们上面一直说的witness_table地址。我们验证下:

image.png

因此我们可以得出一个大致的协议的结构体类型

struct KBProtocolBox {

    var heapObject: UnsafeRawPointer

    var unkown1: UnsafeRawPointer

    var unkown2: UnsafeRawPointer

    var metadata: UnsafeRawPointer

    var witness_table: UnsafeRawPointer

}

2.1 witness_table 的内存结构

我们编译IR代码,可以看到和我们上面定义协议的结构体类似

image.png

我们还需要知道 witness_table 的内存结构,在 IR 中 witness_table 的结构如下:

image.png

可以看到存储了2个指针地址descriptorwitness,我们用结构体定义下

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,如图:

image.png

TargetProtocolConformanceDescriptor找到这个结构的定义,发现它有以下成员,如图:

image.png

其类型的结构为 TargetProtocolDescriptor,为指针类型。现在需要还原 TargetProtocolDescriptor 的结构,TargetProtocolDescriptor 是继承自 TargetContextDescriptorFlagsParent两个成员变量,我们再看一下它自身有什么,如图:

image.png

因此会有 image.png

最终还原出来的结构

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)")

    }

}

打印结果

image.png

2.3 Existential Container内存优化

我们把引用类型改为值类型

image.png 此时heapObject存储的就是20的值。

  • 我们添加3个属性值

image.png

  • 超过3个值的话

image.png

此时会在堆区生成一个heapObject对象,存放我们的成员变量。
Existential Container: 它是编译器生成的一种特殊的数据类型,用于管理遵守了相同协议的协议类型,因为这些类型的内存大小不一致,所以通过当前的 Existential Container 统一管理。 小结:

  • 对于小容量的数据,直接存储在 Value Buffer

  • 对于大容量的数据,通过堆区分配,存储堆空间的地址。

  • 如果这个实例是引用类型,那么第一个 8 字节存储的就是实例在堆空间的地址值

  • 如果这个实例是值类型,当着 24 个字节可以完全存储值类型的内存(也就是值类型的属性值),那么它就直接存储在这 24 个字节里。如果超出了 24 个字节,会通过堆区分配,然后第一个 8 字节存储堆空间的地址

3.总结

  1. 一个类遵循多个协议则会生成对应多个sil_witness_table;一个类父类遵循协议,子类和父类共用一个sil_witness_table,子类无法再次遵循同样协议,只能重写父类实现协议的方法;多个类遵循统一额协议,每个类对应一个sil_witness_table
  2. 通过上面的分析可知,协议的底层是一个40字节大小的结构体,前24字节称为valueBuffer,用来存储值。后16字节则由metadatawitness_table组成。
  3. 对于遵循协议引用类型的指针 在valueBuffer首地址直接存储的引用类型的heapObjec对象地址。而值类型则分为2种,如果成员变量大小小于等于24字节,则直接存储在valueBuffer中,大于24字节则会开辟新的堆内存空间,首地址存放的为heapObject变量,里面在存储我们值类型的成员变量。
  4. 对于witness_table 相当于一个过渡层,编译的时候在协议见证表中获取方法,但是还是会去遵循协议的类中查找方法