Swift协议

188 阅读11分钟

1、协议的基本语法

1.1、协议的定义

  • 协议可以用来定义 方法属性下标的声明,协议可以被 枚举结构体 遵守(多个协议之间用逗号隔开)
    • class 本质上定义了一个对象是什么 
    • protocol 本质上定义了一个对象有哪些行为

1.2、声明协议

  • 使用关键字protocol进行声明

    protocol MyProtocol{}
    
  • 协议中定义方法时不能有默认参数值,且默认情况下,协议中定义的内容必须全部都实现。如果我们不想强制让遵循协议的类型实现,可以使用 optional 作为前缀放在协议的定义,并且 protocoloptional 前要加上 @objc

    @objc protocol personProtocol {
        @objc optional func addAge(by: Int)
    }
    
  • 可以添加属性和方法

    • 必须指定属性是get 还是 get set(只读/可读写)的,且必须是变量用var ,添加类型属性最好用static关键字
    protocol MyProtocol {
        var age: Int{ get set }
        var name: String{ get }
        var description : String{ get }
        func run()
    }
    

1.3、协议中的异变方法

  • 协议中的异变方法,表示 该方法可以改变遵守该协议的类型的实例以及该实例的所有属性(用于枚举和结构体);在为 实现该方法的时候不需要写 mutating 关键字
    protocol MyProtocol {
        mutating func run()
    }
    
    class Person: MyProtocol {
        func run() {
            print("Person run left road")
        }
    }
    
    struct Dog: MyProtocol {
        mutating func run() {
            print("Dog run right road")
        }
    }
    

1.4、协议定义初始化方法

  • 协议中也可以定义初始化方法,当类遵守协议实现初始化器时,必须使用required关键字表示构造函数必须实现,或者用final修饰类 image.png

1.5、协议的优点

  • 通过一个 协议 来描述多个类的共同行为,并通过 extension 的方式来对我们的类进行扩展,既降低了对原有代码的侵入,又对可复用的函数进行抽取
    protocol MyProtocol {
        var height: Int{ get set }
        var name: String{ get }
        var description : String { get }
        mutating func countNowHeight()
    }
    
    struct LZPerson {
        var IncreaseHeight : Int
    }
    
    extension LZPerson : MyProtocol {
        var height: Int {
            get {
                return 175
            }
            set {}
        }
    
        var name: String {
            get {
                return "LZ"
            }
        }
    
        mutating func countNowHeight() {
            print(description)
        }
    
        var description : String { get { return "person: height=\(height+IncreaseHeight),name=\(name)" } }
    }
    
    var p = LZPerson(IncreaseHeight: 10)
    p.countNowHeight()
    
    //打印结果
    person: height=185,name=LZ
    

1.6、协议组合

  • 可以包含1个 类型(最多1个)
    //用 typealias 给协议组合取别名
    
    typealias RealPerson = Person & JumpProtocol & RunProtocol
    // 接收同时遵守 Livable、Runnable 协议、并且是 Person 或者其子类的实例
    func fn(obj: RealPerson) {}
    

1.7、CaseIterable 和 CustomStringConvertible

  • 枚举 遵守 CaseIterable 协议,可以实现遍历枚举值

    enum Season: CaseIterable {
        case spring, summer, autumn, winter
    }
    
    let seasons = Season.allCases
    print(seasons.count) // 4
    
    for season in seasons {
        print(season)
    } // spring summer autumn winter
    
  • 遵守 CustomStringConvertibleCustomDebugStringConvertible 协议,都可以自定义实例的打印字符串

    class Person: CustomStringConvertible, CustomDebugStringConvertible {
        var age = 0
        var description: String{ "person_\(age)" }
        var debugDescription: String{ "debug_person_\(age)" }
    }
    
    var person = Person()
    print(person)       // person_0
    debugPrint(person)  // debug_person_0
    
    • print 调用的是 CustomStringConvertible 协议的 description
    • debugPrint 调用的是 CustomDebugStringConvertible 协议的 debugDescription

1.8、类专用协议

  • 在协议后面写上 :AnyObject:class 都代表只有 能遵守这个协议
    protocol MyProtocol1: AnyObject {}
    
    protocol MyProtocol2: class {}
    

2、witness_table (PWT:Protocol Witness Table)

  • 我们知道,类的方法的调度是通过虚函数表VTable)查找到对应的函数进行调用的,而结构体的方法是通过函数的地址直接进行调用的;那么协议中声明的方法呢?如果类或者结构体遵守这个协议,然后实现协议方法,它是如何去查找函数的地址进行调用的呢?

2.1、SIL分析

  • 首先准备一段代码,并转译成SIL文件
    protocol MyProtocol{
        func increment(by: Int)
    }
    
    class Person: MyProtocol{
        func increment(by: Int) {
            _ = by + 1
        }
    }
    let p = Person()
    p.increment(by: 1)
    
    image.png
    • 我们看到 increment(by: Int) 的类型在 sil 中是 class_method 类型的,在 SIL参考文档 中有介绍,class_method 类型的方法是通过 VTable 查找的

    • 在SIL文件最后,我们看到了 vtable,还看到了一个sil_witness_table image.png

  • 我们对代码稍加改动,只把实例对象p声明为协议的类型,再转译成SIL文件看一下
    let p : MyProtocol = Person()
    
    image.png
    • 此时,函数的类型从 class_method,变成了 witness_method 类型的;官网的解释大概意思是:去遵循协议的类型中去找该方法的实现,会使用witness_method方式调度,如果协议使用@objc修饰,会变成 objc 的调用方式
    • 搜索 witness_table 中存储的increment方法名称,得知最终去查找遵守它的类中的 VTable 进行方法的调度
      sil_witness_table hidden Person: MyProtocol module main {
        method #MyProtocol.increment: <Self where Self : MyProtocol> (Self) -> (Int) -> () : @$s4main6PersonCAA10MyProtocolA2aDP9increment2byySi_tFTW	// protocol witness for MyProtocol.increment(by:) in conformance Person
      }
      
      image.png
总结
  • 如果实例对象的静态类型是确定的类型,那么这个协议方法通过 vtable 进行调度。
  • 如果实例对象的静态类型是协议类型,那么这个协议方法通过 witness_table 中对应的协议方法,然后通过协议方法去查找遵守协议的类的 VTable 进行调度。

2.2、在协议的 extention 提供协议方法的默认实现

  • 我们对上边的代码补充一段对MyProtocol协议的 extension,再看一下SIL文件,发现并没有什么变化
    protocol MyProtocol{
        func increment(by: Int)
    }
    
    extension MyProtocol {
        func increment(by: Int){
            print("func_Incrementable")
        }
    }
    
    class Person: MyProtocol{
        func increment(by: Int) {
            _ = by + 1
        }
    }
    let p = Person()
    p.increment(by: 1)
    
    但当我们将MyProtocol中的increment方法注释掉再转译的话,我们可以发现,在 witness_table 中,方法就消失了,所以无法通过 witness_table调用 image.png
总结
  • sil_witness_table 有没有方法取决于在 协议中有没有声明协议方法
    • 若sil_witness_table 中有方法:那么是否通过 witness_method 去调用取决于当前实例的静态类型是 确定类型 还是 协议类型
      let p = Person()               //确定类型
      let p : MyProtocol = Person()  //协议类型
      
      • 是协议类型:通过 witness_method 进行方法的调度
      • 是确定类型:正常调度
    • 若sil_witness_table 中没有方法:那么遵守这份协议的类型 VTable 派发 还是 静态派发(直接函数地址调用)按正常走
  • 总的来说,当协议中有声明并且实例类型为协议类型时,sil_witness_table 中有方法并且通过 witness_method 调用,但也无非就是多了一层函数调用,最终都是走 vtable

2.3、sil_witness_table 在继承关系的情况

  • witness_table 和遵守协议的类直接是一一对应的,一个类遵守一个协议就会有一个 witness_table,但是该类的子类和父类是共用一份 sil_witness_table 的

3、协议底层结构

  • 上面分析了协议方法是如何调度的 ,以及 witness_table和类型之间的关系,那么 witness_table到底如何存储的呢?先来看一下协议类型的大小

    protocol Shape {
        var area: Double { get }
    }
    
    class Circle: Shape {
        var radius: Double
    
        init(_ radius: Double) {
            self.radius = radius
        }
    
        var area: Double {
            get {
                return radius * radius * 3.14
            }
        }
    }
    
    print("Circle size: \(MemoryLayout<Circle>.size)") // Circle size: 8
    print("Shape size: \(MemoryLayout<Shape>.size)")   // Shape size: 40
    

    我们通过 MemoryLayout 获取类型的 Size 的时候,发现 协议类型类类型 的 size 不一致,类类型的 size 等于 8 这是正常的,因为类的内存在堆空间,这个 8 仅仅只是一个指针类型的大小,要想拿到类真正的大小得通过 class_getInstanceSize 函数;这个协议类型的 size 等于 40 又是怎么回事呢?我们接下来在测试一段代码

    let c1: Circle = Circle(10)
    let c2: Shape = Circle(20)
    
    print("c1 size: \(MemoryLayout.size(ofValue: c1))") // c1 size: 8
    print("c2 size: \(MemoryLayout.size(ofValue: c2))") // c2 size: 40
    

    我们发现,同样是 Circle 的实例,但是当实例指定为 协议类型 的时候,这个实例的 size 就变成了 40。这个时候,代表着 c1 和 c2 的内存结构不一致了。

    • c1布局 image.png

    • c2协议类型布局 image.png

      位置内容
      第0个8字节实例对象地址
      第1个8字节未知
      第2个8字节未知
      第3个8字节实例对象的metadata
      第4个8字节witnesstable
    • 大致分析出 协议类型 的结构如下:

      struct ProtoclInstaceStruct {
          var heapObject: UnsafeRawPointer
          var unkown1: UnsafeRawPointer
          var unkown2: UnsafeRawPointer
          var metadata: UnsafeRawPointer
          var witness_table: UnsafeRawPointer
      }
      
    • 40字节原因:因为编译器在编译过程中,因为将实例规定成了 协议类型,所以并不能得到实例的动态类型,因此在分配内存空间时,编译器为了避免麻烦,用空间换取时间,使用了Existential Container(存在容器)这个中间层结构统一管理

3.1、协议结构

  • 通过一系列复杂的探究(源码、IR代码等方式)这里不做赘述,大致得到witness_table 的内存结构:
    struct ProtoclInstaceStruct {     //Existential Container,最主要的结构
        var heapObj: UnsafeRawPointer
        var unkown1: UnsafeRawPointer
        var unkown2: UnsafeRawPointer //此3项都是valueBuffer
        
        var metadata: UnsafeRawPointer
        var witness_table: UnsafeMutablePointer<TargetWitnessTable>
    }
    
    struct TargetWitnessTable {
        var  protocol_conformance_descriptor: UnsafeMutablePointer<TargetProtocolConformanceDescriptor>
        var  protocol_witness: UnsafeRawPointer
    }
    
    struct TargetProtocolConformanceDescriptor {
        var `Protocol`: 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 RelativeOffset: Int32
    
        mutating func getmeasureRelativeOffset() -> UnsafeMutablePointer<Pointee>{
            let offset = self.RelativeOffset
    
            return withUnsafePointer(to: &self) { p in
                return UnsafeMutablePointer(mutating: UnsafeRawPointer(p).advanced(by: numericCast(offset)).assumingMemoryBound(to: Pointee.self))
            }
        }
    }
    
  • 验证代码
    var c2: Shape = Circle(20)
    
    withUnsafePointer(to: &c2) { c2_ptr in
        c2_ptr.withMemoryRebound(to: ProtoclInstaceStruct.self, capacity: 1) { pis_ptr in
            print(pis_ptr.pointee)
    
            let protocolDesPtr = pis_ptr.pointee.witness_table.pointee.protocol_conformance_descriptor.pointee.Protocol.getmeasureRelativeOffset()
            print("协议名称:\(String(cString: protocolDesPtr.pointee.Name.getmeasureRelativeOffset()))")
            print("协议方法的数量:\(protocolDesPtr.pointee.NumRequirements)")
            print("witnessMethod:\(pis_ptr.pointee.witness_table.pointee.protocol_witness)")
        }
    }
    
    //打印结果:
    ProtoclInstaceStruct(heapObj: 0x000000010732a1c0, unkown1: 0x0000000000000000, unkown2: 0x0000000000000000, metadata: 0x00000001000081f0, witness_table: 0x0000000100004088)
    协议名称:Shape
    协议方法的数量:1
    witnessMethod:0x00000001000021d0
    

3.2、符号还原方法

  • 在终端使用 nm -p <可执行文件> | grep <内存地址> 打印出这个方法的符号信息
  • 接着用 xcrun swift-demangle <符号信息> 还原这个符号信息

总结

这个协议见证表(witness_table)的本质其实就是 TargetWitnessTable。第一个元素存储的是一个 descriptor,记录协议的一些描述信息,例如名称和方法的个数等。那么从第二个元素的指针开始存储的就是函数的指针

注意!ProtoclInstaceStruct 中的 witness_table 变量是一个连续的内存空间,所以这个 witness_table 变量存放的可能是很多个协议的见证表。

存放多个协议见证表的因素取决于变量的静态类型,如果这个变量的类型是协议组合类型,那么 witness_table 存放的就是协议组合中所有协议的见证表,如果这个变量的类型是指定单独的某个协议,那么 witness_table 存放的只有这个协议的见证表。

协议原理探究

  • 每个遵守了协议的类,都会有自己的PWT,遵守的协议越多,PWT中存储的函数地址就越多 
  • PWT的本质是一个指针数组,第一个元素存储TargetProtocolConformanceDescriptor,其后面存储的是函数地址 
  • PWT的数量与协议数量一致 
  • Existential Container 是编译器生成的一种特殊的数据类型,用于管理遵守了相同协议的协议类型,因为这些类型的内存大小不一致,所以通过当前的 Existential Container 统一管理 
    • 对于小容量的数据,直接存储在 Value Buffer 
    • 对于大容量的数据,通过堆区分配,存储堆空间的地址

Existential Container

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

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

  • 示例查看什么意思

    struct Circle: Shape {
        var radius = 10
        var width: Int
        var height: Int
    
        init(_ radius: Int) {
            self.radius = radius
            self.width = radius * 2
            self.height = radius * 3
        }
    
        var area: Double {
            get {
                return Double(radius * radius) * 3.14
            }
        }
    }
    
    var c2: Shape = Circle(10)
    print("end")
    

    image.png

    • 从图中看出,协议类型的成实例 c2 结构中,前 24 个字节分别存储着 Circle 的 radius、width 和 height属性值,后续为 metadatawitness_table,此时是24字节能容纳下3个属性的情况,那么如果再增加一个属性呢?
    struct Circle: Shape {
        var radius: Int
        var width: Int
        var height: Int
        var long: Int
    
        init(_ radius: Int) {
            self.radius = radius
            self.width = radius * 2
            self.height = radius * 3
            self.long = radius * 4
        }
    
        var area: Double {
            get {
                return Double(radius * radius) * 3.14
            }
        }
    }
    

    image.png

    • 看到了第一个valueBuffer不再直接存储属性值,而是改成了存储堆空间地址,并将4个属性值存到了堆空间去

写时复制

  • 在swift中,像Array、Dictionary、Set等集合类型都是通过 写时复制(copy-on-write)技术实现的
  • 为了降低堆区内存消耗所做的优化。当valueBuffer不能存储所有属性的时候,会把结构体在堆区开辟空间存储为引用类型,但在变量发生改变的时候,会去判断应用计数是否大于1,如果大于1,将在堆区重新开辟空间来存储改变后的结构体。
protocol Shape {
    var radius: Int { get set }
}

struct Circle: Shape {
    var radius: Int
    var width: Int
    var height: Int
    var height1: Int

    init(_ radius: Int) {
        self.radius = radius
        self.width = radius * 2
        self.height = radius * 3
        self.height1 = radius * 4
    }
    var area: Double {
        get {
            return Double(radius * radius) * 3.14
        }
    }
}
var c1: Shape = Circle(10)
var c2 = c1
print("修改数据")

c2.radius = 12
print("修改完毕")

image.png

expr -f float -- <地址值> :还原地址中的float值