你可能听过“给类加上final就会变快”。这听起来像都市传说,但背后确实有精确的机制。
本文接续上一篇文章 Swift进阶 #4。
看似理所当然的方法调用,实际上会以三种不同方式编译。采用哪种方式,会影响性能和优化可能性。
进阶系列本篇讨论分派。
调用的三张脸——直达、表格与消息
像 object.doSomething() 这样的代码,翻译成机器码时有三种方式。
静态分派。 编译时就能确定函数,因此直接跳转到函数地址调用。
它最快;更重要的是,编译器可以内联,移除调用并嵌入函数体,从而打开后续优化的空间。struct的方法、全局函数和final方法都属于此类。
表格分派。 支持继承和重写的类方法采用这种方式。
即使变量的编译时类型是Animal,实际实例也可能是Dog。要调用哪个实现,只有运行时才能知道。
因此每个类都有虚函数表(vtable)。调用时会从实例的表中找到函数索引并跳转。多态性的代价是一次间接引用。
协议也有一个名为witness table的近亲,作用相同。这就是讲some和any时看到的表。
消息分派。 这是Objective-C运行时的方式,通过objc_msgSend进行基于名称的查找。
三者中它最慢,但换来了极高的灵活性,甚至可以在运行时替换方法。在Swift中,标记为@objc dynamic的方法会进入这个世界。
objc_msgSend的内部机制已在另一篇文章中介绍,这里先不展开。
归根结底,这是灵活性与速度的交换。越依赖运行时决定就越慢;越能在编译时固定,就越快,也越容易优化。
final和private为何是性能关键字
现在可以拆解这个都市传说了。编译器在什么条件下能通过去虚拟化,将表格分派静态降级?
它必须证明“这个方法不可能被重写”。
final正是这项证明。final class和final func声明“子类不会重新定义”。
编译器可以移除vtable查询,改为直接调用,同时打开内联的大门(Swift Optimization Tips)。
private也有类似效果。文件外不可见的声明,让编译器能够看到文件内所有使用位置,并自行证明不存在重写。
启用整模块优化(WMO,如今发布构建的默认设置)后,证明范围会扩展到整个模块。没有子类的internal类也会自动按final处理。
实践规则很简单:不打算继承的类就加上final。
这不仅是设计声明(该类型是继承树的叶节点),对编译器来说也是一份优化证明。
但不要期待“加了final,App就变快了”。调用开销只有纳秒级,除非在热点循环中,否则几乎感觉不到。
真正有价值的是内联后连锁发生的优化,而这些由编译器自动完成。我们要做的只是不要阻碍这项证明。
协议extension的陷阱——是需求还是不是?
分派知识在实践中最能发挥作用的地方(准确地说,是不了解时最容易踩坑的地方)就是协议默认实现。先看一道小测验。
protocol Greeter {
func hello() // 声明为需求
}
extension Greeter {
func hello() { print("你好") }
func bye() { print("再见") } // 不是需求
}
struct Korean: Greeter {
func hello() { print("嗨") }
func bye() { print("拜拜") }
}
let k: any Greeter = Korean()
k.hello() // ?
k.bye() // ?
答案是“你好”和“再见”。hello是协议需求,因此通过witness table进行动态分派,Korean的实现已注册到表中。
而bye是需求列表中没有的extension专用方法。通过协议类型调用时,会静态直接调用extension实现,不管Korean定义了什么。
不了解这条规则,就会出现“明明实现了,代码却没被调用”的神秘Bug。解决方法很简单。
凡是采用者必须能够替换的方法,都要在协议主体中声明为需求。extension中的默认实现可以保留,但主体声明才能在表中创建位置。
在POP(面向协议编程)一文中,我们将协议extension介绍为继承的替代方案。这就是使用该工具的安全规则。
通过测量确认——不要凭感觉,要用性能分析器
分派话题的结尾总该有同一个警告:这是微优化领域,而且顺序很重要。
先用Instruments的Time Profiler找出真正的瓶颈。大多数性能问题并非来自分派,而是算法(O(n²)循环)、不必要的工作(每帧重新计算)或I/O。
关于警惕过早优化,正如Knuth那篇文章所说。
如果性能分析确实捕捉到热点循环中的动态分派,这时才可以处理:将类型设为final、用some或泛型替换any以促成特化,或把协议边界移到循环外。
换句话说,这项知识平时的用途是理解设计,而不是优化。
为什么struct是默认选择(适合静态分派),为什么推荐some而不是any(可以特化),以及为什么SwiftUI使用struct视图。
语言的重要决策都扎根于这一层;了解分派后,就能把Swift的设计读成一幅完整图景。
总结
- 方法调用会编译为静态(直接)、表格(vtable/witness table)或消息(objc_msgSend)分派,灵活性与速度成反比。
- final、private和WMO证明“没有重写”,将动态调用降为静态调用,并打开内联的大门。不打算继承的类,final是基本功。
- 协议extension方法是否动态分派,取决于是否声明为需求。必须可替换的方法,一定要声明在协议主体中。
- 顺序是先用性能分析器。分派知识的日常价值不是优化技巧,而是理解语言设计。
下一篇再深入一层:讨论内存布局,包括struct大小如何确定、属性顺序为何会改变内存,以及any箱子的实际大小。
延伸阅读
来源与验证
- Swift Optimization TipsSwift 프로젝트 · 官方文档 · 核查 2026年8月17日依据: 静态与动态分派、final、whole-module optimization及性能特征

![[Swift进阶 #5] Swift分派三兄弟:final为何能提升性能 封面图](/assets/images/posts/f974f02c-833c-4ab1-9350-a2e3543e8391/swift-method-dispatch-1.jpg)