Swift 协议介绍
一、协议与继承
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
本质上定义了一个对象有哪些行为
二、协议的基本语法
- 协议要求一个属性必须明确是 get 或 get 和 set
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
}
}
- 协议中的异变方法,表示在该方法可以改变其所属的实例,以及该实例的所有属性(用于枚 举和结构体),在为类实现该方法的时候不需要写 mutating 关键字
protocol Togglable {
mutating func toggle()
}
class CXTeacher: Togglable {
func toggle() {
print(#function)
}
}
struct CXPerson: Togglable {
mutating func toggle() {
print(#function)
}
}
- 类在实现协议中的初始化器,必须使用 required 关键字修饰初始化器的实现(类的初始化器前添加 required 修饰符来表明所有该类的子类都必须实现该初始化器)
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
}
}
- 类专用协议(通过添加 AnyObject 关键字到协议的继承列表,你就可以限制协议只能被类 类型采纳)
- 可选协议:如果我们不想强制让遵循协议的类类型实现,可以使用 optional 作为前缀 放在协议的定义。
@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
代码来看一下。
-
swift
代码
protocol Incrementable {
func increment(by: Int)
}
class CXPerson: Incrementable {
func increment(by: Int) {
print(by)
}
}
let p: CXPerson = CXPerson()
p.increment(by: 10)
-
SIL
代码分析
第一步我们先找到 main
函数,在这里可以看到 increment
函数是通过 class_method
方式进行调度的,下面我们在官方文档看一下对 class_method
的解释。
通过文档介绍可以知道 class_method
是通过 v-table
的方式进行调度的。
这里我们搜索 s4main8CXPersonC9increment2byySi_tF
。
这里可以看到 increment
被声明在了 v-table
中。下面我们对 swift
代码做下修改,将 p
声明成 Incrementable
类型再来看一下调度方式是否一样。
-
swift
代码
protocol Incrementable {
func increment(by: Int)
}
class CXPerson: Incrementable {
func increment(by: Int) {
print(by)
}
}
let p: Incrementable = CXPerson()
p.increment(by: 10)
-
SIL
代码分析
这里在 main
函数中可以看到 increment
函数的调度变成了 _method
,所以我们先在官方文档看一下 witness_method
的介绍。
文档的大致意思是讲会通过查到当前类的 witness-table
来找到当前方法的实现。这里 witness-table
叫作协议见证表,当一个类准了一个协议并且实现了协议中的方法,编译器就会为当前类创业一个 witness-table
, witness-table
就记录了当前类实现协议方法的编码信息。
在 SIL
代码中我们确实看到了 sil_witness_table
,这里我们通过搜索 s4main8CXPersonCAA13IncrementableA2aDP9increment2byySi_tFTW
来看一下协议方法是如何调度的。
这里会通过 s4main8CXPersonCAA13IncrementableA2aDP9increment2byySi_tFTW
来查找具体的方法类型,也就是我们在 CXPerson
类中实现 increment
方法 。也就是说当变量被声明成协议类型的时候,编译器会通过 witness-table
做一层桥接, 最终找到变量的具体实际类型及具体实现,并完成方法的调度。
通过汇编代码分析协议方法的调度
-
swift
代码
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-table
,x2
偏移 0x8
得到 x8
,也就是 increment
函数地址。在 blr x8
断点这里操作 fn + control + F7
快捷键,也就是指令单步执行,进入到 increment
函数的具体实现。
这里可以看到,有经过了一层跳转,通过 x8
偏移 0x50
,这个时候才是 increment
函数的真正执行地址。通过 fn + control + F7
快捷键也可以看到执行了 CXPerson
的 increment
方法。