摘要
- 很多 Objective-C 开发者转 Swift 后,语法很快就能上手,但对 Swift 方法调用的理解往往还停留在
obj.foo()这一层。真正决定你是"会写 Swift",还是"理解 Swift"的分水岭,不在语法,而在方法派发模型。Swift 并不是只有一种"消息发送"机制,它同时存在直接派发、vtable派发、witness table派发,以及 Objective-C Runtime 消息派发。理解这四种机制,才能真正看懂 Swift 的性能、多态、协议扩展和 Runtime 边界。
正文
做了很多年 Objective-C,再转到 Swift,最容易出现一种"熟悉的错觉"。
你会觉得这门语言并不陌生。
一样有对象,一样有方法,一样可以写页面、网络、组件化,甚至在混编项目里还能继续和 NSObject、Selector、KVO 打交道。表面上看,只是把以前的写法换了一套更现代的语法糖。
但真正写一段时间 Swift 后,你会慢慢发现,很多问题已经不能再用 Objective-C 那套单一 Runtime 视角去解释了。
比如:
- 为什么
final经常被说成有性能收益? - 为什么协议扩展里的方法"看起来重写了",但调用结果却不符合预期?
- 为什么同样是多态,类继承和协议抽象的底层机制完全不一样?
- 为什么有些 Swift 方法能被 KVO、Swizzling 干预,有些却完全不行?
- 为什么中高级 Swift 面试几乎都会追问
vtable、witness table、dynamic?
这些问题表面不同,但底层其实都在指向同一个主题:Swift 方法到底是怎么被调用的。
如果用一句话概括这篇文章的核心结论,那就是:
对一个多年 Objective-C 开发者来说,转 Swift 最重要的认知升级,不是语法升级,而是从"单一消息发送思维"升级到"多派发模型思维"。
一、Objective-C 的经验,为什么在 Swift 里会失效一半
Objective-C 开发者对方法调用这件事,通常有一套非常统一的认知。
当你写:
objc
[obj doSomething];
你很自然就会想到背后那条经典链路:
- 对象持有
isa isa指向类对象- 类对象维护方法列表、缓存和父类链
- 调用本质是
objc_msgSend(receiver, selector)
这套模型很强,也很优雅,因为它非常动态。
它允许你做很多强 Runtime 能力的事情:
- Method Swizzling
- 动态添加方法
- 消息转发
respondsToSelector- KVC / KVO
- 动态替换实现
所以做久了 Objective-C,很容易形成一种潜意识:
方法调用,本质上就是消息发送。
这句话在 Objective-C 语境下没问题,但放到 Swift 里,只对了一部分。
因为 Swift 的目标从来不只是"更现代的 Objective-C"。
Swift 还要同时追求这些东西:
- 更强的类型安全
- 更强的编译期优化
- 更高的执行效率
- 更好的泛型能力
- 更弱的 Runtime 依赖
- 更好的跨平台能力
这就决定了 Swift 不可能把所有方法调用都默认交给 objc_msgSend。
Swift 的底层策略更像是:
- 编译期能确定的,就尽量在编译期确定
- 必须支持多态时,再引入表派发
- 必须依赖 Objective-C Runtime 时,才走消息派发
也就是说,Swift 不是没有"消息发送",而是它把"调用目标如何被解析"做了更细的分层。
二、先别急着背概念,先理解"派发"到底在解决什么问题
无论是 Swift 还是 Objective-C,方法调用底层都在解决一个核心问题:
这次调用,最终到底应该跳到哪段函数实现?
当你写下这行代码:
swift
obj.foo()
编译器和运行时真正关心的,不是点语法本身,而是这几个问题:
obj的真实类型是什么foo的实现到底在哪里- 能不能在编译期就把目标确定下来
- 如果不能,是不是要查某张表
- 如果需要动态能力,是不是要交给 Runtime
所以,"派发"这个词本质上就是:
解析方法实现地址的方式。
理解了这句话,再去看 Swift 的四种派发方式,就会非常清楚:
直接派发:编译期就知道地址vtable派发:类多态,运行时按虚表查地址witness table派发:协议多态,运行时按协议实现表查地址objc消息派发:运行时发消息,由 Runtime 决定最终实现
三、Swift 最重要的默认路径:直接派发
先看最简单的例子:
swift
struct Counter {
func increment() {
print("increment")
}
}
let counter = Counter()
counter.increment()
这段代码为什么快?
因为编译器几乎没有歧义。
Counter是值类型- 值类型没有继承
increment()不存在 override- 目标实现是确定的
既然目标实现在编译期就能确定,那编译器就完全没必要把这个调用拖到运行时再去解析。它可以直接生成对目标函数的调用,甚至继续做更多优化,比如内联。
再看一个类的例子:
swift
final class Dog {
func run() {
print("run")
}
}
let dog = Dog()
dog.run()
这里虽然是类,但因为 Dog 被 final 修饰,run() 不可能再被子类重写,调用目标依然稳定。
这也是为什么 Swift 里 final 的价值远不只是"不能继承"。
更准确地说,它是在告诉编译器:
- 这里不需要为多态保留额外动态性
- 你可以放心做更激进的优化
对一个从 Objective-C 转来的开发者来说,这是第一个非常重要的认知切换:
Swift 里的很多方法调用,根本不是"发消息",而是编译期直接定死。
四、类继承下的多态,靠的是 vtable,不是默认 objc_msgSend
来看经典多态:
swift
class Animal {
func speak() {
print("animal")
}
}
class Dog: Animal {
override func speak() {
print("dog")
}
}
let value: Animal = Dog()
value.speak()
这里左边变量类型是 Animal,但真实对象是 Dog。
如果编译器只按左边类型去决定调用目标,那结果就会错误地变成 Animal.speak()。
所以这里必须支持运行时多态。
Swift 在这类原生类多态场景下,通常依赖的是 vtable,也就是虚函数表。
你可以把它理解成:
类元数据上的一张方法槽位表。
调用过程可以粗略想象成这样:
- 拿到对象实例
- 找到它的真实类型元数据
- 找到对应类的
vtable - 按方法槽位拿到最终实现地址
- 再完成调用
子类一旦 override 某个方法,本质上就是把对应槽位替换成自己的实现。
所以 vtable 派发的核心价值是:
- 支持类继承体系里的动态多态
- 比 Objective-C 完整消息查找链路更偏静态
- 性能通常比全面 Runtime 消息派发更可控
这个点很关键,因为很多人会把 Swift 类方法的动态调用直接等同于 objc_msgSend。
其实不是。Swift 原生类模型有自己的一套多态分发机制。
五、真正拉开 Swift 和 Objective-C 差距的,是 witness table
如果说 vtable 是类多态的底层支撑,那 witness table 就是协议多态的底层支撑。
先看代码:
swift
protocol Runner {
func run()
}
struct Person: Runner {
func run() {
print("person run")
}
}
func start(_ runner: Runner) {
runner.run()
}
这里 start(_:) 只知道参数符合 Runner 协议,但并不知道它到底是:
PersonRobotCat- 还是别的实现类型
也就是说,编译器知道一定存在一个 run(),但它不知道最终应该调用哪个具体实现。
怎么办?
Swift 的做法是:
除了值本身,还额外携带一份"这个类型如何满足该协议"的映射信息。
这份映射信息,就是 witness table。
你可以把 witness table 理解成:
某个具体类型对某个协议的实现清单。
它记录了:
- 这个类型确实遵守该协议
- 协议要求的方法,对应到这个类型上的哪个函数实现
所以协议调用的底层逻辑不是"沿着类继承树去查",而是:
- 拿到具体类型信息
- 拿到该类型针对协议的
witness table - 从表里找到目标方法实现
- 再去调用
这就是 Swift 面向协议编程真正成立的基础。
很多开发者会说,Swift 更推崇协议而不是继承。
这句话如果没有底层理解,很容易流于口号。
只有真正理解了 witness table,你才知道:
Swift 不是"语法上鼓励协议",而是底层真的为协议多态提供了一套完整的运行机制。
六、协议扩展为什么是 Swift 最容易踩坑的地方
看下面这段代码:
swift
protocol P {
func foo()
}
extension P {
func foo() {
print("P.foo")
}
func bar() {
print("P.bar")
}
}
struct S: P {
func foo() {
print("S.foo")
}
func bar() {
print("S.bar")
}
}
let p: P = S()
p.foo()
p.bar()
很多人第一次看到结果都会愣一下:
p.foo()输出S.foop.bar()却输出P.bar
为什么?
因为:
foo()是协议要求的方法bar()不是协议要求的方法,它只是协议扩展新增的方法
这两者的派发语义不一样。
foo() 作为协议要求,会通过 witness table 去找到具体类型 S 的实现。
而 bar() 不属于协议要求,对协议类型变量来说,很多时候是静态绑定到扩展实现,而不是动态转发到 S.bar()。
这个细节非常能体现 Swift 和 Objective-C 思维的差异。
Objective-C 开发者更容易默认认为:
只要同名实现存在,运行时应该就能"找到更具体那个"。
但 Swift 不是这么设计的。
Swift 更强调"协议要求"和"扩展附加实现"的边界。
也正因为如此,协议扩展成为了 Swift 最高频、也最值得深挖的派发面试题之一。
七、Swift 什么时候才真正回到 Objective-C 的消息发送世界
Swift 并不是抛弃了 Runtime,而是把它从默认主路径降级成了一种按需启用的能力。
比如下面这段代码:
swift
import Foundation
class Student: NSObject {
@objc dynamic func study() {
print("study")
}
}
这里的 @objc dynamic 其实是在告诉编译器:
- 这个方法要暴露给 Objective-C Runtime
- 不要再按 Swift 更静态的方式优化
- 这里要保留 Objective-C 风格的动态派发能力
进入这条路径之后,底层逻辑就回到我们熟悉的那套:
receiverselectorobjc_msgSend- cache 查找
- method list 查找
- 父类链查找
- 消息转发
所以在 Swift 里,objc 消息派发常见于这些场景:
@objcdynamicNSObject继承体系- KVC / KVO
- Target-Action
- Method Swizzling
- Objective-C Runtime 桥接能力
这时候,一个从 Objective-C 转过来的人会突然"感觉很熟"。
但你要注意一个边界:
这已经不是 Swift 的默认路径,而是 Swift 为了兼容 Runtime 动态能力而打开的一条特殊通道。
八、为什么这件事不只是面试题,而是架构能力分水岭
很多人第一次接触这套知识,是因为面试官问:
- Swift 方法调用有哪些方式?
final为什么有性能收益?dynamic做了什么?- 协议扩展为什么有坑?
但如果你只把这些当作"八股",就低估它的工程价值了。
理解方法派发,至少会直接影响五件事:
第一,影响你如何做性能优化。
你会知道哪些地方适合用 final,哪些调用更容易被编译器去虚拟化,哪些抽象层会引入额外分发成本。
第二,影响你如何做架构设计。
你会更清楚什么时候用继承,什么时候用协议,什么时候需要 Runtime 动态能力,什么时候应该避免过度动态化。
第三,影响你如何定位问题。
很多"为什么没走到子类实现""为什么协议扩展输出不对""为什么 KVO 不生效"的问题,底层本质都是派发规则没理解清楚。
第四,影响你如何做混编。
在 Objective-C 和 Swift 共存的工程里,如果你不知道一个 Swift 方法到底是不是走 Objective-C Runtime,就很容易在桥接、反射、动态替换这些场景里做错误设计。
第五,影响你如何表达自己的技术深度。
初级开发者会说"Swift 有实例方法和类方法"。
中级开发者会说"Swift 有直接派发和动态派发"。
真正有深度的表达,会把四种派发方式和各自解决的问题讲清楚。
九、从多年 OC 开发者转 Swift,我建议优先补这四个认知
如果你正在从 Objective-C 体系迁移到 Swift,我建议优先补齐这四件事。
第一,改掉"方法调用 = objc_msgSend"的单一路径认知。
Swift 不是只有一种调用模型。
第二,把四种派发方式在脑子里彻底分开。
不要混着记。
直接派发:编译期确定目标vtable:类继承多态witness table:协议多态objc 消息派发:Runtime 动态能力
第三,重点理解协议扩展。
因为它最容易暴露你到底有没有真正理解 Swift。
第四,学会把派发机制映射到工程判断里。
比如:
- 这里为什么要加
final - 这里为什么适合协议抽象
- 这里为什么必须
@objc dynamic - 这里为什么不能指望 Runtime 接管
真正的掌握,从来不是"记住名词",而是你能不能把这些机制变成日常决策能力。
十、结尾:Swift 的难点,从来不是语法,而是运行模型
对多年 Objective-C 开发者来说,Swift 最容易上手,也最容易误判。
因为语法层确实不难。
optional、enum、protocol、extension,花一段时间都能写得很熟。
但如果你没有把方法调用背后的派发模型建立起来,你对 Swift 的理解就始终停留在"会写"的阶段,而不是"看懂了它为什么这样运行"。
Objective-C 的魅力,来自统一而强大的 Runtime。
Swift 的魅力,则来自它在性能、类型安全和动态能力之间做出的精细分层。
所以,从 Objective-C 走到 Swift,真正重要的不是把旧语法翻译成新语法,而是完成这次底层思维切换:
从"统一消息发送思维",切换到"静态优化优先、动态能力按需开启的多派发模型思维"。
当你真正理解了 直接派发、vtable、witness table 和 objc_msgSend 的边界时,你写的就不再只是能跑的 Swift,而是你真正理解了 Swift。
图 1:Swift 方法派发总览

图 2:类继承下的 vtable
图 3:协议调用下的 witness table
图 4:Objective-C 消息派发链路
