理解 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 会完全放弃这些函数,并在调用时产生预先计算好的结果!

相关推荐
Zender Han1 小时前
VS Code 开发 Flutter 常用快捷键和插件工具详解
android·vscode·flutter·ios
库奇噜啦呼2 小时前
【iOS】 Blocks
macos·ios·cocoa
报错小能手2 小时前
ios开发方向——swift并发进阶核心 Task、Actor、await 详解
开发语言·学习·ios·swift
光影少年3 小时前
开发RN项目时,如何调试iOS真机、Android真机?常见调试问题排查?
android·前端·react native·react.js·ios
疯狂的程序猴21 小时前
Flutter应用代码混淆完整指南:Android与iOS平台配置详解
后端·ios
SY.ZHOU21 小时前
移动端架构体系(五):终篇总结
flutter·ios·系统架构·安卓·鸿蒙
Digitally1 天前
如何不用 iTunes 将 iPhone 备份到移动硬盘?
ios·iphone
sysinside1 天前
Cisco Catalyst 9000 IOS XE 26.1.1 GA - 思科 Catalyst 9000 交换产品系列 IOS XE 系统软件
ios·cisco
低保和光头哪个先来1 天前
解决 ios 使用 video 全屏未铺满页面问题
前端·javascript·vue.js·ios·前端框架
报错小能手1 天前
SwiftUI 框架 认识 SwiftUI 视图结构 + 布局
ui·ios·swift