Swift 协议

前言

本篇文章主要讲解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点👇

  1. 协议同时要求一个属性必须明确可读的/可读可写的
  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种情况👇

  1. 作为函数、方法或者初始化程序中的参数类型或者返回值
  2. 作为常量、变量或属性类型
  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种情况👇

  1. 元素指定的Shape是类时,数组中存储的都是引用类型地址(这一点很好理解,没问题)
  2. 元素指定的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分析

  • 部分一 👉 MyProtocolMyClass的定义
  • 部分二 👉 main函数中的调用

从上图中我们知道👇

  1. 对象object 👉 方法teach的调用是通过witness_method调用
  2. 而对象object1 👉 方法teach的调用是通过class_method调用

接着我们在SIL代码中分别搜索#MyProtocol.teach#MyClass.teach👇

发现了两个方法列表:MyClasssil_vtablesil_witness_table

  1. sil_vtable这个我们很熟悉,之前的Swift 值类型 引用类型 & 方法调度文章提过,就是类MyClass的函数列表
  2. 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函数调用

上图可知👇

  1. 第一个打印MyProtocol,是因为调用的是协议扩展中的teach方法,这个方法的地址是在编译时期就已经确定的,即通过静态函数地址调度
  2. 第二个打印MyClass,同上个例子一样,是类的函数表调用
  • 方法列表

上图可知,查看SIL中的witness_table,其中已经没有teach方法,因为👇

  1. 声明在Protocol中的方法,在底层会存储在PWTPWT中的方法也是通过class_method,去类的V-Table中找到对应的方法的调度。
  2. 如果没有声明在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个成员widthheight,所以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协议在底层的存储结构👇

  1. 前24个字节,主要用于存储遵循了协议class/struct属性值,如果24字节不够存储,会在堆区开辟一个内存空间,然后在24字节中的前8个字节存储该堆区地址(超出24字节是直接分配堆区空间,然后存储值,并不是先存储值,然后发现不够再分配堆区空间)
  2. 后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)
    }
}

从输出结果来看,两个协议变量rectanglerectangle1内存地址是一模一样的。
如果修改rectangle1width属性的值(需要将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遵循协议时,其PWTValue Buffer的内存地址的分布,希望大家掌握,从容应对面试。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,565评论 6 479
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,021评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,003评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,015评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,020评论 5 370
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,856评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,178评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,824评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,264评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,788评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,913评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,535评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,130评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,102评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,334评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,298评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,622评论 2 343

推荐阅读更多精彩内容

  • Protocol:所谓协议,就是一组属性和/或方法的定义,而如果某个具体类型想要遵守一个协议,那它需要实现这个协议...
    帅驼驼阅读 528评论 4 3
  • Swift 进阶之路 文章汇总[//www.greatytc.com/p/5fbedf309237] 本...
    Style_月月阅读 545评论 0 2
  • 协议的基本⽤法 协议的语法格式 我们熟悉的 class , struct , enum 都可以遵循协议,如果要遵守...
    Mjs阅读 517评论 0 0
  • 协议为方法、属性、以及其他特定的任务需求或功能定义蓝图。协议可被类、结构体、或枚举类型采纳以提供所需功能的具体实现...
    HotPotCat阅读 1,485评论 2 5
  • 协议的基本语法 class、struct、甚至enum都可以遵循协议,如果需要遵循多个协议,使用逗号分隔遵循协议时...
    H丶ym阅读 2,479评论 3 4