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

语言: CN / TW / HK

highlight: a11y-dark

一、协议的基本语法

1. 协议的定义

协议可以用来定义方法、属性、下标的声明,协议可以被枚举、结构体、类遵守(多个协议之间用逗号隔开)。 ```swift // 协议定义方法、属性、下标 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。 swift @objc protocol Incrementable { @objc optional func increment(by: Int) }

2. 协议中的属性

  • 协议中定义属性时必须用var关键字。

  • 实现协议时的属性权限要不小于协议中定义的属性权限。协议定义 get、set,用 var 存储属性或 get、set 计算属性去实现,协议定义 get,用任何属性都可以实现。

例如: ```swift 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 定义类型方法、类型属性、类型下标。 ```swift 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。 ```swift 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。swift final class Size: Drawable { init(x: Int, y: Int) { } }

class Point: Drawable { required init(x: Int, y: Int) { } } ```

如果从协议实现的初始化器,刚好是重写了父类的指定初始化器,那么这个初始化必须同时加required、override。 ```swift 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! 去实现。 ```swift 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. 协议中的继承和组合

一个协议可以继承其他协议。例如: ```swift protocol Runnable { func run() }

protocol Livable: Runnable { func breath() }

class Person: Livable { func breath() {} func run() {} } ```

协议组合,可以包含1个类类型(最多1个)。我们来看下面的例子: ```swift // 接收 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 给协议组合取别名,例如: swift typealias RealPerson = Person & Livable & Runnable // 接收同时遵守 Livable、Runnable 协议、并且是 Person 或者其子类的实例 func fn3(obj: RealPerson) {}

5. CaseIterable 和 CustomStringConvertible

  • 让枚举遵守 CaseIterable 协议,可以实现遍历枚举值。 ```swift 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 协议,都可以自定义实例的打印字符串。 ```swift 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 ```

debugPrint po 打印.png

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

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

6. 类专用协议

在协议后面写上 :AnyObject 代表只有类能遵守这个协议,在协议后面写上 :class 也代表只有类能遵守这个协议。 swift protocol MyProtocol: AnyObject {} swift protocol MyProtocol: class {}

二、witness_table

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

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

1. witness_table 的引入

我们先声明一份协议 Born,里面有一个 born(:) 方法。类 - Person 遵守 Born 并实现 born(:) 方法,代码如下: ```swift 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(:) 方法的调用,如图:

sil main 函数.png

注意看,born(:) 的类型在 sil 中是 class_method 类型的,在 SIL参考文档 中有介绍,class_method 类型的方法是通过 VTable 查找的,如图:

class_method 的介绍.png

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

sil_vtable 和 sil_witness_table.png

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

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

witness_method 类型.png

此时,我们发现函数的类型变了,变成了 witness_method 类型的,我们来看 SIL参考文档 中是如何介绍 witness_method 的:

witness_method 文档介绍.png

翻译如下:

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

啥意思呢,我们全局搜索 @protocol witness for main.Born.born(Swift.String) -> (),找到它的实现,如图:

witness born 的实现.png

注意看,它最终还是会去查找遵守它的类中的 VTable 进行方法的调度。我们两次的测试唯一的区别在于是否指定变量的类型为 Born 的协议类型,也可以理解为这个调用的方式和我这个变量指定的静态类型有关。

总结如下: - 如果实例对象的静态类型就是确定的类型,那么这个协议方法通过 VTalbel 进行调度。

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

2. 结构体的 witness_table

知道类的 witness_table 调度情况了之后,我们来看一下结构体的 witness_table,还是老办法,通过 sil 代码分析,代码如下: ```swift 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 函数,如图:

结构体协议方法的调用.png

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

结构体协议汇编的调用.png

可以看到,结构体调用协议方法的方式直接就是函数地址调用。当我指定这个变量的类型为 Born 协议的时候,sil main 函数的实现如下:

结构体指定变量为协议类型的实现.png

注意看,这个时候它的类型变成了 witness_method ,我们再来看这个方法对应的 witness_method 的实现,如图:

结构体 witness_method 的实现.png

可以看到,它最终还是找到了结构体 born(:) 方法的地址直接进行调用。那这个就是结构体 witness_method 的调用情况。

3. 在协议的 extention 提供协议方法的默认实现

如果对一个协议进行一个 extension,并且实现协议的方法。同时,遵守这个协议的类也实现这个协议方法。那么,通过这个类调用协议方法的时候,调用的是类中实现的协议方法。

代码如下: ```swift 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 中实现的方法。

代码如下: ```swift 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,如图:

协议的扩展中实现协议方法.png

可以看到,针对于第二种情况,它直接就是拿到 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 在内存中的位置

我们接下来看一段比较有意思的代码,如下: ```swift 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.size)") // Circle size: 8 print("Shape size: (MemoryLayout.size)") // Shape size: 40 ```

我们通过 MemoryLayout 获取类型的 Size 的时候,发现协议类型和类类型的 size 不一致,类类型的 size 等于 8 这是正常的,因为类的内存在堆空间,这个 8 仅仅只是一个指针类型的大小,要想拿到类真正的大小得通过 class_getInstanceSize 函数。

这个协议类型的 size 等于 40 又是怎么回事呢,我们接下来在测试一段代码,如下: ```swift 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 存储的是它堆空间实例对象的地址,我们来看一下它的内存布局,如图:

c1 的内存布局.png

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

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

c2 的内存布局.png

注意看: - 第一个 8 字节的内存存储的依然是堆空间的地址值。

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

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

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

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

汇编验证 witness_table 存储的位置.png

如图所示,所以最后的 8 字节存储的其实是 witness_table 的地址。通过以上的分析,就可以得出 c2 这个类型变量的大致结构,代码如下: swift 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 函数,代码如下: ```swift 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 的结构如下:

witness_table 在 IR 中的结构.png

可以看到,这个 witness_table 的结构中有两个成员,那么根据这个信息,还原出来的 witness_table 的结构如下: swift struct TargetWitnessTable{ var protocol_conformance_descriptor: UnsafeRawPointer var protocol_witness: UnsafeRawPointer }

那么此时,ProtoclInstaceStruct 的结构就变成如下代码: swift 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,如图:

源码的 TargetWitnessTable.png

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

TargetProtocolConformanceDescriptor 的成员.png

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

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

TargetProtocolDescriptor 的结构.png

此时此刻,TargetProtocolDescriptor 的结构可以还原出来了,代码如下: swift 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 的结构还原出来,代码如下: swift struct TargetProtocolConformanceDescriptor { var `Protocol`: TargetRelativeDirectPointer<TargetProtocolDescriptor> var TypeRef: UnsafeRawPointer var WitnessTablePattern: UnsafeRawPointer var Flags: UInt32 }

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

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

还原出来的完整代码如下: ```swift struct ProtoclInstaceStruct { var heapObj: UnsafeRawPointer var unkown1: UnsafeRawPointer var unkown2: UnsafeRawPointer var metadata: UnsafeRawPointer var witness_table: UnsafeMutablePointer }

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

struct TargetProtocolConformanceDescriptor { var Protocol: TargetRelativeDirectPointer var TypeRef: UnsafeRawPointer var WitnessTablePattern: UnsafeRawPointer var Flags: UInt32 }

struct TargetProtocolDescriptor { var Flags: UInt32 var Parent: TargetRelativeDirectPointer var Name: TargetRelativeDirectPointer var NumRequirementsInSignature: UInt32 var NumRequirements: UInt32 var AssociatedTypeNames: TargetRelativeDirectPointer }

struct TargetRelativeDirectPointer { 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))
    }
}

} ```

下面是我的验证代码: ```swift 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)")
}

} swift 打印结果: 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 <符号信息> 还原这个符号信息。

如图: nm -p 还原符号信息.png

所以,这个协议见证表(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 的结构应该是这样的: swift struct ExistentialContainer { var valueBuffer1: UnsafeRawPointer var valueBuffer2: UnsafeRawPointer var valueBuffer3: UnsafeRawPointer var metadata: UnsafeRawPointer var witness_table: UnsafeRawPointer }

接下来我们通过一个结构体来验证,为了方便测试,我们还是那之前的 Circle 稍微改一下,代码如下: ```swift 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") ```

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

值类型的存在容器.png

此时存在容器的前 24 个字节分别存储着 Circle 的 radius、width 和 height。接下来我添加一个属性 height1,代码如下: ```swift 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
    }
}

} ```

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

值类型的存在容器超出.png

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