美文网首页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:协议

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