9、协议与泛型

289 阅读8分钟

一、协议

1.1 协议的用法

  • class 本质上是定义了对象是什么
  • protocol 本质上定义了一个对象有哪些行为
class LGTeacher{
    var age = 10
    var name = "Kody"
}

extension LGTeacher:CustomStringConvertible{
    var description: String {
        get{
            return "LGTeacher-age:\(age) name:\(name)"
        }
    }
}


class Dog{
    var name = "糯米"
    var type = "白梗"
}

extension Dog:CustomStringConvertible{
    var description: String {
        get{
            return "Dog-age:\(name) name:\(type)"
        }
    }
}

1.2 协议的基本语法

  • 协议要求属性必须是var类型
  • 并且明确 get 或 get set,实现协议者必须实现对应的 get set
  • 只声明了 get 并不是说这个属性一定是计算属性
protocol MyProtocal{
    var age:Int{ get set }
    var name:String{ get }
}

class CTTeacher:MyProtocal{
    var age: Int = 18
    var name: String
    
    init(_ name:String) {
        self.name = name
    }
}

协议中的异变方法

  • 表示在该方法可以改变其所属的实例以及该实例的所有属性(用于枚举结构体
  • 实现该方法的时候不需要写 mutating 关键字
protocol Togglable{
    mutating func togglefunc()
}

struct CTTeacher2:Togglable{
    mutating func togglefunc() {
        
    }
}

class CTTeacher3:Togglable{
    func togglefunc() {
        
    }
}

类在实现协议中的初始化器

  • 必须使用 required 关键字修饰初始化器的实现
  • 类的初始化器前添加 required 修饰符来表明所有该类的子类必须实现该初始化器
protocol MyProtocal2{
    init(_ age:Int)
}

class CTTeacher4:MyProtocal2{
    var age:Int = 0
    required init(_ age: Int) {
        self.age = age
    }
}

//或者前面加 final 表示该类不可继承
final class CTTeacher4:MyProtocal2{
    var age:Int = 0
    init(_ age: Int) {
        self.age = age
    }
}

类专用协议

  • 通过添加 AnyObject 关键字到协议的继承列表,来限制此协议只能被 class类 遵循
protocol MyProtocal3:AnyObject{
    
}

class CTTeacher5:MyProtocal3{

}

//报错:Non-class type 'CTTeacher6' cannot conform to class protocol 'MyProtocal3'
struct CTTeacher6:MyProtocal3{

}

可选协议

  • 如果我们不想强制让遵循协议的类实现,可以使用 optional 前缀
  • 注意要标识为@objc
@objc protocol MyProtocal4{
    @objc optional func increment(by:Int)
}

1.3 witness_table

再来个例子:

一般类的方法都会在v-table里,那么遵循了协议的方法,会不会也在v-table里?

protocol MyProtocal4{
    func increment(by:Int)
}

class CCTeacher:MyProtocal4{
    
    //在不在v-table里面?
    func increment(by: Int) {
        print(by)
    }
}

var t:CCTeacher = CCTeacher()
t.increment(by: 10)

其中increment()方法,编译成sil看一下:

看main函数,是一个class_method调度,也就是在v_table中调度 Snipaste_2022-07-08_14-24-22.png

再看到最后面的v_table: Snipaste_2022-07-08_14-26-10.png

但如果 var t:CCTeacher = CCTeacher() 改为:var t:MyProtocal4 = CCTeacher()呢?

先看一下main函数: 这次不是class_method调度了,是witness_method调度 Snipaste_2022-07-08_15-07-47.png

再看看v-table,会发现v_table中确实也有increament()方法,但是多了一个witness_table表。 Snipaste_2022-07-08_15-08-25.png

也就是说main函数是在witness_table表中查找对应的方法。文档链接 Snipaste_2022-07-08_15-15-54.png

那再看看看witness_table如何调用到increament()方法的:本质上最终会通过class_method调度,去到CCTeacher类的v_table中调度increament()方法,也就是说witness_table可以理解为一个中间层。 ![Snipaste_2022-07-08_15-22-28.png](p1-juejin.byteimg.com/tos-cn-i-k3…

1.4 协议底层探索

protocol Incrementable{
     func increment(by: Int)
}

extension Incrementable{
    func increment(by: Int){
        print("Incrementable")
    }
}

class CCTeacher:Incrementable{
    func increment(by: Int) {
        print("CCTeacher")
    }
}

var t:Incrementable = CCTeacher()
t.increment(by: 11)

运行一下:

Snipaste_2022-07-08_17-08-12.png

如果注释掉 protocol 里面的 func increment(by: Int),运行一下:

Snipaste_2022-07-08_17-09-56.png

为啥出现这种情况?编译成sil文件看一下:

1、 protocol 里面的 func increment(by: Int)没注释掉的情况:走的是witness_table调用,最终会到CCTeacher的v_table中找increment方法。

Snipaste_2022-07-08_17-18-13.png

2、protocol 里面的 func increment(by: Int)注释掉了的情况:直接调用了协议扩展中的increment()的函数地址,因为extension声明的函数是默认派发的,也就是在编译时extension中的函数地址已经明确了。

Snipaste_2022-07-08_17-17-22.png

来个例子:

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 circle1:Circle = Circle.init(10.0)
print(class_getInstanceSize(Circle.self))   //16 + 8 = 24
print(MemoryLayout.size(ofValue: circle1))  //8


var circle2:Shape = Circle.init(10.0)
var circleType = type(of: circle2)
print(class_getInstanceSize(circleType as! AnyClass))  //16 + 8 = 24
print(MemoryLayout.size(ofValue: circle2))             //40

可以看出,静态类型不一致(var circle1:Circle 以及 var circle2:Shape),那么变量里面存的东西是不一样的。如果静态类型不一致,变量里到底存了什么?

1、先看静态类型一致的时候(var circle1:Circle

变量circle存的就是引用对象在堆空间的内存地址

Snipaste_2022-07-11_15-27-33.png

2、看静态类型不一致的时候(var circle1:Shape

Snipaste_2022-07-11_15-47-39.png

分析到这里,可以初步得出一个结构:

struct CTProtocolBox {
    var valueBuffer1: UnsafeRawPointer
    var valueBuffer2: UnsafeRawPointer
    var valueBuffer3: UnsafeRawPointer
    var metadata: UnsafeRawPointer
    var witness_table: UnsafeRawPointer
}

以下代码,编译成IR代码来看看

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 circle:Shape = Circle.init(10.0)

截取main函数的部分: Snipaste_2022-07-11_16-23-12.png

我们仔细看到这个部分: Snipaste_2022-07-11_17-05-11.png

我们全局查一下s4main6CircleCAA5ShapeAAWP是个什么:

这里面存储了两个值:一个是s4main6CircleCAA5ShapeAAMc,一个是s4main6CircleCAA5ShapeA2aDP4areaSdvgTW,可以通过在终端的xcrun swift-demangle命令来还原一下分别是什么 Snipaste_2022-07-11_17-07-10.png

xcrun swift-demangle s4main6CircleCAA5ShapeAAMc

protocol conformance descriptor for main.Circle : main.Shape in main

xcrun swift-demangle s4main6CircleCAA5ShapeA2aDP4areaSdvgTW

protocol witness for main.Shape.area.getter : Swift.Double in conformance main.Circle : main.Shape in main

可以看出来,第一个是一个叫做protocol conformance descriptor这么个类型的玩意儿,第二个是实现的协议方法 area的getter

到这里可以进一步补充协议的数据类型:

struct CTProtocolBox {
    var valueBuffer1: UnsafeRawPointer
    var valueBuffer2: UnsafeRawPointer
    var valueBuffer3: UnsafeRawPointer
    var metadata: UnsafeRawPointer
    var witness_table: UnsafeMutablePointer<TargetWitnessTable>
}

struct TargetWitnessTable{
    var protocol_conformance_descriptor: UnsafeRawPointer
    var witnessMethod: UnsafeRawPointer
}

那么protocol_conformance_descriptor这个玩意儿里面存了什么呢?查阅源码 swift-source -> Metadata.h -> 搜索 TargetWitnessTable

Snipaste_2022-07-11_17-51-10.png

分析后最终可得出这样协议的结构:

struct CTProtocolBox {
    var valueBuffer1: UnsafeRawPointer
    var valueBuffer2: UnsafeRawPointer
    var valueBuffer3: UnsafeRawPointer
    var metadata: UnsafeRawPointer
    var witness_table: UnsafeMutablePointer<TargetWitnessTable>
}

struct TargetWitnessTable{
    var protocol_conformance_descriptor: UnsafeMutablePointer<TargetProtocolConformanceDescriptor>
    var witnessMethod: UnsafeRawPointer
}

struct TargetProtocolConformanceDescriptor{
    var protocolDesc: TargetRelativeDirectPointer<TargetProtocolDescriptor>
    var typeRef: UnsafeRawPointer
    var WitnessTablePattern: UnsafeRawPointer
    var flags: UInt32
}

struct TargetProtocolDescriptor
{
    var flags: UInt32
    var parent: TargetRelativeDirectPointer<UnsafeRawPointer>
    var Name: TargetRelativeDirectPointer<CChar>
    var NumRequirementsInSignature: UInt32
    var NumRequirements: UInt32
    var AssociatedTypeNames: TargetRelativeDirectPointer<CChar>
}

struct TargetRelativeDirectPointer<Pointee>{
    var offset: Int32

    mutating func getmeasureRelativeOffset() -> UnsafeMutablePointer<Pointee>{
        let offset = self.offset

        return withUnsafePointer(to: &self) { p in
           return UnsafeMutablePointer(mutating: UnsafeRawPointer(p).advanced(by: numericCast(offset)).assumingMemoryBound(to: Pointee.self))
        }
    }
}

跑个例子看一下:

var circle: Shape = Circle.init(10.0)

withUnsafePointer(to: &circle){ptr in
    ptr.withMemoryRebound(to: LGProtocolBox.self, capacity: 1){ pointer in
        print(pointer.pointee)
        let descPtr = pointer.pointee.witness_table.pointee.protocol_conformance_descriptor.pointee.protocolDesc.getmeasureRelativeOffset()

        print(String(cString: descPtr.pointee.Name.getmeasureRelativeOffset()))
        print(pointer.pointee.witness_table.pointee.witnessMethod)
        print("end")
    }
}

Snipaste_2022-07-12_14-54-43.png

这个 0x0000000100004130 实现的协议方法可以通过mach-o文件来交叉验证:

命令行输入:nm -p mach-o | grep 0000000100004130

Snipaste_2022-07-12_15-15-33.png

总结: 每个遵循协议的class都有自己的 witnessTable,遵循多少个协议,就有多少个witnessTable,现协议里的函数越多,witnessTable里面存储的函数地址就越多。

1.5 Existential Container 存在容器

上面我们总结了这么个结构出来

struct CTProtocolBox {
    var valueBuffer1: UnsafeRawPointer
    var valueBuffer2: UnsafeRawPointer
    var valueBuffer3: UnsafeRawPointer
    var metadata: UnsafeRawPointer
    var witness_table: UnsafeMutablePointer<TargetWitnessTable>
}

这个玩意儿其实是编译器生成的一种特殊的数据类型,称为 Existential Container存在容器,用于管理遵守了相同协议的协议类型,因为这些类型的内存大小不一致,所以通过Existential Container存在容器进行统一管理。

说人话:

上面的例子中:如果不是class引用类型,而是struct值类型,或者内部不止只有var radious:Double这一个属性,有很多个,在 var circle:Shape = Circle.init(10.0) 中,编译器无法直接推断 circle变量 占用多大内存,也就是说无法为 circle 这个变量分配内存空间,它占用多大内存取决于它的动态类型,也就是 Circle.init(10.0);为了解决这个问题,那统一的,都放在一个40字节的Existential Container存在容器当中,可以理解为一个中间层。

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)
  • 1、对于小容量的数据,直接存储在 value Buffer中

我们把它的前24个字节,称为valueBuffer,是一个存放值得区域,一个连续的空间

struct CTProtocolBox {
    var valueBuffer1: UnsafeRawPointer
    var valueBuffer2: UnsafeRawPointer
    var valueBuffer3: UnsafeRawPointer
    var metadata: UnsafeRawPointer
    var witness_table: UnsafeMutablePointer<TargetWitnessTable>
}

例子:

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

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

var circle: Shape = Circle.init(10.0)

Snipaste_2022-07-13_15-03-19.png

对于Struct值类型来说,如果它的值能够在valueButter的24字节中放得下,那么就会依次存放在valueBuffer中。

  • 2、对于大容量的数据,通过堆区分配,存储堆空间的地址

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

struct Circle: Shape{
    var radious: Double
    var width: Double = 20
    var height: Double = 30
    var height1: Double = 30
    init(_ radious: Double) {
        self.radious = radious
    }
}

var circle: Shape = Circle.init(10.0)

Snipaste_2022-07-13_15-11-18.png

对于Struct值类型来说,如果它的值超过了valueButter的24字节,那么会把所有内容移到堆区存放,并且在valueBuffer中存储其对应的堆区地址。

2、写时复制

针对【存在容器存储堆空间地址 的情况】,在进行对象赋值后,新对象在修改的时候,会有写时复制的操作。

来个例子:

protocol Shape {
    var radious: Double {get set}
    var width: Double {get set}
    var height: Double {get set}
    var height1: Double {get set}
}

struct Circle: Shape{
    var radious: Double
    var width: Double = 20
    var height: Double = 30
    var height1: Double = 30

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

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

print("修改前")

circle1.width = 50.0

print("修改后")

circle赋值给circle1,并且修改circle1,在修改前,内存情况是这样的:

Snipaste_2022-07-15_15-37-11.png

修改了circle1之后,内存情况是这样的:

Snipaste_2022-07-15_16-03-38.png

也就是说circle1修改前和修改后的内存情况是不一样的,在进行修改的时候,会判断circle的引用计数是不是大于1,如果大于1,会进行写时复制