Swift学习(八)协议Protocol

619 阅读7分钟

协议的基本语法

定义协议

在Swift开发中通过关键字Protocol来声明一个协议。

//定义一个协议
protocol OSProtocolOne {
}

协议中可以声明方法。

protocol OSProtocolOne {
    //声明方法
    func func1()
}

协议中也可以定义属性,但必须是(get)、( get set)类型的,并且必须是变量即只能用var修饰。

image.png

image.png

protocol OSProtocolOne {
    func func1()
    var age: Int { get }
}

注意⚠️:协议中允许定义构造函数,如果是类遵循协议,构造函数需要使用关键字request修饰,或者该累使用final修饰。

遵循协议

Swfit中class、struct、enum都可以遵循协议,如果遵循多个协议,可以使用逗号隔开,也可以分成多个Extention来书写。 遵循协议后必须实现协议中定义的方法和属性。

image.png

class OSComputer: OSProtocolOne {
    func func1() {
        print(#function)
    }
    
    var age: Int = 0
}

extension OSComputer: OSProtocolTwo {

}

和OC交互

如果OC类需要遵循协议,需要在协议前面增加关键字@objc,如果想使用OC的optionl的属性,需要在方法或者属性添加关键字@objc optional

@objc protocol OSProtocolOne {
    //可选的
    @objc optional func func1()
    var age: Int { get }
}

这样我们就定义了一个可以和OC交互的协议了。

协议方法的调度

SIL分析

通过前文方法,了解到了方法的存储方式是通过V-table来存储的,这里我们用同样的方式来探索协议中的方法调度方式。 编辑如下代码,并编译成SIL语言。

//sil编译命令
swiftc -emit-sil fileName >outputFileName
protocol OSProtocolTest {
    func func11()
}

class OSProtoclProduct: OSProtocolTest{
    //协议的方法
    func func11() {
        print("11")
    }
    //OSProtoclProduct类的方法,在V-table中。
    func func12() {
        print(12)
    }
}

func enter() {
    let product = OSProtoclProduct()
    product.func11()
    product.func12()
}

在SIL中找到enter函数对应的代码。

image.pngSIL官方文档中查找class_method的相关资料。

image.png 可以看到,class_method是通过VTables来调用的。而在SIL语言中也可以找到对应vtable中func11的定义,如下图:

image.png

如果我们在定义product的时候,声明product的类型为OSProtocolTest,同样的方式来查看调用。 修改代码如下:

func enter() {
    let product: OSProtocolTest = OSProtoclProduct()
    product.func11()
    //func12不在OSProtocolTest的定义内,所以不能被调用。
//    product.func12()
}

编译成SIL语言后,可以看到调用方式发生了改变,这次使用了witness_method. image.png 在SIL官方文档中找到witness_method的定义:

image.png

翻译比较难理解。

image.png 大致意思是,去遵循协议的类型中去找该方法的实现,会使用witness_method方式调度,如果协议使用@objc修饰,会变成objc的调用方式。

继续看SIL语言中:

image.png 这里的witness_table中也有一个函数定义,大家都叫witness可以大胆猜测,witness_method是去witness_table中去寻找实现。而在witness_table的定义中也可以证实这一猜想。

image.png

那么我们继续看编译后的SIL语言中关于witnss_tablefun11的定义:

image.png 这个方法里面然后再通过class_method去调用vtable中的fun11

小结:如果实例类型是真正类型,方法通过vtable调度,如果实例类型为协议类型,方法需要通过witness_table包装(或者说桥接)一层再去vtable调度。

注意⚠️:

  • witness_table和类是一一对应的。如果多个类遵循同一个协议,将会有多个 witness_table
  • 父类遵循协议,子类也会有对应的witness_table

汇编分析

除了SIL分析之外,我们还可以通过汇编代码调试来分析协议方法调度的过程。首先在函数调度的代码行下断点,然后打开汇编调试。 打开汇编调试 xcode -> Debug -> Debug Workflow -> Always Show Disassemple 。 汇编中的函数调用指令为 blr ,我们主要看blr 寄存器中的内容。汇编调试过程中可以通过Contro健+点击⬇️来控汇编代码逐行执行(Xcode13中),Xcode12可以直接下断点调试。注意:本次汇编分析的是arm架构,如果不是M1芯片的电脑,需要使用真机。

image.png 执行到上图的时候,继续Contro健+点击⬇️,就可以进入函数内部,可以看到这个函数就是witness-table中的func11。 而汇编代码中blr只有一句,这一句也就对应来SIL中class_mathod func11,即调用vtable中的func11image.png

image.png 通过打印x8寄存器,可以看到这里的的确确是调用了func11函数。

分类实现协议方法

协议可以通过extention的方式去实现定义的方法,实现之后,遵循协议的类可以不再实现该方法。

protocol OSProtocolTest {
    func func11()
}

extension OSProtocolTest {
    func func11() {
        print("11 from protocol extension")
    }
}

class OSProtoclProduct: OSProtocolTest{
    //协议的方法
//    func func11() {
//        print("11")
//    }
    //OSProtoclProduct类的方法,在V-table中。
    func func12() {
        print(12)
    }
}

如果遵循协议的类,和协议的分类都实现了协议方法,会优先调用类中的方法。如果协议中没有声明方法,调用时需要看实例的具体类型。

protocol OSProtocolTest {

}

extension OSProtocolTest {
    func func11() {
        print("11 from protocol extension")
    }
}

class OSProtoclProduct: OSProtocolTest{
    //协议的方法
    func func11() {
        print("11 from OSProtoclProduct")
    }
    //OSProtoclProduct类的方法,在V-table中。
    func func12() {
        print(12)
    }
}

执行结果:

image.png 可以大致看一下编译后的SIL语言。

image.png 作为静态语言,swift编译器在决定函数调用的时候,会根据静态类型调用函数。

协议的结构

类型还原

通过上面的例子,我们发现静态类型不同,会影响函数调用,是不是会影响实例对象的结构呢?带着疑问我们继续探索:

func func3() {
        let product1:OSProtoclProduct = OSProtoclProduct()
        print(MemoryLayout.size(ofValue: product1))
        
        let product2: OSProtocolTest = OSProtoclProduct()
        print(MemoryLayout.size(ofValue: product2))
    }

image.png 惊呆了,老铁!静态类型不同,居然会影响内存中的大小。product1的8字节很好理解,是实例对象的地址。 那我们就再分析下这个40字节的内容了。

image.png

位置内容
第0个8字节实例对象的地址
第1个8字节未知
第2个8字节未知
第3个8字节实例对象的metadate
第4个8字节witnesstable

得到最最粗略的结构:

struct OSProtocolBox {
    var heapObject: UnsafeRawPointer
    var unkonw1: UnsafeRawPointer
    var unkonw2: UnsafeRawPointer
    var metadata: UnsafeRawPointer
    var witnessTable: UnsafeRawPointer
}

经过一系列复杂的探究(源码、IL代码、查资料等方式)这里不做赘述,终于有了这个所有的结构。

struct OSProtocolBox {
    var heapObject: UnsafeRawPointer
    var unkonw1: UnsafeRawPointer
    var unkonw2: UnsafeRawPointer
    var metadata: UnsafeRawPointer
    var witnessTable: 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>
}

重新绑定

有了结构之后,我们通过内存重新绑定,可以动态获取到协议的更多信息了。

withUnsafePointer(to: &product2) { ptr in
            ptr.withMemoryRebound(to: OSProtocolBox.self, capacity: 1) { ptr1 in
                let name = ptr1.pointee.witnessTable.pointee.protocol_conformance_descriptor.pointee.protocolDesc.getmeasureRelativeOffset().pointee.Name.getmeasureRelativeOffset()
                print(String(cString: name))
            }
        }

image.png 可以看到协议的名称,同样的方法还可以打印出遵循协议的方法地址。

image.png 可以通过

$ nm-p mach-o | grep 函数地址
$ xcrun swift-demangle <string>

来证实。 也可以通过汇编调试的打印来证实。

image.png 可以看到两个地址是一样的。

Existential Container

上面的OSProtocolBox其实是存在容器, Existential Container 是编译器生成的一种特殊的数据类型,用于管理遵守了相同协议的协议类型,因为这些类型的内存大小不一致,所以通过当前的 Existential Container 统一管理。也就是说不管是class、struct、enum,只要遵循了协议,就会由这个Existential Container来统一管理他们的内存。

valueBuffer

我们使用一个结构体来遵循协议,来看一看Existential Container的变化。

struct OSProtocolCoder: OSProtocolTest {
    var age: Int = 20
    var level: Int = 2
}
func func4() {
        var coder: OSProtocolTest = OSProtocolCoder()
        print(coder)
    }

image.png 可以看到这里把结构体的属性的值存在了第0个8字节,和第1个8字节。那就想知道如果有更多个属性会有什么样的变化。

struct OSProtocolCoder: OSProtocolTest {
    var age: Int = 20
    var level: Int = 2
    var leve11: Int = 2
    var level2: Int = 2
    var level3: Int = 2
}

image.png 好家伙,这是存不下了,在堆区开辟地址去存了。

总结:Existential Container的前24个字节为Value Buffer 对于小容量的数据(小于24字节),直接存储在 Value Buffer中, 对于大容量的数据,则通过堆区分配存储堆空间的地址。

更新存在容器(ExistentialContainer)数据结构:

struct OSProtocolBox {=
    var valueBuffer1: UnsafeRawPointer
    var valueBuffer2: UnsafeRawPointer
    var valueBuffer3: UnsafeRawPointer
    var metadata: UnsafeRawPointer
    var witnessTable: UnsafeMutablePointer<TargetWitnessTable>
}

写时复制

image.png

image.png

可以看到,在给level复制的时候,对应堆区的对象发生了改变。这里是系统为了降低堆区内存消耗所做的优化。当valueBuffer不能存储所有属性的时候,会把结构体在堆区开辟空间存储为引用类型,但在变量发生改变的时候,会去判断应用计数是否大于1,如果大于1,将在堆区重新开辟空间来存储改变后的结构体。