一、协议的基本语法

1. 协议的界说

协议能够用来界说办法、特点、下标的声明,协议能够被枚举、结构体、类恪守(多个协议之间用逗号隔开)。

// 协议界说办法、特点、下标
protocol Drawable {
    func draw()
    var x: Int { get set }
    var y: Int { get }
    subscript(index: Int) -> Int { get}
}
protocol Protocol1 {}
protocol Protocol2 {}
protocol Protocol3 {}
// 恪守协议
class Person: Protocol1, Protocol2, Protocol3 {}

协议中界说办法时不能有默许参数值,且默许状况下,协议中界说的内容有必要全部都完成。假如咱们不想强制让遵循协议的类型完成,能够使用 optional 作为前缀放在协议的界说,并且 protocol 和 optional 前要加上 @objc。

@objc protocol Incrementable {
    @objc optional func increment(by: Int)
}

2. 协议中的特点

  • 协议中界说特点时有必要用var关键字。

  • 完成协议时的特点权限要不小于协议中界说的特点权限。协议界说 get、set,用 var 存储特点或 get、set 计算特点去完成,协议界说 get,用任何特点都能够完成。

例如:

protocol Drawable {
    func draw()
    var x: Int { get set }
    var y: Int { get }
    subscript(index: Int) -> Int { get}
}
class Person: Drawable {
    func draw() {
        print("Person draw")
    }
    var x: Int = 0
    var y: Int = 0
    subscript(index: Int) -> Int {
        set{}
        get{ index }
    }
}

3. 协议中的 static、class、mutating 和 init

  • static 为了确保通用,协议中有必要用 static 界说类型办法、类型特点、类型下标。
protocol Drawable {
    static func draw()
}
class Person: Drawable {
    // class func draw
    static func draw() {
        print("Person draw")
    }
}
Person.draw()   // Person draw
  • mutating 只要将协议中的实例办法标记为 mutating,才答应结构体、枚举的具体完成修正本身内存。类在完成办法时不用加 mutating,枚举、结构体才需求加 mutating。
protocol Drawable {
    mutating func draw()
}
class Person: Drawable {
    func draw() {
        print("Person draw")
    }
}
struct Point: Drawable {
    mutating func draw() {
        print("Point draw")
    }
}
  • init 协议中还能够界说初始化器 init,非 final 类完成时有必要加上 required。
final class Size: Drawable {
    init(x: Int, y: Int) { }
}
class Point: Drawable {
    required init(x: Int, y: Int) { }
}

假如从协议完成的初始化器,刚好是重写了父类的指定初始化器,那么这个初始化有必要一起加required、override。

protocol Livable {
    init(name: String)
}
class Person {
    init(name: String) {}
}
class Student: Person, Livable {
    required override init(name: String) {
        super.init(name: name)
    }
}

协议中界说的 init?、init!,能够用 init、init?、init! 去完成,协议中界说的 init,能够用 init、init! 去完成。

protocol Livable {
    init()
    init?(age:Int)
    init!(no:Int)
}
class Person: Livable {
    required init() {}
    //    required init!() {}
    required init?(age: Int) {}
    //    required init(age: Int) {}
    //    required init!(age: Int) {}
    required init!(no: Int) {}
    //    required init(no: Int) {}
    //    required init?(no: Int) {}
}

4. 协议中的承继和组合

一个协议能够承继其他协议。例如:

protocol Runnable {
    func run()
}
protocol Livable: Runnable {
    func breath()
}
class Person: Livable {
    func breath() {}
    func run() {}
}

协议组合,能够包括1个类类型(最多1个)。咱们来看下面的比如:

// 接纳 Person 或许其子类的实例
func fn0(obj: Person) {}
// 接纳恪守 Livable 协议的实例
func fn1(obj: Livable) {}
// 接纳一起恪守 Livable、Runnable 协议的实例
func fn2(obj: Livable & Runnable) {}
// 接纳一起恪守 Livable、Runnable 协议、并且是 Person 或许其子类的实例
func fn3(obj: Person & Livable & Runnable) {}

fn3 中,参数 obj 协议组合的声明太长了,咱们能够用 typealias 给协议组合取别号,例如:

typealias RealPerson = Person & Livable & Runnable
// 接纳一起恪守 Livable、Runnable 协议、并且是 Person 或许其子类的实例
func fn3(obj: RealPerson) {}

5. 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
  • 恪守 CustomStringConvertible、CustomDebugStringConvertible 协议,都能够自界说实例的打印字符串。
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

iOS-Swift 独孤九剑:八、协议的本质

  • print 调用的是 CustomStringConvertible 协议的 description。

  • debugPrint、po 调用的是 CustomDebugStringConvertible 协议的 debugDescription。

6. 类专用协议

在协议后边写上 :AnyObject 代表只要类能恪守这个协议,在协议后边写上 :class 也代表只要类能恪守这个协议。

protocol MyProtocol: AnyObject {}
protocol MyProtocol: class {}

二、witness_table

witness_table 翻译过来叫做见证表,它是用来干什么,咱们接下来对它进行一个开端的认识。

在《办法》这篇文章中咱们知道,类的办法的调度是经过虚函数表(VTable)查找到对应的函数进行调用的,而结构体的办法直接便是拿到函数的地址进行调用。那么协议中声明的办法呢,假如类或许结构体恪守这个协议,然后完成协议办法,它是怎么去查找函数的地址进行调用的呢。

1. witness_table 的引入

咱们先声明一份协议 Born,里边有一个 born(:) 办法。类 – Person 恪守 Born 并完成 born(:) 办法,代码如下:

protocol Born {
    func born(_ nickname: String)
}
class Person: Born {
    var nickname: String?
    func born(_ nickname: String) {
        self.nickname = nickname
    }
}
let p = Person()
p.born("Coder_张三")

接下来咱们把当时的 main.swift 文件编译成 main.sil 文件,经过 sil 代码来观察是否有 VTable。编译完成后找到 main 函数,查看 born(:) 办法的调用,如图:

iOS-Swift 独孤九剑:八、协议的本质

留意看,born(:) 的类型在 sil 中是 class_method 类型的,在 SIL参阅文档 中有介绍,class_method 类型的办法是经过 VTable 查找的,如图:

iOS-Swift 独孤九剑:八、协议的本质

接下来咱们看到 main.sil 文件最底部的代码,如图:

iOS-Swift 独孤九剑:八、协议的本质

能够看到,born(:) 确实是存储在 VTable 当中了,可是下面的 witness_table 是用来干啥的,并且里边也有一个 born(:),这是个啥。接下来我干一件事,我把变量 p 声明为 Born 协议,代码如下:

let p: Born = Person()

接下来重新将 main.swift 文件编译成 main.sil 文件,然后直接看 main 函数,如图:

iOS-Swift 独孤九剑:八、协议的本质

此刻,咱们发现函数的类型变了,变成了 witness_method 类型的,咱们来看 SIL参阅文档 中是怎么介绍 witness_method 的:

iOS-Swift 独孤九剑:八、协议的本质

翻译如下:

查找受该协议束缚的泛型类型变量的协议办法的完成。结果将在原始协议的 Self 原型上是通用的,并具有 witness_method 调用约好。假如引证的协议是 @objc 协议,则结果类型具有 objc 调用约好。

啥意思呢,咱们大局查找 @protocol witness for main.Born.born(Swift.String) -> (),找到它的完成,如图:

iOS-Swift 独孤九剑:八、协议的本质

留意看,它最终仍是会去查找恪守它的类中的 VTable 进行办法的调度。咱们两次的测验唯一的区别在于是否指定变量的类型为 Born 的协议类型,也能够理解为这个调用的办法和我这个变量指定的静态类型有关。

总结如下:

  • 假如实例对象的静态类型便是确定的类型,那么这个协议办法经过 VTalbel 进行调度。

  • 假如实例对象的静态类型是协议类型,那么这个协议办法经过 witness_table 中对应的协议办法,然后经过协议办法去查找恪守协议的类的 VTable 进行调度。

2. 结构体的 witness_table

知道类的 witness_table 调度状况了之后,咱们来看一下结构体的 witness_table,仍是老办法,经过 sil 代码剖析,代码如下:

protocol Born {
    func born(_ nickname: String)
}
struct Person: Born {
    var nickname: String?
    func born(_ nickname: String) {
        self.nickname = nickname
    }
}
let p: Born = Person()
p.born("Coder_张三")

接下来重新将 main.swift 文件编译成 main.sil 文件,然后直接看 main 函数,如图:

iOS-Swift 独孤九剑:八、协议的本质

咱们再来看一下汇编代码,如图:

iOS-Swift 独孤九剑:八、协议的本质

能够看到,结构体调用协议办法的办法直接便是函数地址调用。当我指定这个变量的类型为 Born 协议的时分,sil main 函数的完成如下:

iOS-Swift 独孤九剑:八、协议的本质

留意看,这个时分它的类型变成了 witness_method ,咱们再来看这个办法对应的 witness_method 的完成,如图:

iOS-Swift 独孤九剑:八、协议的本质

能够看到,它最终仍是找到了结构体 born(:) 办法的地址直接进行调用。那这个便是结构体 witness_method 的调用状况。

3. 在协议的 extention 供给协议办法的默许完成

假如对一个协议进行一个 extension,并且完成协议的办法。一起,恪守这个协议的类也完成这个协议办法。那么,经过这个类调用协议办法的时分,调用的是类中完成的协议办法。

代码如下:

protocol Born {
    func born(_ nickname: String)
}
extension Born {
    func born(_ nickname: String) {
        print("Born born(:)")
    }
}
class Person: Born {
    func born(_ nickname: String) {
        print("Person born(:)")
    }
}
let p = Person()
p.born("Coder_张三")  // Person born(:)

假如在协议中没有声明这个协议办法,可是在协议的 extension 完成了,恪守这个协议的类也完成了这个办法。那么,经过这个类调用这个协议办法的时分,调用的仍是类中完成的办法,可是假如指定了这个变量的类型是协议类型,调用的便是协议的 extension 中完成的办法。

代码如下:

protocol Born {}
extension Born {
    func born(_ nickname: String) {
        print("Born born(:)")
    }
}
class Person: Born {
    func born(_ nickname: String) {
        print("Person born(:)")
    }
}
let p: Born = Person()
p.born("Coder_张三")  // Born born(:)

那其实关于第一种状况来讲,这个协议办法的调用流程是和第 1 点中验证的流程结果是相同的,想验证的靓仔能够自己编译成 sil 代码去验证比照。咱们接下来主要看第二种状况,咱们直接看 main 函数和 sil_witness_table,如图:

iOS-Swift 独孤九剑:八、协议的本质

能够看到,针关于第二种状况,它直接便是拿到 extension 中的函数地址进行调用,并且 sil_witness_table 中没有任何办法。

需求留意的是,这个时分咱们指定了 p 变量的类型为协议类型,但其实就算指定变量 p 的类型为 Person,sil_witness_table 中仍是没有任何办法,这个感兴趣的靓仔能够去尝试,这儿就不一一贴图了,比较费事。

那这儿咱们来做一个总结:

  • 首先 sil_witness_table 有没有办法取决于在协议中有没有声明协议办法。

  • 假如 sil_witness_table 中没有办法,那么恪守这份协议的类型该 VTable 调度就 VTable 调度,该直接函数地址调用就直接函数地址调用。

  • 假如 sil_witness_table 中有办法,那么是否经过 witness_method 去调用取决于当时实例的静态类型是否是协议类型。假如不是,该怎么调度就怎么调度。假如是,那么就经过 witness_method 进行办法的调度。

总的来说当 sil_witness_table 中有办法并且经过 witness_method 调用的时分,无非便是多了一层函数调用。

4. sil_witness_table 在承继关系的状况

  • 当一份协议被多个类恪守的时分,那么在各自类中都会有一个 sil_witness_table。

  • 当一个类恪守多份协议的时分,那么在这个类中,都有一个每份协议对应的 sil_witness_table,也便是会有很多个 sil_witness_table,这个取决于协议的数量。

  • 假如一个类恪守了一份协议,这个类必定会有一个 sil_witness_table,那么这个类的子类和父类是共用一份 sil_witness_table 的。

以上这三点都是能够经过 sil 的代码进行验证比照的,感兴趣的靓仔能够自己试着验证。这儿就不贴图了,比较费事。

三、witness_table 内存布局和内存结构

1. 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 变量的内存地址咱们应该知道,c1 存储的是它堆空间实例对象的地址,咱们来看一下它的内存布局,如图:

iOS-Swift 独孤九剑:八、协议的本质

这个便是 c1 的内存布局,并且咱们经过 expr -f float -- <内存地址> 表达式打印出了 radius 的值。

咱们接下来看 c2 的内存布局,如图:

iOS-Swift 独孤九剑:八、协议的本质

留意看:

  • 第一个 8 字节的内存存储的依然是堆空间的地址值。

  • 第二个和第三个 8 字节存储的是啥咱们也不知道是什么。

  • 第四个 8 字节存储的是堆空间 metadata 的地址。

  • 最终的 8 字节存储的其实是 witness_table 的地址。

那怎么知道最终的 8 字节存储的便是 witness_table 的地址呢?最终的 8 字节内存地址为 0x0000000100004028,咱们接下来翻开汇编调试,找到 c2 的创立后找到 witness_table 相关的代码,如图:

iOS-Swift 独孤九剑:八、协议的本质

如图所示,所以最终的 8 字节存储的其实是 witness_table 的地址。经过以上的剖析,就能够得出 c2 这个类型变量的大致结构,代码如下:

struct ProtoclInstaceStruct {
    var heapObject: UnsafeRawPointer
    var unkown1: UnsafeRawPointer
    var unkown2: UnsafeRawPointer
    var metadata: UnsafeRawPointer
    var witness_table: UnsafeRawPointer
}

2. witness_table 的内存结构

经过第 1 点咱们现已知道了 witness_table 在内存中存储的位置,那这个 witness_table 的内存结构是怎么样的呢,这个时分就能够经过 IR 代码去进行剖析了。IR 的语法和怎么编译成 IR 代码在《闭包及其实质剖析》和《办法》这两篇文章中有介绍。

接下来咱们就直接将当时的 main.swift 文件编译成 main.ll 文件,代码仍是第一点的代码,只不过为了避免搅扰我把 c1 变量和 print 打印注释了。编译成 main.ll 文件后咱们直接看 main 函数,代码如下:

define i32 @main(i32 %0, i8** %1) #0 {
    entry:
    %2 = bitcast i8** %1 to i8*
    // 获取 Circle 的 metadata
    %3 = call swiftcc %swift.metadata_response @"type metadata accessor for main.Circle"(i64 0) #7
    %4 = extractvalue %swift.metadata_response %3, 0
    // %swift.type = type { i64 }
    // %swift.refcounted = type { %swift.type*, i64 }
    // %T4main6CircleC = type <{ %swift.refcounted, %TSd }>
    // 创立 Circle 的实例,此刻这个实例的结构为:{ %swift.refcounted, %TSd }
    %5 = call swiftcc %T4main6CircleC* @"main.Circle.__allocating_init(Swift.Double) -> main.Circle"(double 2.000000e+01, %swift.type* swiftself %4)
    // %T4main5ShapeP = type { [24 x i8], %swift.type*, i8** },%T4main5ShapeP 实质上是一个结构体
    // 留意看,getelementptr为获取结构体成员,i32 0 结构体的内存地址,拿到这个结构体后将 %4 存储到这个结构体的第二个成员变量上
    // 也便是将 metadata 存储到这个结构体的第二个成员变量上,此刻这个结构体的结构为:{ [24 x i8], metadata, i8** }
    store %swift.type* %4, %swift.type** getelementptr inbounds (%T4main5ShapeP, %T4main5ShapeP* @"main.c2 : main.Shape", i32 0, i32 1), align 8
    // 这一行在获取 witness table,然后将 witness table 存储到 %T4main5ShapeP 这个结构体的第三个成员变量上(由于取的是 i32 2)
    // 此刻 %T4main5ShapeP 的结构为:{ [24 x i8], metadata, witness_table }
    store i8** getelementptr inbounds ([2 x i8*], [2 x i8*]* @"protocol witness table for main.Circle : main.Shape in main", i32 0, i32 0), i8*** getelementptr inbounds (%T4main5ShapeP, %T4main5ShapeP* @"main.c2 : main.Shape", i32 0, i32 2), align 8
    // [24 x i8] 是 24 个 Int8 数组,内存中等价 [3 x i64] 数组,等价于 %T4main5ShapeP = type { [3 x i64], %swift.type*, i8** }
    // 这儿是将 %T4main5ShapeP 这个结构体强制转换成 %T4main6CircleC,此刻的结构为:{ [3 x i64], metadata, witness_table }
    // 然后把 %5 寄存到 %T4main5ShapeP 的第一个元素。所以最终的结构为:{ [%T4main6CircleC*, i64, i64], metadata, witness_table },
    store %T4main6CircleC* %5, %T4main6CircleC** bitcast (%T4main5ShapeP* @"main.c2 : main.Shape" to %T4main6CircleC**), align 8
    ret i32 0
}

经过这一段代码的解读也从而验证了第 1 点推断出来的 c2 的内存结构。接下来咱们还需求知道 witness_table 的内存结构,在 IR 中 witness_table 的结构如下:

iOS-Swift 独孤九剑:八、协议的本质

能够看到,这个 witness_table 的结构中有两个成员,那么根据这个信息,复原出来的 witness_table 的结构如下:

struct TargetWitnessTable{
    var  protocol_conformance_descriptor: UnsafeRawPointer
    var  protocol_witness: UnsafeRawPointer
}

那么此刻,ProtoclInstaceStruct 的结构就变成如下代码:

struct ProtoclInstaceStruct {
    var heapObj: UnsafeRawPointer
    var unkown1: UnsafeRawPointer
    var unkown2: UnsafeRawPointer
    var metadata: UnsafeRawPointer
    var witness_table: UnsafeMutablePointer<TargetWitnessTable>
}

3. 源码剖析 witness_table 的内存结构

接下来咱们经过源码来剖析 witness_table 的内存结构,咱们大局查找 TargetWitnessTable,在 Metadata.h 文件中找到 TargetWitnessTable,如图:

iOS-Swift 独孤九剑:八、协议的本质

留意看,源码中的注释也清楚的写着这个是一个协议的见证表,并且,此刻咱们知道第 2 点剖析出来的 protocol_conformance_descriptor 是一个 TargetProtocolConformanceDescriptor,找到这个结构的界说,发现它有以下成员,如图:

iOS-Swift 独孤九剑:八、协议的本质

咱们看 Protocol 这个成员变量,它是一个相对类型指针,其类型的结构为 TargetProtocolDescriptor,相对类型指针在《元类型以及 Mirror 源码和 HandyJson 剖析复原枚举、结构体、类的 Metadata》这篇文章中有介绍,并且咱们现已把这个相对类型指针给复原了出来,咱们用的时分直接仿制过来就好了。

现在需求复原 TargetProtocolDescriptor 的结构,TargetProtocolDescriptor 是承继自 TargetContextDescriptor 的,TargetContextDescriptor 咱们应该无比的熟悉了,在上面说到的文章中也有介绍。所以,TargetProtocolDescriptor 必定有 Flags 和 Parent 两个成员变量,咱们再看一下它本身有什么,如图:

iOS-Swift 独孤九剑:八、协议的本质

此刻此刻,TargetProtocolDescriptor 的结构能够复原出来了,代码如下:

struct TargetProtocolDescriptor {
    var Flags: UInt32
    var Parent: TargetRelativeDirectPointer<UnsafeRawPointer>
    var Name: TargetRelativeDirectPointer<CChar>
    var NumRequirementsInSignature: UInt32
    var NumRequirements: UInt32
    var AssociatedTypeNames: TargetRelativeDirectPointer<CChar>
}

TargetProtocolDescriptor 的结构复原出来后,咱们接着也把 TargetProtocolConformanceDescriptor 的结构复原出来,代码如下:

struct TargetProtocolConformanceDescriptor {
    var `Protocol`: TargetRelativeDirectPointer<TargetProtocolDescriptor>
    var TypeRef: UnsafeRawPointer
    var WitnessTablePattern: UnsafeRawPointer
    var Flags: UInt32
}

4. 验证复原出来的 witness_table 的内存结构

经过上面几点呢,咱们把 witness_table 的内存结构复原出来了,复原出来后咱们做一个验证,看看复原的是否正确。

复原出来的完好代码如下:

struct ProtoclInstaceStruct {
    var heapObj: UnsafeRawPointer
    var unkown1: UnsafeRawPointer
    var unkown2: UnsafeRawPointer
    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

咱们在剖析 IR 代码的时分,应该有留意到 TargetWitnessTable 的 protocol_witness,这一个其实存储的便是咱们的 witnessMethod,在上面的 IR 代码中其实现已写的很清楚了,但咱们仍是来验证一下。

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

如图:

iOS-Swift 独孤九剑:八、协议的本质

所以,这个协议见证表(witness_table)的实质其实便是 TargetWitnessTable。第一个元素存储的是一个 descriptor,记载协议的一些描绘信息,例如称号和办法的个数等。那么从第二个元素的指针开端存储的便是函数的指针

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

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

四、Existential Container

咱们在第三大点中研讨的对象一直是协议的见证表(witness_table),那么在这个探索的进程,咱们从前复原出 c2 实例的内存布局,也便是 ProtoclInstaceStruct 这个结构。这个是什么呢,咱们来介绍一个东西 – Existential Container

Existential Container: 它是编译器生成的一种特殊的数据类型,用于办理恪守了相同协议的协议类型,由于这些类型的内存巨细不一致,所以经过当时的 Existential Container 统一办理。

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

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

想说理解的一点便是复原出来的 ProtoclInstaceStruct 其实便是 Existential Container,翻译过来叫做存在容器。这个存在容器最终的两个 8 字节存储的内容是固定的,存储的是这个实例类型的元类型和协议的见证表。

那前面的 24 个字节用来寄存什么:

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

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

所以 ProtoclInstaceStruct 的结构应该是这样的:

struct ExistentialContainer {
    var valueBuffer1: UnsafeRawPointer
    var valueBuffer2: UnsafeRawPointer
    var valueBuffer3: UnsafeRawPointer
    var metadata: UnsafeRawPointer
    var witness_table: UnsafeRawPointer
}

接下来咱们经过一个结构体来验证,为了便利测验,咱们仍是那之前的 Circle 略微改一下,代码如下:

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 * 2
    }
    var area: Double {
        get {
            return Double(radius * radius) * 3.14
        }
    }
}
var c2: Shape = Circle(10)
print("end")

咱们来看一下它的内存布局,如图:

iOS-Swift 独孤九剑:八、协议的本质

此刻存在容器的前 24 个字节分别存储着 Circle 的 radius、width 和 height。接下来我添加一个特点 height1,代码如下:

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 * 2
        self.height1 = radius * 2
    }
    var area: Double {
        get {
            return Double(radius * radius) * 3.14
        }
    }
    var area1: Double {
        get {
            return Double(radius * radius) * 3.14
        }
    }
}

咱们来看一下它的内存布局,如图:

iOS-Swift 独孤九剑:八、协议的本质

如图所示,这就验证了前面临存在容器的概念和含义。