前言
本篇文章主要讲解Swift中常用的协议Protocol
,主要分析protocol的用法及底层存储结构
。
一、基本用法
先来看看Swift中协议的基本用法(和OC的差别不大)👇
1.1 语法格式
协议的语法格式👇
protocol MyProtocol {
// body
}
-
class、struct、enum
都可以遵守
协议,如果需要遵守多个协议,可以使用逗号分隔
,例如👇
struct LGTeacher: Protocol1, Protocol2 {
// body
}
- 如果class中有
superClass
,一般是放在遵守的协议之前
👇
struct LGTeacher: NSObject, Protocol1, Protocol2 {
// body
}
1.2 协议中的属性
再来看看协议中的属性
,需要注意2点👇
- 协议同时要求一个属性必须
明确
是可读的/可读可写的
- 属性要求定义为
变量类型
,即使用var而不是let
protocol LGTestProtocol {
var age: Int {get set}
}
1.3 协议中的方法
最后看看协议中的方法,和OC一样,只需声明不需实现。例如👇
protocol MyProtocol {
func doSomething()
static func teach()
}
然后类遵循了该协议,必须实现协议中的方法👇
class LGTeacher: MyProtocol{
func doSomething() {
print("LGTeacher doSomething")
}
static func teach() {
print("LGTeacher teach")
}
}
var t = LGTeacher()
t.doSomething()
LGTeacher.teach()
- 协议中也可以
定义初始化
方法,当实现初始化器
时,必须使用required关键字
(OC不需要)👇
protocol MyProtocol {
init(age: Int)
}
class LGTeacher: MyProtocol {
var age: Int
required init(age: Int) {
self.age = age
}
}
- 如果一个协议
只能被类实现
,需要协议继承AnyObject
。如果此时结构体遵守该协议,会报错!👇
二、进阶用法
协议的进阶用法 👉 将协议作为类型
,主要有以下3种情况👇
- 作为
函数、方法或者初始化
程序中的参数类型
或者返回值
- 作为
常量、变量或属性
的类型
- 作为
数组、字典或者其他容器
中元素Item的类型
继承的方式
先看看,下面的代码输出结果是什么?👇
class Shape{
var area: Double{
get{
return 0
}
}
}
class Circle: Shape{
var radius: Double
init(_ radius: Double) {
self.radius = radius
}
override var area: Double{
get{
return radius * radius * 3.14
}
}
}
class Rectangle: Shape{
var width, height: Double
init(_ width: Double, _ height: Double) {
self.width = width
self.height = height
}
override var area: Double{
get{
return width * height
}
}
}
var circle: Shape = Circle.init(10.0)
var rectangle: Shape = Rectangle.init(10.0, 20.0)
var shapes: [Shape] = [circle, rectangle]
for shape in shapes{
print(shape.area)
}
上面的代码是基于继承的方式
来实现的,基类
中的area
必须有一个默认实现
。当然,这种情况也可以采用协议的方式
来实现👇
协议的方式
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
}
}
}
class Rectangle: Shape{
var width, height: Double
init(_ width: Double, _ height: Double) {
self.width = width
self.height = height
}
var area: Double{
get{
return width * height
}
}
}
var circle: Shape = Circle.init(10.0)
var rectangle: Shape = Rectangle.init(10.0, 20.0)
var shapes: [Shape] = [circle, rectangle]
for shape in shapes{
print(shape.area)
}
shape变成了协议
,提供了一个只读属性area
,遵循该协议的类都要实现
age的get方法
。接着我们再来看var shapes
👉 里面的元素存在2种情况👇
- 元素指定的
Shape是类
时,数组中存储的都是引用类型
的地址
(这一点很好理解,没问题) - 元素指定的
Shape是协议
时,数组中存储的是什么?
那如何让数组shapes里的元素是协议?👉 让协议默认实现area的get方法👇
protocol Shape {
}
extension Shape{
var area: Double {
get{return 0}
}
}
然后,我们这么调用,看看输出什么?👇
var circle: Shape = Circle.init(10.0)
print(circle.area)
输出是0.0
,为什么不是10*10*3.14
?因为在协议Shape的extension中,声明的方法是静态调用
,那么在编译期间
代码的地址就定下来了
,是无法改变
的,这点我们可以用SIL代码来验证👇
- 首先看看
main函数
- 再看看协议
shape协议extension中
实现的area的get方法
👇
上图SIL代码中可以看出,Circle.init(10.0)
初始化里虽然传递的是10.0
,但是SIL代码中初始化确使用的是$Builtin.FPIEEE64
,而$Builtin.FPIEEE64
恰巧是shape协议extension中
实现的area的get方法
的返回值(即是0
),最后我们再练看看circle.area方法源码👇
调用的也是$Builtin.FPIEEE64
👉 0.0,所以print(circle.area)
输出当然是0.0
。
三、底层原理
我们先来看看下面的案例输出什么?👇
protocol MyProtocol {
func teach()
}
extension MyProtocol{
func teach(){ print("MyProtocol") }
}
class MyClass: MyProtocol{
func teach(){ print("MyClass") }
}
let object: MyProtocol = MyClass()
object.teach()
let object1: MyClass = MyClass()
object1.teach()
为什么输出的结果一样呢?老规矩,从SIL分析👇
3.1 示例SIL分析
- 部分一 👉
MyProtocol
和MyClass
的定义
- 部分二 👉
main函数
中的调用
从上图中我们知道👇
- 对象object 👉 方法teach的调用是通过
witness_method
调用 - 而对象object1 👉 方法teach的调用是通过
class_method
调用
接着我们在SIL代码中分别搜索#MyProtocol.teach
和#MyClass.teach
👇
发现了两个方法列表:MyClass
的sil_vtable
和sil_witness_table
:
-
sil_vtable
这个我们很熟悉,之前的Swift 值类型 引用类型 & 方法调度文章提过,就是类MyClass的函数列表
-
sil_witness_table
对应的就是Protocol Witness Table(简称PWT)
,里面存储的是方法数组
,里面包含了方法实现的指针地址
,一般我们调用方法时,是通过获取对象的内存地址
和方法的位移offset
去查找的。
而sil_witness_table
里面其实调用的还是MyClass的teach方法👇
这也是为什么object.teach()
输出的是MyClass
的原因。
扩展:去掉Protocol中声明的方法
//如果去掉协议中的声明呢?打印结果是什么
protocol MyProtocol {
}
extension MyProtocol{
func teach(){ print("MyProtocol") }
}
class MyClass: MyProtocol{
func teach(){ print("MyClass") }
}
let object: MyProtocol = MyClass()
object.teach()
let object1: MyClass = MyClass()
object1.teach()
继续SIL分析👇
- MyProtocol没有了teach函数的声明👇
- main函数调用
上图可知👇
- 第一个打印MyProtocol,是因为调用的是协议扩展中的teach方法,这个方法的地址是在编译时期就已经确定的,即通过静态函数地址调度
- 第二个打印MyClass,同上个例子一样,是类的函数表调用
- 方法列表
上图可知,查看SIL中的witness_table,其中已经没有teach方法
,因为👇
- 声明在
Protocol中的方法
,在底层会存储在PWT
,PWT
中的方法也是通过class_method
,去类的V-Table中
找到对应的方法的调度。 - 如果
没有声明
在Protocol中的函数,只是通过Extension
提供了一个默认实现,其函数地址
在编译过程中
就已经确定
了,对于遵守协议的类
来说,这种方法是无法重写
的。
3.2 协议的PWT存储位置
我们在分析函数调度时,已经知道了V-Table
是存储在metadata中
的,而且根据上面的分析,协议中的方法存储在PWT
,那PWT
存储在哪里呢?接下来我们来探究一下。
首先我们来看看下面的示例,输出什么?👇
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
}
}
}
var circle: Shape = Circle(10.0)
print(MemoryLayout.size(ofValue: circle))
print(MemoryLayout.stride(ofValue: circle))
var circle1: Circle = Circle(10.0)
print(MemoryLayout.size(ofValue: circle1))
print(MemoryLayout.stride(ofValue: circle1))
circle的类型是协议Shape
,而circle1的类型是类Circle
,输出结果👇
circle的size和stride
均为40,why?
- 首先lldb看看👇
circle首地址的metadata地址
中,heapObject
里保存了10
这个值。
- 接着看看SIL(
main函数
代码)👇
我们发现,SIL中,系统是通过调用init_existential_addr
读取之前声明的circle变量,而circle1却是👇
circle是通过调用load
指令读取的,那么init_existential_addr
这个指令代表什么意思呢?我们去SIL官网说明文档,查到👇
上图中的existential container
是编译器生成的一种特殊的数据类型
,也用于管理
遵守了相同协议的协议类型
。因为这些数据类型的内存空间尺寸不同
,使用existential container
进行管理可以实现存储一致性
。
所以,系统使用existential container
容器包含了Shape类型
,接着调用existential container
这个类型来初始化circle
变量,相当于对circle包装了一层
。那么,重点就来到了existential container
,接下来我们通过IR代码,看看这个容器中存储的数据格式是什么样的?
- 继续查看IR代码👇
接着看main函数代码👇
也就是最终结构是{ heapObject, metadata, PWT }
,这和之前lldb查看的内存分布一模一样!
仿写
接下来,我们可以尝试仿写IR的main函数这块内存绑定的流程,代码👇
// HeapObject结构体(Swift类的本质)
struct HeapObject {
var type: UnsafeRawPointer
var refCount1: UInt32
var refCount2: UInt32
}
// %T4main5ShapeP = type { [24 x i8], %swift.type*, i8** }
struct protocolData {
//24 * i8 :因为是8字节读取,所以写成3个指针,正好24字节
var value1: UnsafeRawPointer
var value2: UnsafeRawPointer
var value3: UnsafeRawPointer
//type 存放metadata,目的是为了找到Value Witness Table 值目录表
var type: UnsafeRawPointer
// i8* 存放pwt,即协议的方法列表
var pwt: UnsafeRawPointer
}
// 2、定义协议+类
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
}
}
}
//对象类型为协议
var circle: Shape = Circle(10.0)
// 3、将circle强转为protocolData结构体
withUnsafePointer(to: &circle) { ptr in
ptr.withMemoryRebound(to: protocolData.self, capacity: 1) { pointer in
print(pointer.pointee)
}
}
运行👇
至此,我们知道了PWT的存储位置👇
存储在一个
existential container
容器中,该容器的大致结构是{ heapObject, metadata, PWT }
修改一:将class改成 struct
我们再定义一个结构体Rectangle
,也遵循Shape协议
👇
protocol Shape {
var area: Double {get}
}
struct Rectangle: Shape{
var width, height: Double
init(_ width: Double, _ height: Double) {
self.width = width
self.height = height
}
var area: Double{
get{
return width * height
}
}
}
//对象类型为协议
var rectangle: Shape = Rectangle(10.0, 20.0)
struct HeapObject {
var type: UnsafeRawPointer
var refCount1: UInt32
var refCount2: UInt32
}
// %T4main5ShapeP = type { [24 x i8], %swift.type*, i8** }
struct protocolData {
//24 * i8 :因为是8字节读取,所以写成3个指针
var value1: UnsafeRawPointer
var value2: UnsafeRawPointer
var value3: UnsafeRawPointer
//type 存放metadata,目的是为了找到Value Witness Table 值目录表
var type: UnsafeRawPointer
// i8* 存放pwt
var pwt: UnsafeRawPointer
}
//将circle强转为protocolData结构体
withUnsafePointer(to: &rectangle) { ptr in
ptr.withMemoryRebound(to: protocolData.self, capacity: 1) { pointer in
print(pointer.pointee)
}
}
Rectangle
有2个成员width
和height
,所以protocolData
中的value1和value2分别存储着他们的值👇
接下来我们看看IR代码中是怎么处理的👇
上图可知,width
所对应的%4
是从0开始偏移
存储8字节
,那么就是0~7
,而height
对应的5%
是从1开始的
,就是8~15
。(如果Rectangle
是类class
的话,应该都是存储在0~7
,因为存储的是HeapObject
)
修改二:struct中有3个属性
继续修改,再添加一个属性,变成3个属性呢?👇
struct Rectangle: Shape{
var width, height: Double
var width1 = 30.0
init(_ width: Double, _ height: Double) {
self.width = width
self.height = height
}
var area: Double{
get{
return width * height
}
}
}
从结果中可以看出,width1
是存储在value3
。
修改三:struct中有4个属性
继续,4个属性呢?👇
struct Rectangle: Shape{
var width, height: Double
var width1 = 30.0
var height1 = 40.0
init(_ width: Double, _ height: Double) {
self.width = width
self.height = height
}
var area: Double{
get{
return width * height
}
}
}
我们再看看value1的地址👇
小结
所以Protocol协议
在底层的存储结构👇
-
前24个
字节,主要用于存储遵循了协议
的class/struct
的属性值
,如果24字节不够存储
,会在堆区
开辟一个内存空间,然后在24字节中的前8个字节
存储该堆区地址
(超出24字节是直接分配堆区空间
,然后存储值,并不是先存储值
,然后发现不够再分配堆区空间) -
后16个字节
分别用于存储vwt(值目录表)、pwt(协议目录表)
3.3 写时复制(copy on write)
继续修改例子,将Rectangle
改为class
,声明一个数组存储circle 和 rectangle对象
👇
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
}
}
}
class Rectangle: Shape{
var width, height: Double
init(_ width: Double, _ height: Double) {
self.width = width
self.height = height
}
var area: Double{
get{
return width * height
}
}
}
var circle: Shape = Circle.init(10.0)
var rectangle: Shape = Rectangle.init(10.0, 20.0)
var shapes: [Shape] = [circle, rectangle]
for shape in shapes{
print(shape.area)
}
我们知道,protocol中存储了pwt,pwt的内部也是通过class_method
查找,在代码运行过程中,底层通过容器结构体
,将metadata和pwt
关联起来,所以可以根据metadata
找到对应的v-table
,从而完成方法的调用
。所以,上图中输出的314 和 200就说明了 👉 系统是去各自的类中查找属性area的get方法。
再看下面的示例👇(将Rectangle
还原回结构体,然后再声明一个变量rectangle1 = rectangle)
struct Rectangle: Shape{
var width, height: Double
var width1 = 30.0
var height1 = 40.0
init(_ width: Double, _ height: Double) {
self.width = width
self.height = height
}
var area: Double{
get{
return width * height
}
}
}
//对象类型为协议
var rectangle: Shape = Rectangle(10.0, 20.0)
//将其赋值给另一个协议变量
var rectangle1: Shape = rectangle
然后使用withMemoryRebound
绑定值到结构体protocolData
中查看内存👇
// 查看其内存地址
struct HeapObject {
var type: UnsafeRawPointer
var refCount1: UInt32
var refCount2: UInt32
}
// %T4main5ShapeP = type { [24 x i8], %swift.type*, i8** }
struct protocolData {
//24 * i8 :因为是8字节读取,所以写成3个指针
var value1: UnsafeRawPointer
var value2: UnsafeRawPointer
var value3: UnsafeRawPointer
//type 存放metadata,目的是为了找到Value Witness Table 值目录表
var type: UnsafeRawPointer
// i8* 存放pwt
var pwt: UnsafeRawPointer
}
withUnsafePointer(to: &rectangle) { ptr in
ptr.withMemoryRebound(to: protocolData.self, capacity: 1) { pointer in
print(pointer.pointee)
}
}
withUnsafePointer(to: &rectangle1) { ptr in
ptr.withMemoryRebound(to: protocolData.self, capacity: 1) { pointer in
print(pointer.pointee)
}
}
从输出结果来看,两个协议变量rectangle
和rectangle1
内存地址是一模一样的。
如果修改rectangle1
的width
属性的值(需要将width
属性声明到protocol
)👇
protocol Shape {
var width: Double {get set}
var area: Double {get}
}
调用代码👇
withUnsafePointer(to: &rectangle) { ptr in
ptr.withMemoryRebound(to: protocolData.self, capacity: 1) { pointer in
print(pointer.pointee)
}
}
withUnsafePointer(to: &rectangle1) { ptr in
ptr.withMemoryRebound(to: protocolData.self, capacity: 1) { pointer in
print(pointer.pointee)
}
}
rectangle1.width = 50.0
withUnsafePointer(to: &rectangle1) { ptr in
ptr.withMemoryRebound(to: protocolData.self, capacity: 1) { pointer in
print(pointer.pointee)
}
}
修改前
rectangle和rectangle1
的heapObject也就是value1相同0x00000001005421b0
,修改后
rectangle1
的heapobject变成了0x0000000100611720
。这里也就验证了struct值类型
(虽然超过了24字节存储到了堆上)【写时赋值】👇
当复制时,并没有值的修改,所以两个变量指向同一个堆区内存,当第二个变量修改了属性值时,会将原本堆区内存的值拷贝到一个新的堆区内存,并进行值的修改
如果将struct值类型改为class引用类型
,结果会怎样?
class Rectangle: Shape{
var width: Double
var height: Double
var width1 = 30.0
var height1 = 40.0
init(_ width: Double, _ height: Double) {
self.width = width
self.height = height
}
var area: Double{
get{
return width * height
}
}
}
上图可知,修改前后,地址没有发生任何变化
!
Value Buffer
-
struct结构体
中24字节
官方叫法是Value Buffer。 -
Value Buffer
用来存储当前的值,如果超过
存储的最大容量的话会开辟一块堆空间
。 - 针对
值类型
来说在赋值时
会先拷贝
heapobject地址(Copy on write
)。在修改时
会先检测引用计数
,如果引用计数大于1
,此时开辟新的堆空间
把要修改的内容拷贝到新的堆空间(这么做为了提升性能
)。
Value Buffer
在容器existential container
中的位置👇
总结
本篇文章讲解了Swift中有一个重要的概念 👉 协议Protocol
,从基础概念、用法,进阶用法和底层这条主线,详细讲解了值类型struct
与引用类型class
遵循协议时,其PWT
和Value Buffer
的内存地址的分布,希望大家掌握,从容应对面试。