理解 Swift 中的方法派发机制 - 动态派发

动态派发也可以称之为表派发或者运行时派发,动态派发的关键定义:派发的函数是在运行时动态选择的。表分派从名字来看并不那么明显,但通过指针表这个实现细节可以看出:表派发也是基于指针表来动态派发的。 Swift 实际上实现了两种类型的表派发:用于类层次结构的虚拟表(virtual tables)和用于协议的见证表(witness tables)。

虚表派发

代码示例如下:

swift 复制代码
class Cat { 
    func cry() { 
        print("Meow")
    }

    func eat() {
        print("Purr")
    }
}

final class Lion: Cat {
    override func cry() { 
        print("Roar")
    }
}

由于我们的 .swift 文件是独立编译的,Swift 编译器无法确定在哪里使用哪个实现,这些信息仅在运行时可用。 所以在对象实际创建之前,我们不知道我们正在处理的是 Cat 还是 Lion。

虚拟表其实也没有什么神奇的地方。它只是一个在每个子类的编译时创建的列表,它将每个函数映射到它们在内存中的实现。如果 Lion 重写了 cry(),则该表指向Lion.cry() 上定义的指令。如果它不重写 eat(),那么 Lion 的虚拟表将指向 Cat 父类中定义的指令。

scss 复制代码
// Cat 的虚表
| 函数名 |  指针地址  |  
|-------|-----------|
| cry() | 0x1000    |
| eat() | 0x1008    |


// Lion 的虚表
| 函数名 | 指针地址 |
|-------|-------- |
| cry() | 0x2000 [重写] |
| eat() | 0x1008 [继承]|

表派发比直接派发更慢。因为在运行时,如果要动态分派到函数,Swift 进行以下几个步骤:

  • 跳转到子类类型元数据中存储的虚拟表。
  • 选出正确的函数指针。
  • 再次跳转到函数的内存地址。

协议的见证表

协议可以让开发者通过组合的形式为目标类型添加多态性,甚至可以为结构或枚举等值类型添加多态性。协议方法通过协议见证表派发。

它们的机制与虚拟表相同:符合协议的类型包含元数据(存储在 Existential containers 中),其中包括指向其见证表的指针,该见证表本身就是一个函数指针表。

当在协议类型上执行函数时,Swift 检查 Existential containers,查找见证表,并派发到要执行的函数的内存地址。

通常,当我们使用依赖项注入时,我们只指定协议我们的依赖项符合的接口而没有具体类型。其他时候,我们可能有一个 Collection,其中包含我们想要迭代的各种符合协议的对象。在这些情况下,方法派发是通过见证表进行的。

虚表和见证表的 SIL

如前所述,在 Swift 中调用动态派发有两种主要方法。首先,让我们看一下虚表派发:

swift 复制代码
class Incrementer {
    func increment(_ int: Int) -> Int {
        return int + 1
    }

    func deincrement(_ int: Int) -> Int {
        return int - 1
    }
}

class DoubleIncrementer: Incrementer { 
    override func increment(_ int: Int) -> Int {
        return int + 2
    }
}

let threePlusTwo = DoubleIncrementer().increment(3)

我们可以看到在 SIL 中创建的虚表(vtable)。DoubleIncrementer 实现了这两个方法,但只覆盖了一个指针,指向自己的实现:

rust 复制代码
sil_vtable Incrementer {
  #Incrementer.increment: (Incrementer) -> (Int) -> Int : @$s4main11IncrementerC9incrementyS2iF // Incrementer.increment(_:)
  #Incrementer.deincrement: (Incrementer) -> (Int) -> Int : @$s4main11IncrementerC11deincrementyS2iF // Incrementer.deincrement(_:)
}

sil_vtable DoubleIncrementer {
  #Incrementer.increment: (Incrementer) -> (Int) -> Int : @$s4main17DoubleIncrementerC9incrementyS2iF [override] // DoubleIncrementer.increment(_:)
  #Incrementer.deincrement: (Incrementer) -> (Int) -> Int : @$s4main11IncrementerC11deincrementyS2iF [inherited] // Incrementer.deincrement(_:)
}

让我们看看当我们使用协议时它是什么样子的:

swift 复制代码
protocol Incrementer {
    func increment(_ int: Int) -> Int
}

struct IncrementerImpl: Incrementer {
    func increment(_ int: Int) -> Int {
        return int + 1
    }
}

let fourPlusOne = IncrementerImpl().increment(4)

这一次,我们在 SIL 中看到一个见证表(sil_witness_table):

rust 复制代码
sil_witness_table hidden IncrementerImpl: Incrementer module main {
  method #Incrementer.increment: <Self where Self : Incrementer> (Self) -> (Int) -> Int : @$s4main15IncrementerImplVAA0B0A2aDP9incrementyS2iFTW // protocol witness for Incrementer.increment(_:) in conformance IncrementerImpl
}

在这两个非常简单的示例中,编译器实际上最终将 main() 函数静态地直接派发给increment()的方法实现,而绕过分派表。如果你进行了优化编译,Swift 会完全放弃这些函数,并在调用时产生预先计算好的结果!

相关推荐
游戏开发爱好者84 小时前
带签名时间戳接口的重放与压力测试实战,用动态值在发送前现算签名
网络协议·计算机网络·网络安全·ios·adb·https·压力测试
2501_9160074717 小时前
申请 iOS 推送证书并配置 APNs 群发推送教程
android·ios·小程序·https·uni-app·iphone·webview
weixin_4038101318 小时前
iOS自动化脚本怎么避免风控?无屏幕镜像截图原理详解
ios·自动化·跨境电商·苹果群控·苹果投屏·多媒体运营
wangruofeng19 小时前
3 步把 MacBook 变成 7×24 远程主机,人在外面随时接管桌面
ios·agent·mac
weixin_4038101319 小时前
iOS免越狱自动化蓝牙HID方案:不装代理也能跑脚本
运维·ios·自动化·跨境电商·ai智能体·ios投屏·苹果投屏
GitLqr1 天前
Flutter 3.47 与 Dart 3.13:你必须了解的迁移指南
flutter·ios·dart
冯汉栩1 天前
Swift Control View,Label渐变颜色(源码)
开发语言·ios·swift
jike_20262 天前
iPhone日历查看会议纪要APP推荐:按日期查找历史录音更方便
ios·语音识别·iphone
做前端的娜娜子2 天前
文本切片与召回(Chunk、Overlap 到混合检索)学习笔记
langchain·openai·掘金·金石计划
做前端的娜娜子2 天前
#浏览器存储方案:localStorage、sessionStorage 与 Cookie
前端·面试·掘金·金石计划