swift学习

Swift — 协议(Protocol)

2021-03-17  本文已影响0人  just东东

Swift — 协议(Protocol)

[TOC]

协议定义了一个蓝图,规定了用来实现某一特定任务或者功能的方法、属性,以及其他需要的东西。类、结构体和枚举都可以遵循协议,并为协议定义的这些要求提供具体实现。某个类型能够满足某个协议的要求,就可以说该类型遵循这个协议。

除了遵循协议的类型必须实现的要求外,还可以对协议进行扩展,通过扩展来实现一部分要求或者实现一些附加功能,这些遵循协议的类型就能够使用这些功能。

1. 协议的基本用法

1.1 协议语法

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

protocol SomeProtocol {
    // 这里是协议的定义部分
}
struct SomeStructure: FirstProtocol, AnotherProtocol {
    // 这里是结构体的定义部分
}
class SomeClass: SomeSuperClass, FirstProtocol, AnotherProtocol {
    // 这里是类的定义部分
}

1.2 属性要求

我们可以在协议中添加属性,但需要注意以下几点:

  1. 属性可以是实例属性和类型属性
  2. 属性需要使用var修饰,不能属于let
  3. 类型属性只能使用static修饰,不能使用class
  4. 我们需要声明属性必须是可读的或者可读可写的
protocol SomeProtocol {
    var propertyOne: Int { get set }
    var propertyTwo: Int { get }
    static var propertyThree: Int { get set }
}

1.3 方法要求

我们可以在协议中添加方法,但需要注意以下几点:

  1. 可以是实例方法或类方法
  2. 像普通方法一样放在协议定义中,但不需要大括号和方法体
  3. 协议中不支持为协议中的方法提供默认参数
  4. 协议中的类方法也只能使用static关键字作为前缀,不能使用class
  5. 可以使用mutating提供异变方法,以使用该方法时修改实体的属性等。
  6. 可以定义构造方法,但是使用的时候需要使用required关键字
protocol SomeProtocol {
    func someMethod1()
    func someMethod2() ->Int
}

构造方法

protocol SomeProtocol {
    init(param: Int)
}

class SomeClass: SomeProtocol {
    required init(param: Int) { }
}

异变方法

protocol Togglable {
    mutating func toggle()
}

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

1.4 协议作为类型

尽管协议本身并未实现任何功能,但是协议可以被当做一个功能完备的类型来使用。协议作为类型使用,有时被称作「存在类型」,这个名词来着存在着一个类型T,该类型遵循协议T。

协议可以像其他普通类型一样使用,使用场景如下:

protocol SomeProtocol { }

class SomeClass {
    required init(param: SomeProtocol) {}
}

1.5 其他

更多的关于协议的用法请参考:

Protocols

以及它的译文:

SwiftGG 协议

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

print(circle.area)
print(rectangle.area)

<!--打印结果-->
314.0
200.0

此时的打印结果是符合我们的预期的。

我们知道协议可以扩展,此时我们把协议的代码修改成如下:

protocol Shape {
//    var area: Double {get}
}
extension Shape{
    var area: Double {
        get{return 0.0}
    }
}

<!--打印结果-->
0.0
0.0

此时并没有如我们预期的打印,如果我们声明变量的时候写成如下呢:

var circle: Circle = Circle.init(10.0)
var rectangle: Rectangle = Rectangle.init(10.0, 20.0)

<!--打印结果-->
314.0
200.0

此时的打印就符合我们的预期了。

其实我们也能够清楚的了解到为什么会打印0.0,在Swift 方法调度这篇文章中我们介绍了extension中声明的方法是静态调用的,也就是说在编译后当前代码的地址已经确定,我们无法修改,当声明为Shap类型后,默认调用的就是Shape extension中的属性的get方法。下面我们在通过sil代码来验证一下,关于生成sil代码的方法,请参考我以前的文章。

为了方便查看,我们精简并修改代码为如下:

protocol Shape {
//    var area: Double {get}
}
extension Shape{
    var area: Double {
        get{return 0.0}
    }
}

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.init(10.0)

var a = circle.area

生成的sil代码:


image

通过sil代码我们可以清晰的看到,这里直接调用的Shape.area.getter方法。

下面我们换一些简单的代码再次看一下:

protocol PersonProtocol {
    func eat()
}
extension PersonProtocol{
    func eat(){ print("PersonProtocol eat") }
}
class Person: PersonProtocol{
    func eat(){ print("Person eat") }
}
let p: PersonProtocol = Person()
p.eat()
let p1: Person = Person()
p1.eat()

<!--打印结果-->
Person eat
Person eat

可以看到上面这段代码的打印结果都是Person eat,那么为什么会打印相同的结果呢?首先通过代码我们可以知道,在PersonProtocol中声明了eat方法。对于声明的协议方法,如果类中也实现了,就不会调用协议扩展中的方法。上面的属性的例子中并没有在协议中声明属性,只是在协议扩展中添加了一个属性。下面我们看看上面这段代码的sil代码:

image

首先我们可以看到,对于两个eat方法的确实存在不同,首先声明为协议类型的变量调用eat方法是通过witness_method调用,另一个则是通过class_method调用。

image

在刚刚sil代码中我们可以找到sil_witness_table,在里面有PersonProtocol.eat方法,找到PersonProtocol.eat方法可以发现里面是调用class_method寻找的类中VTablePerson.eat方法。

如果我们不在协议中声明eat方法:

protocol PersonProtocol {
//    func eat()
}
extension PersonProtocol{
    func eat(){ print("PersonProtocol eat") }
}
class Person: PersonProtocol{
    func eat(){ print("Person eat") }
}
let p: PersonProtocol = Person()
p.eat()
let p1: Person = Person()
p1.eat()

<!--打印结果-->
PersonProtocol eat
Person eat

查看sil代码:

image

此时我们可以看到,对于不在协议中声明方法的时候,依然是直接调用(静态调用)。

所以对于协议中方法的调度:

3. 协议原理探索

在上面探索协议中的方法调用的时候,我们提到过PWT也就是Protocol witness table,协议目击表,那么它存储在什么地方呢?我们在Swift 方法调度这篇文章中讲过,V-Table是存储在metadata中的,那么我们就探索一下PWT的存储位置。

3.1 内存占用

首先我们先来看看如下代码的的打印结果:

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

<!--打印结果-->
40
40
8
8

3.2 lldb探索内存结构

看到这个打印结果我能第一时间想到的就是生命为协议类型会存储更多的信息。生命为类的时候,存储的是类的实例对象的指针8字节。下面我们通过lldb调试来探索一下这个40字节都存储了什么信息。

image

3.3 sil 探索内存结构

通过lldb我们可以看到其内部应该存储着一些信息,那么具体存了什么呢?我们在看看sil代码:

image

在sil代码中我们可以看到,在初始化circle这个变量的时候使用到了init_existential_addr,查看SIL文档

image

译文:用一个准备好包含类型为$T的存在容器部分初始化%0引用的内存。该指令的结果是一个地址,该地址引用了所包含值的存储空间,该存储空间仍然没有初始化。包含的值必须存储为-d或copy_addr-ed,以便完全初始化存在值。如果存在容器的值未初始化时需要销毁,则必须使用deinit_existential_addr来完成此操作。可以像往常一样使用destroy_addr销毁完全初始化的存在性容器。销毁一个部分初始化存在容器的addr是未定义的行为。

文档中的意思是,使用了包含$Texistential container来初始化%0引用的内存。在这里就是使用包含Circleexistential container来初始化circle引用的内存,简单来说就是将circle包装到了一个existential container初始化的内存。

existential container是编译器生成的一种特殊的数据类型,也用于管理遵守了相同协议的协议类型。因为这些塑化剂类型的内存空间尺寸不同,使用existential container进行管理可以实现存储一致性。

3.4 IR代码探索内存结构

那么这个existential container都包装了什么呢?目前通过sil代码是看不出来什么了,那么我们就看看IR代码:

; 一个结构体,占用24字节内存的数组,wift.type指针, i8*指针
%T4main5ShapeP = type { [24 x i8], %swift.type*, i8** }

define i32 @main(i32 %0, i8** %1) #0 {
entry:
  %2 = bitcast i8** %1 to i8*
  ; main.Circle 的 metadata
  %3 = call swiftcc %swift.metadata_response @"type metadata accessor for main.Circle"(i64 0) #7
  %4 = extractvalue %swift.metadata_response %3, 0
  ;init放
  %5 = call swiftcc %T4main6CircleC* @"main.Circle.__allocating_init(Swift.Double) -> main.Circle"(double 1.000000e+01, %swift.type* swiftself %4)
  ; 存%4 也就是metadata,存到T4main5ShapeP结构体中,这里存的位置是第二个位置
  store %swift.type* %4, %swift.type** getelementptr inbounds (%T4main5ShapeP, %T4main5ShapeP* @"main.circle : main.Shape", i32 0, i32 1), align 8
  ; 存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.circle : main.Shape", i32 0, i32 2), align 8
  ; 存放%5到二级指针,%5是init出来的对象,所以这里也就是个HeapObject结构,也就是T4main6CircleC结构体的第一个8字节内存空间处
  store %T4main6CircleC* %5, %T4main6CircleC** bitcast (%T4main5ShapeP* @"main.circle : main.Shape" to %T4main6CircleC**), align 8
}

从IR代码中我们可以知道,这里面的存储是一个结构体,结构体中主要分为三个方面:

  1. 一个连续的24字节空间
  2. 一个存放metadata的指针
  3. 存放pwt指针

3.5 仿写

下面我们就来仿写一下这个结构:

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

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
}

3.5.1 类遵循协议重绑定

进行内存的重新绑定:

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)

// 将circle强转为protocolData结构体
withUnsafePointer(to: &circle) { ptr in
    ptr.withMemoryRebound(to: protocolData.self, capacity: 1) { pointer in
        print(pointer.pointee)
    }
}

<!--打印结果-->
protocolData(value1: 0x00000001006082b0, value2: 0x0000000000000000, value3: 0x0000000000000000, type: 0x0000000100008180, pwt: 0x0000000100004028)
image

通过lldb查看:

  1. 我们也可以看到对应HeapObject结构
    1. 该结构存储的是Circle的实例变量
    2. 并且在这里面的metadataprotocolData里面的存储的metadata的地址是一致的;
  2. 通过cat address命令查看pwt对应的指针,可以看到这段内存对应的就是SwiftProtocol.Circleprotocol witness table

至此我们就清楚的找到你了PWT的存储位置,PWT存在协议类型实例的内存结构中。

3.5.2 结构体遵循协议重绑定

在上面这个例子中我们使用的是类,我们知道类是引用类型,如果换成结构体呢?

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
}


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

<!--打印结果-->
protocolData(value1: 0x4024000000000000, value2: 0x4034000000000000, value3: 0x0000000000000000, type: 0x0000000100004098, pwt: 0x0000000100004028)
image

此时我们可以看到,此时并没有存储一个HeapObject结构的指针,而是直接存储Double类型的值,metadatapwt没有变。

在看下IR代码:

define i32 @main(i32 %0, i8** %1) #0 {
entry:
  %2 = bitcast i8** %1 to i8*
  %3 = call swiftcc { double, double } @"main.Rectangle.init(Swift.Double, Swift.Double) -> main.Rectangle"(double 1.000000e+01, double 2.000000e+01)
  ; 10
  %4 = extractvalue { double, double } %3, 0
  ; 20
  %5 = extractvalue { double, double } %3, 1
  ;metadata
  store %swift.type* bitcast (i64* getelementptr inbounds (<{ i8**, i64, <{ i32, i32, i32, i32, i32, i32, i32 }>*, i32, i32 }>, <{ i8**, i64, <{ i32, i32, i32, i32, i32, i32, i32 }>*, i32, i32 }>* @"full type metadata for main.Rectangle", i32 0, i32 1) to %swift.type*), %swift.type** getelementptr inbounds (%T4main5ShapeP, %T4main5ShapeP* @"main.rectangle : main.Shape", i32 0, i32 1), align 8
  ;pwt
  store i8** getelementptr inbounds ([2 x i8*], [2 x i8*]* @"protocol witness table for main.Rectangle : main.Shape in main", i32 0, i32 0), i8*** getelementptr inbounds (%T4main5ShapeP, %T4main5ShapeP* @"main.rectangle : main.Shape", i32 0, i32 2), align 8
  ;存%4 也就是10
  store double %4, double* getelementptr inbounds (%T4main9RectangleV, %T4main9RectangleV* bitcast (%T4main5ShapeP* @"main.rectangle : main.Shape" to %T4main9RectangleV*), i32 0, i32 0, i32 0), align 8
  ; 存%5 也就是20
  store double %5, double* getelementptr inbounds (%T4main9RectangleV, %T4main9RectangleV* bitcast (%T4main5ShapeP* @"main.rectangle : main.Shape" to %T4main9RectangleV*), i32 0, i32 1, i32 0), align 8
}

通过IR代码我们可以看到:

那么如果有3个属性呢?

struct Rectangle: Shape{
    var width, width1, height: Double
    init(_ width: Double, _ width1: Double, _ height: Double) {
        self.width = width
        self.width1 = width1
        self.height = height
    }

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

<!--内存绑定后的打印结果-->
protocolData(value1: 0x4024000000000000, value2: 0x4034000000000000, value3: 0x403e000000000000, type: 0x0000000100004098, pwt: 0x0000000100004028)

这个三个Value的值分别是10,20,30

那如果是4个呢?

struct Rectangle: Shape{
    var width, width1, height, height1: Double
    init(_ width: Double, _ width1: Double, _ height: Double, _ height1: Double) {
        self.width = width
        self.width1 = width1
        self.height = height
        self.height1 = height1
    }

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

var rectangle: Shape = Rectangle(10.0, 20.0, 30.0, 40.0)

<!--内存绑定后的打印结果-->
protocolData(value1: 0x0000000100715870, value2: 0x0000000000000000, value3: 0x0000000000000000, type: 0x00000001000040c0, pwt: 0x0000000100004050)

此时并没有直接看到Double值了,查看value1的内存:

image

此时我们可以看到,这个内存中存储了10,20,30,40这四个值。

所以如果我们需要存储的数据超过了24 x i8*,也就是24字节时,就会开辟内存空间进行存储。这里只存储指向新开辟内存空间的指针。

这里的顺序是,如果不够存储就直接开辟内存空间,存储值,记录指针。而不是先存储不够了在开辟内存空间。

我们都知道,结构体是值类型,如果超过这24字节的存储空间就会开辟内存用来存储结构体中的值,如果此时发生拷贝会是神马结构呢?下面我们就来验证一下:

结构体拷贝:

protocol Shape {
    var area: Double {get}
}
struct Rectangle: Shape{
    var width, width1, height, height1: Double
    init(_ width: Double, _ width1: Double, _ height: Double, _ height1: Double) {
        self.width = width
        self.width1 = width1
        self.height = height
        self.height1 = height1
    }

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

var rectangle: Shape = Rectangle(10.0, 20.0, 30.0, 40.0)
var rectangle1 = rectangle

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


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

<!--打印结果-->
protocolData(value1: 0x000000010683bac0, value2: 0x0000000000000000, value3: 0x0000000000000000, type: 0x00000001000040c0, pwt: 0x0000000100004050)
protocolData(value1: 0x000000010683bac0, value2: 0x0000000000000000, value3: 0x0000000000000000, type: 0x00000001000040c0, pwt: 0x0000000100004050)

此时我们看到打印结果是一样的。

那么修改呢?

添加如下代码:

protocol Shape {
    // 为了方便修改,在这声明一下
    var width: Double {get set}
    var area: Double {get}
}

rectangle1.width = 50
image

通过lldb重新打印,我们可以看到在修改值后,内存地址已经修改了,此时就是写时复制。当复制时并没有值的修改,所以两个变量指向同一个堆区内存。当修改变量的时候,会原本的堆区内存的值拷贝到一个新的内存区域,并进行值的修改。

如果我们将struct修改成class,这里并不会触发写时复制,因为在Swift中类是引用类型,修改类的值就是修改其引用地址中的值。这里就不验证了,感兴趣的可以自己去试试。

如果我们将Double换成String原理也是一致的,这里也就不一一验证了。

3.5.3 小结

至此我们也就清楚了,为什么协议中通过witness_method调用,最终能找到V-Table中的方法,原因就是存储了metadatapwt。这也是我们都声明为协议类型,最终能打印出不同形状的面积根本原因。

4. 总结

至此我们对Swift中协议的分析就结束了,现总结如下:

  1. Swift中类、结构体、枚举都可以遵守协议
  2. 遵守多个协议使用逗号(,)分隔
  3. 有父类的,父类写在前面,协议在后面用逗号(,)分隔
  4. 协议中可以添加属性
    1. 属性可以是实例属性和类型属性
    2. 属性需要使用var修饰,不能属于let
    3. 类型属性只能使用static修饰,不能使用class
    4. 我们需要声明属性必须是可读的或者可读可写的
  5. 协议中可以添加方法
    1. 可以是实例方法或类方法
    2. 像普通方法一样放在协议定义中,但不需要大括号和方法体
    3. 协议中不支持为协议中的方法提供默认参数
    4. 协议中的类方法也只能使用static关键字作为前缀,不能使用class
    5. 可以使用mutating提供异变方法,以使用该方法时修改实体的属性等。
    6. 可以定义构造方法,但是使用的时候需要使用required关键字
  6. 如果定义由类专属协议,则需要继承自AnyObject
  7. 协议可以作为类型
    1. 作为函数、方法或构造器中的参数类型或返回值类型
    2. 作为常量、变量或属性的类型
    3. 作为数组、字典或其他容器中的元素类型
  8. 协议的底层存储结构是:24字节的ValueBuffer+ metadata(8字节,也就是vwt) + pwt(8字节)
    1. 前24字节,官方说法是ValueBuffer,主要用于存储遵循了协议的实体的属性值
    2. 如果超过ValueBuffer最大容量就会开辟内存进行存储,此24字节拿出8字节存储指向该内存区域的指针
    3. 目前对于类,发现其存储的都是指针
    4. 存储metadata是为了查找遵守协议的实体中实现协议的方法
    5. pwt就是protocol witness table协议目击表,存储协议中的方法
上一篇 下一篇

猜你喜欢

热点阅读