Swift协议

作者: 正_文 | 来源:发表于2022-09-12 15:09 被阅读0次

    一、协议的介绍

    协议的定义方式与结构体枚举的定义非常相似:

    protocol SomeProtocol {
        // 这里是协议的定义部分
    }
    

    要让自定义类型遵循某个协议,在定义类型时,需要在类型名称后加上协议名称,中间以冒号(:)分隔。遵循多个协议时,各协议之间用逗号(,)分隔。
    若是一个类拥有父类,应该将父类名放在遵循的协议名之前,以逗号分隔。

    属性

    协议可以要求遵循协议的类型提供特定名称和类型的实例属性类型属性。协议不指定属性是存储属性还是计算属性,它只指定属性的名称和类型

    1. 协议要求一个属性必须明确可读的/可读可写的,类型声明后加上 { set get }来表示属性是可读可写的;
    2. 属性要求定义为变量类型,即使用var而不是let。
    protocol SomeProtocol {
        var mustBeSettable: Int { get set }          //可读可写
        var doesNotNeedToBeSettable: Int { get }     //可读
    }
    
    方法

    在协议中定义方法,只需要定义当前方法的名称、参数列表和返回值。类遵循了协议,必须实现协议中的方法。
    协议中也可以定义初始化方法,当实现初始化器时,必须使用required关键字。
    如果一个协议只能被类实现,需要协议继承自AnyObject。如果此时结构体遵守该协议,会报错。
    mutating 在方法中改变方法所属的实例。

    protocol Togglable {
        mutating func toggle()
    }
    
    enum OnOffSwitch: Togglable {
        case off, on
        mutating func toggle() {
            switch self {
            case .off:
                self = .on
            case .on:
                self = .off
            }
        }
    }
    var lightSwitch = OnOffSwitch.off
    lightSwitch.toggle()
    

    注意:实现协议中的 mutating 方法时,若是类类型,则不用写mutating关键字。而对于结构体和枚举,则必须写mutating关键字。

    二、协议作为类型

    尽管协议本身并未实现任何功能,但是协议可以被当做一个功能完备的类型来使用。使用场景如下:
    1、作为函数、方法或构造器中的参数类型或返回值类型;
    2、作为常量、变量或属性的类型;
    3、作为数组、字典或其他容器中的元素类型。
    首先,以下代码,通过继承基类实现的方式,如下:

    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)
    }
    
    //打印结果:314.0   200.0
    

    改为协议的方式实现,如下:

    //1、将Shape改为protocol类型;
    //2、删除实现该协议的类的协议方法的前缀override
    protocol Shape{
        var area: Double{get}
    }
    

    思考:shapes数组的内存是什么情况?
    1、如果,元素指定的Shape是时,数组中存储的都是引用类型的地址。
    2、如果,元素指定的Shape是协议时,数组中存储的是什么?

    通过协议代码分析

    示例1:

    protocol MyProtocol {
        func teach()
    }
    extension MyProtocol{
        func teach(){ print("MyProtocol") }
    }
    class MyClass1: MyProtocol{
        func teach(){ print("MyClass1") }
    }
    
    let object: MyProtocol = MyClass1()
    object.teach()
    let object1: MyClass1 = MyClass1()
    object1.teach()
    
    //打印结果:
    //MyClass1
    //MyClass1
    

    通过SIL,我们可以看到一个新的结构witness_table,也叫PWT(协议目录表)

    image.png

    打印结果分析:
    对象为MyProtocol类型时,方法teach的调用在底层是通过witness_method调用,即通过PWT(协议目录表)获取对应的函数地址,其内部也是通过类的函数表查找进行调用。
    对象为MyClass类型时,方法teach的调用在底层是通过类的函数表来查找函数,主要是基于类的实际类型。

    示例2,修改示例1代码:

    protocol MyProtocol {
        //func teach()
    }
    
    //打印结果:
    //MyProtocol
    //MyClass1
    

    查看SIL,其中已经没有teach方法。
    如果没有声明在Protocol中的函数,只是通过Extension提供了一个默认实现,在Extension中声明的方法是静态调用,其函数地址在编译过程中就已经确定了,对于遵守协议的类来说,这种方法是无法重写的。

    image.png

    打印不同的原因:MyProtocol协议扩展中实现的teach方法不能被类重写,相当于这是两个方法,并不是同一个
    第一个打印MyProtocol,是因为调用的是协议扩展中的teach方法,这个方法的地址是在编译时期就已经确定的,即通过静态函数地址调度;
    第二个打印MyClass,同上个例子一样,是类的函数表调用。

    示例3,再次修改代码:

    protocol MyProtocol {
        func teach()
    }
    extension MyProtocol{
        func teach(){ print("MyProtocol") }
    }
    class MyClass1: MyProtocol{
        //func teach(){ print("MyClass1") }
    }
    
    let object: MyProtocol = MyClass1()
    object.teach()
    let object1: MyClass1 = MyClass1()
    object1.teach()
    
    //打印结果:
    //MyProtocol
    //MyProtocol
    

    以上可以理解为protocol增加可选实现方法,也可以通过@objc和optional实现。

    三、PWT内存

    通过研究函数调度,我们知道V-Table是存储在metadata中的,那么协议的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 circle1: Shape = Circle(10.0)
    print(MemoryLayout.size(ofValue: circle1))
    print(MemoryLayout.stride(ofValue: circle1))
    
    var circle2: Circle = Circle(10.0)
    print(MemoryLayout.size(ofValue: circle2))
    print(MemoryLayout.stride(ofValue: circle2))
    
    //打印结果:
    //40   40
    //8    8
    

    circle1的打印都是40,先LLDB尝试一下。

    image.png
    接着看看SIL,系统通过调用init_existential_addr读取之前声明的circle1变量,而circle1却是通过调用load指令读取的。

    SIL官方文档对init_existential_addr的解释如下:
    其中的existential container是编译器生成的一种特殊的数据类型,也用于管理遵守了相同协议的协议类型。因为这些数据类型的内存空间尺寸不同,使用existential container进行管理可以实现存储一致性
    对应的,以上代码可以理解为:使用了包含Circle的existential container来初始化circle引用的内存。通俗来说就是将circle包装后,存入existential container初始化的内存。

    仿写内存结构

    // 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)
        }
    }
    
    //打印结果:
    //protocolData(value1: 0x00000001005d2900, value2: 0x0000000000000000, 
    //value3: 0x0000000000000000, type: 0x0000000100008278, pwt: 0x0000000100004020)
    
    运行LLDB: image.png

    总结:PWT存储在一个existential container容器中,该容器的大致结构是{ heapObject, metadata, PWT }。

    下面我们分别来分析一下struct和class的pwt的内存管理方式。

    struct和协议

    新建struct实现协议,struct包含3个属性:

    struct Rectangle: Shape{
        var width, height: Int
        var width1 = 30
        init(_ width: Int, _ height: Int) {
            self.width = width
            self.height = height
        }
    
        var area: Double{
            get{
                return Double(width * height)
            }
        }
    }
    
    var rectangle: Shape = Rectangle.init(1, 2)
    withUnsafePointer(to: &rectangle) { ptr in
        ptr.withMemoryRebound(to: protocolData.self, capacity: 1) { pointer in
            print(pointer.pointee)
        }
    }
    //打印结果:protocolData(value1: 0x0000000000000001, value2: 0x0000000000000002,
    //value3: 0x000000000000001e, type: 0x00000001000041c8, pwt: 0x0000000100004040)
    

    观察结果:三个value,分别保存了三个属性的值
    修改struct为4个属性,如下:

    image.png
    此时,value1是一个堆区地址,这个地址里面存储的是struct各个属性的值

    总结:针对协议,对象底层的存储结构如下:
    1、前24个字节,主要用于存储遵循了协议class/struct属性值。如果24字节不够存储,会在堆区开辟一个内存空间用于存储,24字节中的前8个字节存储堆区地址。即,如果超出24,是直接分配堆区空间,然后存储值,并不是先存储值,然后发现不够再分配堆区空间。
    2、后16个字节,分别用于存储 存放metadata(目的是为了找到Value Witness Table值目录表)、pwt(协议目录表)。

    3.2 class—写时复制(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: Int
        init(_ width: Int, _ height: Int) {
            self.width = width
            self.height = height
        }
    
        var area: Double{
            get{
                return Double(width * height)
            }
        }
    }
    
    var circle: Shape = Circle.init(10.0)
    var rectangle: Shape = Rectangle.init(10, 20)
    //所谓的多态:根据具体的类来决定调度的方法
    var shapes: [Shape] = [circle, rectangle]
    //这里能区分不同area的原因是因为 在protocol中存放了pwt(协议目录表),可以根据这个表来正确调用对应的实现方法(pwt中也是通过class_method查找,
    //同时在运行过程中也记录了metadata,在pwt中通过metadata查找V-Table,从而完成当前方法的调用)
    for shape in shapes{
        print(shape.area)
    }
    //打印结果:314.0    200.0
    

    继续回到struct的例子,将其赋值给另一个变量,其内存存放的是否是一样的?

    //对象类型为协议
    var rectangle1: Shape = Rectangle(10, 20)
    //将其赋值给另一个协议变量
    var rectangle2: Shape  = rectangle
    
    withUnsafePointer(to: &rectangle1) { ptr in
        ptr.withMemoryRebound(to: protocolData.self, capacity: 1) { pointer in
            print(pointer.pointee)
        }
    }
    withUnsafePointer(to: &rectangle2) { ptr in
        ptr.withMemoryRebound(to: protocolData.self, capacity: 1) { pointer in
            print(pointer.pointee)
        }
    }
    
    //打印结果是一致的
    

    两个协议变量内存存放的东西是一样的。
    继续,修改rectangle1width属性的值(需要将width属性声明到protocol),修改后的代码如下:

    //修改协议
    protocol Shape {
        var area: Double {get}
        var width: Int {get set}
    }
    
    rectangle2.width = 50
    

    通过lldb调试发现,在rectangle2变量修改width之后,其存储数据的堆区地址发生了变化。这就是所谓的写时复制当复制时,并没有值的修改,所以两个变量指向同一个堆区内存,当第二个变量修改了属性值时,会将原本堆区内存的值拷贝到一个新的堆区内存,并进行值的修改。
    继续,如果将struct修改为class,lldb调试结果如下,属性值修改前后,堆区地址并没有变化。
    也就是说,以上分析,符合对值类型引用类型的理解:

    • 值类型: 在传递过程中并不共享状态;
    • 引用类型: 在传递过程中共享状态。
    四、Value Buffer

    struct结构体中24字节官方叫法是Value Buffer
    Value Buffer用来存储当前的值,如果超过存储的最大容量的话会开辟一块空间。
    针对值类型来说在赋值时会先拷贝heapobject地址(Copy on write)。在修改时会先检测引用计数,如果引用计数大于1,此时开辟新的堆空间把要修改的内容拷贝到新的堆空间(这么做为了提升性能)。
    Value Buffer在容器existential container中的位置:

    image.png

    总结:

    1. classstructenum都可以遵守协议,有以下几点说明:
      1.1 多个协议之间需要使用逗号分隔;
      1.2 如果class中有superClass,一般放在协议之前
    2. 协议中可以添加属性,有以下两点说明:
      2.1 属性必须明确是 可读(get)/可读可写(get + set)的;
      2.2 属性使用var修饰。
    3. 协议中可以定义方法
      3.1 定义方法时,只需要定义当前方法的名称+参数列表+返回值,其具体实现可以通过协议的extension实现,或者在遵守协议时实现
      3.2. 协议中也可以定义初始化方法,当实现初始化器时,必须使用required关键字。
    4. 如果协议只能被class实现,需要协议继承自AnyObject
    5. 协议也可以作为类型,有以下三种场景:
      5.1 作为函数、方法或者初始化程序中的参数类型或者返回值
      5.2 作为常量变量属性的类型;
      5.3 作为数组字典或者其他容器中项目的类型。
    6. 协议的底层存储结构:24字节valueBuffer + vwt(8字节) + pwt(8字节)
      6.1 前24个字节,官方称为Value Buffer,主要用于存储遵循了协议的class/struct的属性值
      6.2 如果超过Value Buffer最大容量。值类型 采用 copy-write引用类型 则是使用同一个堆区地址;
      6.3 后16个字节分别用于存储 vwt(值目录表)pwt(协议目录表)

    相关文章

      网友评论

        本文标题:Swift协议

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