一、协议的基本语法
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
-
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(:) 方法的调用,如图:
注意看,born(:) 的类型在 sil 中是 class_method 类型的,在 SIL参考文档 中有介绍,class_method 类型的方法是通过 VTable 查找的,如图:
接下来我们看到 main.sil 文件最底部的代码,如图:
可以看到,born(:) 确实是存储在 VTable 当中了,但是下面的 witness_table 是用来干啥的,并且里面也有一个 born(:),这是个啥。接下来我干一件事,我把变量 p 声明为 Born 协议,代码如下:
let p: Born = Person()
接下来重新将 main.swift 文件编译成 main.sil 文件,然后直接看 main 函数,如图:
此时,我们发现函数的类型变了,变成了 witness_method 类型的,我们来看 SIL参考文档 中是如何介绍 witness_method 的:
翻译如下:
查找受该协议约束的泛型类型变量的协议方法的实现。结果将在原始协议的 Self 原型上是通用的,并具有 witness_method 调用约定。如果引用的协议是 @objc 协议,则结果类型具有 objc 调用约定。
啥意思呢,我们全局搜索 @protocol witness for main.Born.born(Swift.String) -> (),找到它的实现,如图:
注意看,它最终还是会去查找遵守它的类中的 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 函数,如图:
我们再来看一下汇编代码,如图:
可以看到,结构体调用协议方法的方式直接就是函数地址调用。当我指定这个变量的类型为 Born 协议的时候,sil main 函数的实现如下:
注意看,这个时候它的类型变成了 witness_method ,我们再来看这个方法对应的 witness_method 的实现,如图:
可以看到,它最终还是找到了结构体 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,如图:
可以看到,针对于第二种情况,它直接就是拿到 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 存储的是它堆空间实例对象的地址,我们来看一下它的内存布局,如图:
这个就是 c1 的内存布局,并且我们通过 expr -f float -- <内存地址>
表达式打印出了 radius 的值。
我们接下来看 c2 的内存布局,如图:
注意看:
-
第一个 8 字节的内存存储的依然是堆空间的地址值。
-
第二个和第三个 8 字节存储的是啥我们也不知道是什么。
-
第四个 8 字节存储的是堆空间 metadata 的地址。
-
最后的 8 字节存储的其实是 witness_table 的地址。
那怎么知道最后的 8 字节存储的就是 witness_table 的地址呢?最后的 8 字节内存地址为 0x0000000100004028,我们接下来打开汇编调试,找到 c2 的创建后找到 witness_table 相关的代码,如图:
如图所示,所以最后的 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 的结构如下:
可以看到,这个 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,如图:
注意看,源码中的注释也清楚的写着这个是一个协议的见证表,并且,此时我们知道第 2 点分析出来的 protocol_conformance_descriptor 是一个 TargetProtocolConformanceDescriptor,找到这个结构的定义,发现它有以下成员,如图:
我们看 Protocol 这个成员变量,它是一个相对类型指针,其类型的结构为 TargetProtocolDescriptor,相对类型指针在《元类型以及 Mirror 源码和 HandyJson 分析还原枚举、结构体、类的 Metadata》这篇文章中有介绍,而且我们已经把这个相对类型指针给还原了出来,我们用的时候直接复制过来就好了。
现在需要还原 TargetProtocolDescriptor 的结构,TargetProtocolDescriptor 是继承自 TargetContextDescriptor 的,TargetContextDescriptor 我们应该无比的熟悉了,在上面提到的文章中也有介绍。所以,TargetProtocolDescriptor 必然有 Flags 和 Parent 两个成员变量,我们再看一下它自身有什么,如图:
此时此刻,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 <符号信息>
还原这个符号信息。
如图:
所以,这个协议见证表(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")
我们来看一下它的内存布局,如图:
此时存在容器的前 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
}
}
}
我们来看一下它的内存布局,如图:
如图所示,这就验证了前面对存在容器的概念和意义。