美文网首页swift mvvm
Swift底层进阶--013:协议

Swift底层进阶--013:协议

作者: 帅驼驼 | 来源:发表于2021-01-22 12:10 被阅读0次

Protocol:所谓协议,就是一组属性和/或方法的定义,而如果某个具体类型想要遵守一个协议,那它需要实现这个协议所定义的所有这些内容。协议实际上做的事情不过是“关于实现的约定”

  • Swift中协议被赋予更加强大、灵活的功能。相比Objective-C的协议,Swift的协议不仅可以被用做代理,也可以用作对接口的抽象,对代码的复用。
  • 协议规定了用来实现某一特定功能所必需的方法和属性。任意能够满足协议要求的类型被称为遵循(conform)协议。
  • 类,结构体或枚举类型都可以遵循协议,并提供具体实现来完成协议定义的方法和功能。
基本⽤法
语法格式
protocol MyProtocol {
   // 属性
   // 方法
}

classstructenum都可以遵循协议,如果要遵守多个协议,使⽤逗号分隔

struct LGTeacher: Protocol1, Protocol2 {
   // 属性
   // 方法
}

如果class中有superClass,⼀般放在遵循的协议之前

class LGTeacher: NSObject, MyProtocol{
   // 属性
   // 方法
}
协议中属性定义的要求
  • 属性不能设置默认值
  • 属性必须明确是可读或可读可写
  • 属性必须使用var修饰
协议中方法定义的要求
  • 方法不能有方法体
  • 方法参数不能设置默认值
遵循协议并实现协议中的属性、方法
protocol MyProtocol {
    var age: Int { get }
    var name: String { get set }
    
    func doSomething(age: Int) -> Int
    static func teach()
}

class LGTeacher: MyProtocol {
    let age: Int = 18
    var name: String = "Zang"
    
    func doSomething(age: Int = 18) -> Int {
        print("LGTeacher doSomething")
        return age
    }
    
    static func teach() {
        print("teach")
    }
}

LGTeacher遵循MyProtocol协议

  • 实现协议的属性,可以设置默认值。只读属性可以选择let修饰
  • 实现协议的方法,参数可以设置默认值
  • 协议中使用static修饰的类型方法,可以被类或结构体遵循。被类遵循,可以选择将static替换为class修饰

func使用static或者class修饰的区别与联系

  • static或者class关键字,都可用于指定类方法
  • class关键字指定的类方法可以被子类重写
  • static关键字指定的类方法不能被子类重写
required关键字

协议也可以定义初始化⽅法,当类实现初始化器的时候,必须使⽤required关键字

protocol MyProtocol {
    init(age: Int)
}

class LGTeacher: MyProtocol{
    var age: Int
    
    required init(age: Int) {
        self.age = age
    }
}

如果当前类被final修饰,可以忽略required关键字,因为被final修饰的类不能被继承

protocol MyProtocol: AnyObject {
    init(age: Int)
}

final class LGTeacher: MyProtocol{
    var age: Int

    init(age: Int) {
        self.age = age
    }
}

使用final修饰的类被继承,编译报错

编译报错
要求协议只能被类遵循

如果要求协议只能被类遵循,可以添加AnyObject

protocol MyProtocol: AnyObject {
    init(age: Int)
}

class LGTeacher: MyProtocol{
    var age: Int

    required init(age: Int) {
        self.age = age
    }
}

structenum遵循MyProtocol协议,编译报错

编译报错
将协议作为类型
  • 作为函数、⽅法或初始化程序中的参数类型或返回类型
  • 作为常量、变量或属性的类型
  • 作为数组、字典或其他容器中项⽬的类型
案例1:

MyProtocol协议定义teach方法,在MyProtocol扩展和LGTeacher中都实现teach方法,查看案例1的打印结果:

protocol MyProtocol {
    func teach()
}

extension MyProtocol {
    func teach() {
        print("MyProtocol")
    }
}

class LGTeacher: MyProtocol {
    func teach() {
        print("MyClass")
    }
}

let t1: MyProtocol = LGTeacher()
t1.teach()

let t2: LGTeacher = LGTeacher()
t2.teach()

//输出以下内容:
//MyClass
//MyClass
  • t1声明MyProtocol类型,protocol中定义了teach⽅法,使用witness_method调用,通过PWT协议⽬击表获取对应的函数地址,打印结果MyClass
  • t2声明LGTeacher类型,使用class_method调用,通过V-table函数表查找函数,打印结果MyClass

将上述代码生成SIL文件:swiftc -emit-sil main.swift | xcrun swift-demangle

main

PWT协议⽬击表,LGTeacher遵循MyProtocol协议,并记录实现的方法

PWT

遵循协议后被实现的协议方法teach,内部使用class_method指令通过类的函数表查找函数

bb0
案例2:

如果MyProtocol是空协议,在MyProtocol扩展和LGTeacher中实现teach方法,查看案例2的打印结果:

protocol MyProtocol {}

extension MyProtocol {
    func teach() {
        print("MyProtocol")
    }
}

class LGTeacher: MyProtocol {
    func teach() {
        print("MyClass")
    }
}

let t1: MyProtocol = LGTeacher()
t1.teach()

let t2: LGTeacher = LGTeacher()
t2.teach()

//输出以下内容:
//MyProtocol
//MyClass
  • t1声明MyProtocol类型,protocol中未定义任何⽅法,直接使用extension静态调用。静态调用在编译链接之后方法地址已经确定,对于遵循协议的类来说⽆法重写,打印结果MyProtocol
  • t2声明LGTeacher类型,使用class_method调用,通过V-table函数表查找函数,打印结果MyClass

将上述代码生成SIL文件:swiftc -emit-sil main.swift | xcrun swift-demangle

main

PWT协议⽬击表,虽然LGTeacher遵循MyProtocol协议,但没有记录任何方法,同时也找不到上面定义的被实现的协议方法

PWT
案例3:

MyProtocol协议定义teach方法,仅MyProtocol扩展实现teach方法,查看案例3的打印结果:

protocol MyProtocol {
    func teach()
}

extension MyProtocol {
    func teach() {
        print("MyProtocol")
    }
}

class LGTeacher: MyProtocol {}

let t1: MyProtocol = LGTeacher()
t1.teach()

let t2: LGTeacher = LGTeacher()
t2.teach()

//输出以下内容:
//MyProtocol
//MyProtocol

t1声明MyProtocol类型,protocol中定义了teach⽅法,使用witness_method调用,通过PWT协议⽬击表获取对应的函数地址,打印结果MyProtocol
t2声明LGTeacher类型,由于类中没有实现teach方法,直接使用extension静态调⽤,打印结果MyProtocol

将上述代码生成SIL文件:swiftc -emit-sil main.swift | xcrun swift-demangle

main

PWT协议⽬击表,LGTeacher遵循MyProtocol协议,并记录实现的方法

PWT

遵循协议后被实现的协议方法teach,内部使用extension静态调用

bb0
PWT
案例1

定义Circle类遵循Shape协议,声明Shape类型和Circle类型实例变量,打印各自的大小和步长

protocol Shape{
    var area: Double{ get }
}

class Circle: Shape {
    var radious: Double
    init(_ radious: Double) {
        self.radious = radious
    }

    var area: Double{
        get{
            return radious * radious * 3.14
        }
    }
}

var shape: Shape = Circle.init(10.0)
print("shape of size:\(MemoryLayout.size(ofValue: shape))")
print("shape of stride:\(MemoryLayout.stride(ofValue: shape))")

var circle: Circle = Circle.init(10.0)
print("circle of size:\(MemoryLayout.size(ofValue: circle))")
print("circle of stride:\(MemoryLayout.stride(ofValue: circle))")

//输出以下内容:
//shape of size:40
//shape of stride:40
//circle of size:8
//circle of stride:8

shape声明为Shape类型,大小和步长占40字节circle声明为Circle类型,大小和步长只占8字节

使用lldb调式,能看到的内容有限,无法得出结论

lldb

将上述代码生成SIL文件:swiftc -emit-sil main.swift | xcrun swift-demangle

main

通过官方文档,查看init_existential_addr的作用

init_existential_addr

Existential Container是编译器⽣成的⼀种特殊数据类型,⽤于管理遵守了相同协议的协议类型。因为这些数据类型的内存空间尺⼨不同,使⽤Extential Container进⾏管理可以实现存储⼀致性

如果想进一步分析,明确init_existential_addr存储的到底是什么,需要将SIL代码再降⼀级,通过IR代码观察

将上述代码生成IR文件:swiftc -emit-ir main.swift | xcrun swift-demangle

IR

%T4main5ShapeP是一个结构体,有一个24 x i8的数组,占24字节。有一个%swift.type*的指针和一个i8**的二级指针

%T4main5ShapeP = type { [24 x i8], %swift.type*, i8** }

%4存储的是metadata

%4 = extractvalue %swift.metadata_response %3, 0

%5存储的是HeapObject,它调用了__allocating_init,传入了一个double类型和一个metadata

%5 = call swiftcc %T4main6CircleC* @"main.Circle.__allocating_init(Swift.Double) -> main.Circle"(double 1.000000e+01, %swift.type* swiftself %4)

图中第一句代码,获取结构体本身,拿到第一个元素,然后将%4也就是metadata,存储到结构体的第一个元素中

store %swift.type* %4, %swift.type** getelementptr inbounds (%T4main5ShapeP, %T4main5ShapeP* @"main.shape : main.Shape", i32 0, i32 1), align 8

图中第二句代码,获取结构体本身,拿到第二个元素,然后将PWT协议目击表,存储到结构体的第二个元素中。代码中@"protocol witness table for main.Circle : main.Shape in main"就是PWT

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.shape : main.Shape", i32 0, i32 2), align 8

图中第三句代码,实例对象main.shape是结构体首地址24 x i8数组类型,
使用bitcast将其强转为%T4main6CircleC二级指针类型,再将%5也就是HeapObject存储到强转后的指针中

store %T4main6CircleC* %5, %T4main6CircleC** bitcast (%T4main5ShapeP* @"main.shape : main.Shape" to %T4main6CircleC**), align 8

最终可以分析出%T4main5ShapeP结构体的元素构成

type { HeapObject, metadata, PWT } 

使用Swift代码还原上述结论

定义HeapObject结构体

struct HeapObject{
    var type: UnsafeRawPointer
    var refCount1: UInt32
    var refCount2: UInt32
}

定义ProtocolData结构体,value1value2value3三个属性,各自占8字节,连续内存空间存储,对应IR代码中的24 x i8数组。type对应metadatapwt对应PWT协议目击表

struct ProtocolData {
    var value1: UnsafeRawPointer
    var value2: UnsafeRawPointer
    var value3: UnsafeRawPointer
    var type: UnsafeRawPointer
    var pwt: UnsafeRawPointer
}

将实例对象shape转换为ProtocolData结构

withUnsafePointer(to: &shape){ ptr in
    ptr.withMemoryRebound(to: ProtocolData.self, capacity: 1){ pointer in
        print(pointer.pointee)
    }
}

//输出以下结果:
//ProtocolData(value1: 0x0000000100705910, value2: 0x0000000000000000, value3: 0x0000000000000000, type: 0x000000010000c3b0, pwt: 0x0000000100008070)
  • value1存储HeapObject地址,value2value3未被使用
  • type存储的metadata,实际上是VWT(Value Witness Table)
  • pwt存储的PWT协议目击表

进入终端,使用nm命令查看pwt存储的0000000100008070地址:

nm /Users/zang/Library/Developer/Xcode/DerivedData/LGSwiftTest-ezhaqfxldtlovoammsyvcyhxjxoj/Build/Products/Debug/LGSwiftTest | grep 0000000100008070

输出结果:

0000000100008070 S _$s11LGSwiftTest6CircleCAA5ShapeAAWP

使用xcrun还原符号:

xcrun swift-demangle s11LGSwiftTest6CircleCAA5ShapeAAWP

输出结果:

$s11LGSwiftTest6CircleCAA5ShapeAAWP ---> protocol witness table for LGSwiftTest.Circle : LGSwiftTest.Shape in LGSwiftTest

这也解释了为什么上面看到的Shape类型实例对象会占40字节;其中value1value2value3三个属性共占24字节typepwt共占16字节

案例2

如果定义的是结构体,遵循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 shape: Shape = Rectangle.init(10.0, 20.0)

将上述代码生成IR文件:swiftc -emit-ir main.swift | xcrun swift-demangle

IR

图中%4%5发生了变化,直接将结构体的两个double属性取出来,存储到%T4main5ShapeP结构体首地址的24 x i8数组中

将结构体类型的实例对象shape转换为ProtocolData结构

withUnsafePointer(to: &shape){ ptr in
    ptr.withMemoryRebound(to: ProtocolData.self, capacity: 1){ pointer in
        print(pointer.pointee)
    }
}

//输出以下结果:
//ProtocolData(value1: 0x4024000000000000, value2: 0x4034000000000000, value3: 0x0000000000000000, type: 0x00000001000040e0, pwt: 0x0000000100004070)

value1存储width属性,value2存储height属性,value3未被使用
type存储的metadata,实际上是VWTValue Witness Table
pwt存储的PWT协议目击表

案例3

如果结构体大小超过24字节,存储的结构会有哪些变化?

protocol Shape{
    var area: Double{ get }
}

struct Rectangle: Shape {
    var width, height: Double
    var width1: Double = 40.0
    var height1: Double = 50.0

    init(_ width: Double, _ height: Double) {
        self.width = width
        self.height = height
    }

    var area: Double{
        get{
            return width * height
        }
    }
}

var shape: Shape = Rectangle.init(10.0, 20.0)

案例2的基础上,将结构体增加width1height1两个属性,这时结构体的大小已超过24字节

withUnsafePointer(to: &shape){ ptr in
    ptr.withMemoryRebound(to: ProtocolData.self, capacity: 1){ pointer in
        print(pointer.pointee)
    }
}

//输出以下结果:
//ProtocolData(value1: 0x0000000103304080, value2: 0x0000000000000000, value3: 0x0000000000000000, type: 0x0000000100004108, pwt: 0x0000000100004098)

只有value1有值,value2value3未被使用
typepwt没有变化

通过lldb,查看value1存储的是什么

lldb
  • 结构体首地址24字节,存储的是遵循协议的结构体或类的值
  • 如果是值类型,大小在24字节以内,直接存储值。如果超过24字节,系统将在堆区分配内存空间,将值存储到堆区,然后将堆区的指针地址存储到前面8字节,此时value2value3未被使用
  • 如果是引用类型,直接存储HeapObject
案例4

声明两个协议类型实例对象,将它们存储到数组。循环打印shape.area属性时,它们可以正确调用到各自的实现方法

protocol Shape{
    var area: Double{ get }
}

class Circle: Shape {
    var radious: Double

    init(_ radious: Double) {
        self.radious = radious
    }

    var area: Double{
        get{
            return radious * radious * 3.14
        }
    }
}

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 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)
}

//输出以下内容:
//314.0
//200.0

理解了前三个案例,对于案例4的打印结果应该不会意外;本质上协议容器里存放了PWT协议目击表和metadata,在协议目击表内依然使用class_method查表方式,通过metadata 找到对应的V-table,最终调用正确的实现方法

案例5

声明Shape类型circle,将circle赋值给circle1,这时打印circlecircle1value1地址,会是相同地址吗?如果修改circle1.radious属性,他们value1的地址又会发生怎样的变化?

protocol Shape{
    var radious: Double { get set }
    var area: Double { get }
}

struct Circle: Shape {
    var radious: Double
    var radious1: Double = 3
    var radious2: Double = 6
    var radious3: Double = 9

    init(_ radious: Double) {
        self.radious = radious
    }

    var area: Double{
        get{
            return radious * radious * 3.14
        }
    }
}

struct ValueData {
    var type: HeapObject
    var val1: Double
    var val2: Double
    var val3: Double
    var val4: Double
}

var circle: Shape = Circle.init(10.0)
var circle1 = circle

withUnsafePointer(to: &circle){ ptr in
    ptr.withMemoryRebound(to: ProtocolData.self, capacity: 1){ pointer in
        let ptr = pointer.pointee.value1.assumingMemoryBound(to: ValueData.self)
        print("circle.value1 - 修改前地址:\(pointer.pointee.value1),值:\(ptr.pointee.val1)")
    }
}

withUnsafePointer(to: &circle1){ ptr in
    ptr.withMemoryRebound(to: ProtocolData.self, capacity: 1){ pointer in
        let ptr = pointer.pointee.value1.assumingMemoryBound(to: ValueData.self)
        print("circle1.value1 - 修改前地址:\(pointer.pointee.value1),值:\(ptr.pointee.val1)")
    }
}

circle1.radious = 20.0
print("\n----------\n")

withUnsafePointer(to: &circle){ ptr in
    ptr.withMemoryRebound(to: ProtocolData.self, capacity: 1){ pointer in
        let ptr = pointer.pointee.value1.assumingMemoryBound(to: ValueData.self)
        print("circle.value1 - 修改后地址:\(pointer.pointee.value1),值:\(ptr.pointee.val1)")
    }
}

withUnsafePointer(to: &circle1){ ptr in
    ptr.withMemoryRebound(to: ProtocolData.self, capacity: 1){ pointer in
        let ptr = pointer.pointee.value1.assumingMemoryBound(to: ValueData.self)
        print("circle1.value1 - 修改后地址:\(pointer.pointee.value1),值:\(ptr.pointee.val1)")
    }
}

//输出一下结果:
//circle.value1 - 修改前地址:0x0000000100406ae0,值:10.0
//circle1.value1 - 修改前地址:0x0000000100406ae0,值:10.0
//
//----------
//
//circle.value1 - 修改后地址:0x0000000100406ae0,值:10.0
//circle1.value1 - 修改后地址:0x000000010052ed30,值:20.0

circle1.radious修改前,circlecircle1value1地址是相同的。当circle1.radious发生改变后,circle1.value1的地址也发生了改变,这种现象称之为“写时复制”(copy-on-write

上述代码为什么会触发的“写时复制”?
  • Circle是结构体类型,也就是值类型,所以circlecircle1并不共享状态
  • circle1.radious发生改变,相当于修改副本,对circle没有任何影响
  • protocol结构体中24字节官方叫法是Value BufferValue Buffer用来存储当前的值,如果超过Value Buffer的最大存储容量,系统会开辟堆空间存储值,Value Buffer存储堆区指针地址
  • 修改堆空间里的值类型,在修改前会先检测引用计数,如果引用计数大于1,此时系统会开辟新的堆空间,把要修改的内容拷贝到新空间内,目的是提升性能
  • 如果是引用类型Class,则不会触发写时复制
案例6

案例5的结构体修改为Class,查看输出结果的变化

class Circle: Shape {
    var radious: Double

    init(_ radious: Double) {
        self.radious = radious
    }

    var area: Double{
        get{
            return radious * radious * 3.14
        }
    }
}

var circle: Shape = Circle.init(10.0)
var circle1 = circle

withUnsafePointer(to: &circle){ ptr in
    ptr.withMemoryRebound(to: ProtocolData.self, capacity: 1){ pointer in
        let ptr = pointer.pointee.value1.assumingMemoryBound(to: ValueData.self)
        print("circle.value1 - 修改前地址:\(pointer.pointee.value1),值:\(ptr.pointee.val1)")
    }
}

withUnsafePointer(to: &circle1){ ptr in
    ptr.withMemoryRebound(to: ProtocolData.self, capacity: 1){ pointer in
        let ptr = pointer.pointee.value1.assumingMemoryBound(to: ValueData.self)
        print("circle1.value1 - 修改前地址:\(pointer.pointee.value1),值:\(ptr.pointee.val1)")
    }
}

circle1.radious = 20.0
print("\n----------\n")

withUnsafePointer(to: &circle){ ptr in
    ptr.withMemoryRebound(to: ProtocolData.self, capacity: 1){ pointer in
        let ptr = pointer.pointee.value1.assumingMemoryBound(to: ValueData.self)
        print("circle.value1 - 修改后地址:\(pointer.pointee.value1),值:\(ptr.pointee.val1)")
    }
}

withUnsafePointer(to: &circle1){ ptr in
    ptr.withMemoryRebound(to: ProtocolData.self, capacity: 1){ pointer in
        let ptr = pointer.pointee.value1.assumingMemoryBound(to: ValueData.self)
        print("circle1.value1 - 修改后地址:\(pointer.pointee.value1),值:\(ptr.pointee.val1)")
    }
}

//输出一下结果:
//circle.value1 - 修改前地址:0x0000000103204a20,值:10.0
//circle1.value1 - 修改前地址:0x0000000103204a20,值:10.0
//
//----------
//
//circle.value1 - 修改后地址:0x0000000103204a20,值:20.0
//circle1.value1 - 修改后地址:0x0000000103204a20,值:20.0

Circle修改为Class类型,也就是引用类型,所以circlecircle1共享状态。当circle1.radious发生改变,circle也会随之改变

相关文章

  • Swift底层进阶--013:协议

    Protocol:所谓协议,就是一组属性和/或方法的定义,而如果某个具体类型想要遵守一个协议,那它需要实现这个协议...

  • Swift - 协议底层

    首先我们来看一段代码 那么请问各位看官, draws中存储的是什么呢?事实上,在这种情况下,变量 draws 中存...

  • Swift进阶-协议

    class是本质上定义了一个对象是什么; protocol是本质上定义了一个对象有哪些行为。 一、协议的基本语法 ...

  • Swift进阶之RxSwift(四)

    前言 Swift进阶之RxSwift(一) Swift进阶之RxSwift(二) Swift进阶之RxSwift(...

  • Swift-进阶:Mirror源码解析

    本文主要是分析Mirror的底层实现,以及根据Mirror底层原理仿写其结构的实现 在Swift-进阶:反射Mir...

  • Swift进阶:协议Protocol

    swift 进阶之路:学习大纲[https://www.jianshu.com/p/115367c3eefd] 本...

  • Swift 进阶 - 集合协议

    Swift 中集合协议是数组、字典、集合和字符串实现的基础,有一些数据结构和算法的知识,理解这部分内容更容易一些。...

  • Swift进阶十二:协议

    一:对比Swift协议与其他语言 泛型可以帮助我们写出动态的程序。协议可以与函数和泛型协同工作,让我们代码的动态特...

  • 从零学习Swift 17: 面向协议编程

    swift官方API中到处都有面向协议编程的影子.在swift中,很多数据类型的底层都是struct结构体,而结构...

  • Swift底层进阶--016:Moya

    Moya典型的特性是面向协议编程(Protocol Oriented Programming即:POP),相比面向...

网友评论

    本文标题:Swift底层进阶--013:协议

    本文链接:https://www.haomeiwen.com/subject/qskqzktx.html