1、协议的基本语法
1.1、协议的定义
- 协议可以用来定义 方法、属性、下标的声明,协议可以被 枚举、结构体、类 遵守(多个协议之间用逗号隔开)
- class 本质上定义了一个对象是什么
- protocol 本质上定义了一个对象有哪些行为
1.2、声明协议
-
使用关键字
protocol进行声明protocol MyProtocol{} -
协议中定义方法时不能有默认参数值,且默认情况下,协议中定义的内容
必须全部都实现。如果我们不想强制让遵循协议的类型实现,可以使用optional作为前缀放在协议的定义,并且 protocol 和 optional 前要加上@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修饰类
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 -
遵守
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 调用的是 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)-
我们看到 increment(by: Int) 的类型在 sil 中是
class_method类型的,在 SIL参考文档 中有介绍,class_method 类型的方法是通过 VTable 查找的 -
在SIL文件最后,我们看到了 vtable,还看到了一个
sil_witness_table
-
- 我们对代码稍加改动,只把实例对象p声明为协议的类型,再转译成SIL文件看一下
let p : MyProtocol = Person()- 此时,函数的类型从 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 }
- 此时,函数的类型从 class_method,变成了
总结
- 如果实例对象的
静态类型是确定的类型,那么这个协议方法通过 vtable 进行调度。 - 如果实例对象的
静态类型是协议类型,那么这个协议方法通过 witness_table 中对应的协议方法,然后通过协议方法去查找遵守协议的类的 VTable 进行调度。
2.2、在协议的 extention 提供协议方法的默认实现
- 我们对上边的代码补充一段对MyProtocol协议的 extension,再看一下SIL文件,发现并没有什么变化
但当我们将MyProtocol中的increment方法注释掉再转译的话,我们可以发现,在 witness_table 中,方法就消失了,所以无法通过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)witness_table调用
总结
- 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 去调用取决于当前实例的静态类型是
- 总的来说,
当协议中有声明并且实例类型为协议类型时,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布局
-
c2协议类型布局
位置 内容 第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")- 从图中看出,协议类型的成实例 c2 结构中,前 24 个字节分别存储着 Circle 的 radius、width 和 height属性值,后续为 metadata 和 witness_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 } } }- 看到了第一个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("修改完毕")
expr -f float -- <地址值> :还原地址中的float值