Swiftswift

Swift 协议介绍

2022-01-28  本文已影响0人  晨曦的简书

一、协议与继承

class CXTeacher {
    var age = 10
    var name = "chenxi"
}

class Dog {
    var name = "糯米"
    var type = "泰迪"
}

例如如上代码,这个时候我们有一个需求,要为这两个类添加一个 debug 函数来打印当前 类的信息。从继承的⻆度来说,我们可能会想到抽取一个公共的基类,当然这里两个类都是动物,人也是动物。但是从业务逻辑上来说,这么处理不太合理。所以最直观的办法是对于每一个类都写一个单独的 debug 函数。

class CXTeacher {
    var age = 10
    var name = "chenxi"
    
    func debug(){
        print("")
    }
}

class Dog {
    var name = "糯米"
    var type = "泰迪"
    
    func debug(){
        print("")
    }
}

如果我们对当前代码中的每个类都需要 debug,那上面这种方法显然是行不通的,于是我们有 了下面的代码:

func debug(subject: Any){
    print("")
}

看到这里可能大家也会觉得没有问题,但是如果我们要具体的描述当前类的具体信息,这个时候 我们还需要引入一个公共的基类,同时我们还需要有一个公共的属性 description 来让子类重 载,这无疑对我们的代码是很强的入侵。

所以这个时候我们通过一个协议来描述当前类的共同行为,并通过 extension 的方式来对我们的类进行扩展,这样来处理的话就会好很多。

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

func print(subject: CustomStringConvertible) {
    let string = subject.description
    print(string)
}

这里我们可以稍微的总结一下:

  • class 本质上定义了一个对象是什么
  • protocol 本质上定义了一个对象有哪些行为

二、协议的基本语法

protocol MyProtocol {
    var age: Int{ get set } 
    var name: String{ get }
}

这里需要注意的一点是:并不是说当前声明 get 的属性一定是计算属性

class LGTeacher: MyProtocol{ 
    var age: Int = 18
    var name: String
    init(_ name: String) {
        self.name = name
    }
}
protocol Togglable {
    mutating func toggle()
}

class CXTeacher: Togglable {
    func toggle() {
        print(#function)
    }
}

struct CXPerson: Togglable {
    mutating func toggle() {
        print(#function)
    }
}
protocol MyProtocol {
    init(_ age: Int)
}

class CXPerson: MyProtocol {
    var age = 10
    required init(_ age: Int) {
        self.age = age
    }
}

这里有一种特殊情况,如果 CXPerson 是不可被继承的,那么不加 required 修饰也没问题。

protocol MyProtocol {
    init(_ age: Int)
}

final class CXPerson: MyProtocol {
    var age = 10
    init(_ age: Int) {
        self.age = age
    }
}
@objc protocol Incrementable {
     @objc optional func increment(by: Int) -> Int
}

class CXPerson: Incrementable {

}

let p: Incrementable = CXPerson()
//这里CXPerson没实现increment方法这样调用的话也不会出错
p.increment?(by: 10)

三、协议原理探究

SIL 代码分析协议调度方式

在前面的文章中我们已经了解到类的方法调用是通过 V-table(函数表)来调度的,那么让类来实现协议中的方法的时候调度方式是怎么样的呢?这里我们将 swift 代码编译 成 SIL 代码来看一下。

protocol Incrementable {
    func increment(by: Int)
}

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

let p: CXPerson = CXPerson()
p.increment(by: 10)

第一步我们先找到 main 函数,在这里可以看到 increment 函数是通过 class_method 方式进行调度的,下面我们在官方文档看一下对 class_method 的解释。

通过文档介绍可以知道 class_method 是通过 v-table 的方式进行调度的。

这里我们搜索 s4main8CXPersonC9increment2byySi_tF

这里可以看到 increment 被声明在了 v-table 中。下面我们对 swift 代码做下修改,将 p 声明成 Incrementable 类型再来看一下调度方式是否一样。

protocol Incrementable {
    func increment(by: Int)
}

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

let p: Incrementable = CXPerson()
p.increment(by: 10)

这里在 main 函数中可以看到 increment 函数的调度变成了 _method,所以我们先在官方文档看一下 witness_method 的介绍。

文档的大致意思是讲会通过查到当前类的 witness-table 来找到当前方法的实现。这里 witness-table 叫作协议见证表,当一个类准了一个协议并且实现了协议中的方法,编译器就会为当前类创业一个 witness-tablewitness-table 就记录了当前类实现协议方法的编码信息。

SIL 代码中我们确实看到了 sil_witness_table,这里我们通过搜索 s4main8CXPersonCAA13IncrementableA2aDP9increment2byySi_tFTW 来看一下协议方法是如何调度的。

这里会通过 s4main8CXPersonCAA13IncrementableA2aDP9increment2byySi_tFTW 来查找具体的方法类型,也就是我们在 CXPerson 类中实现 increment 方法 。也就是说当变量被声明成协议类型的时候,编译器会通过 witness-table 做一层桥接, 最终找到变量的具体实际类型及具体实现,并完成方法的调度。

通过汇编代码分析协议方法的调度

protocol Incrementable {
    func increment(by: Int)
}

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

class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        
        let p: Incrementable = CXPerson()
        p.increment(by: 10)
    }
}

这里可以看到 x2 存储的就是 witness-tablex2 偏移 0x8 得到 x8,也就是 increment 函数地址。在 blr x8 断点这里操作 fn + control + F7 快捷键,也就是指令单步执行,进入到 increment 函数的具体实现。

这里可以看到,有经过了一层跳转,通过 x8 偏移 0x50,这个时候才是 increment 函数的真正执行地址。通过 fn + control + F7 快捷键也可以看到执行了 CXPersonincrement 方法。

上一篇下一篇

猜你喜欢

热点阅读