Swift 静态分派与动态分派:方法到底是如何被调用的

本文基于 Sagar Unagar 的《Static vs Dynamic Dispatch in Swift - How Method Dispatch Really Works》(2026-02-21)学习整理,原文链接见文末。

方法分派(Method Dispatch)是 Swift 里一个偏底层、却默默影响着性能、架构与 API 设计的概念。大多数 App 即使从不去想"分派"也能跑得好好的,但理解 Swift 究竟如何决定"调用哪一个方法实现",能帮你写出更可预测、更高效、更易维护的代码------尤其是在构建框架、可复用组件,或是性能敏感的功能时。

本文会讲清楚:

  • 分派在 Swift 中到底指什么
  • 静态分派动态分派的区别
  • 结构体、类、协议、泛型分别是如何影响分派的
  • Swift 依据什么规则选择分派方式
  • 面向分派机制设计 API 的实用准则

目标不是让你过早优化,而是建立起"Swift 究竟如何执行你的代码"的正确心智模型。


一、什么是 Swift 中的"分派"

在 Swift 里,分派(dispatch)指的是:当调用一个方法时,编译器与运行时如何确定到底该执行哪一个函数实现

看这个例子:

swift 复制代码
// 定义一个 Greeter 协议,要求实现 greet() 方法
protocol Greeter {
    func greet() -> String
}

// 英文问候者,遵循 Greeter
struct EnglishGreeter: Greeter {
    func greet() -> String {
        "Hello"
    }
}

// 用协议类型来接收具体实例(存在类型,类型信息被擦除)
let greeter: Greeter = EnglishGreeter()
print(greeter.greet())   // 输出:Hello

在调用点 greeter.greet() 处,Swift 必须决定运行哪一个具体的 greet() 实现。这个决定有两种产生方式:

  • 编译期就定好 → 静态分派(Static dispatch)
  • 运行期再决定 → 动态分派(Dynamic dispatch)

理解这两种时机分别什么时候发生,是理解 Swift 性能模型与抽象模型的关键。


二、静态分派(Static Dispatch)

静态分派 意味着:编译器在编译期就已经确切知道要调用的方法实现,并直接生成对该函数的调用指令,运行期没有任何查找过程

1. 值类型上的静态分派

swift 复制代码
// 结构体是值类型,无法被继承
struct Counter {
    // 方法没有 override 的可能,编译器确切知道调用目标
    func increment(_ value: Int) -> Int {
        value + 1
    }
}

let counter = Counter()
print(counter.increment(10))   // 输出:11

因为 Counter 是值类型且不可被继承,Swift 确切知道该调用哪个 increment(_:)。编译器可以激进地 内联(inline) 并优化这次调用。这也是 Swift 推荐用 struct / enum 来建模数据与行为的重要原因之一。

2. final 类上的静态分派

swift 复制代码
// final 类:明确禁止被继承
final class Logger {
    func log(_ message: String) {
        print(message)
    }
}

let logger = Logger()
logger.log("Hello")   // 输出:Hello

将类标记为 final,等于告诉编译器这个方法不可能被重写。即便它是类方法,Swift 也能因此改用静态分派。

3. 静态分派的好处

静态分派让编译器能够:

  • 内联(inline) 方法调用,消除函数调用开销
  • 去除抽象层开销(如协议/泛型的间接层)
  • 执行跨函数优化(过程间优化)
  • 减小二进制体积
  • 在热点路径(hot path)上提升性能

在很多情况下,经过优化后这次调用甚至会完全消失(被直接替换为常量或内联代码)。


三、动态分派(Dynamic Dispatch)

动态分派 意味着:方法实现要等到运行期 ,根据实例的真实类型才能确定。当 Swift 在编译期无法知道具体实现时,就必须用到它。动态分派带来了多态能力,但也引入了运行期开销。

1. 类继承中的动态分派

swift 复制代码
class Animal {
    func speak() -> String {
        "..."   // 默认实现
    }
}

class Dog: Animal {
    override func speak() -> String {
        "Woof"
    }
}

// 静态类型是 Animal,但运行期实际类型是 Dog
let animal: Animal = Dog()
print(animal.speak())   // 输出:Woof

这里:

  • 静态类型(编译期类型)Animal
  • 运行期类型(动态类型)Dog

Swift 必须在运行期判断出正确的方法实现。它底层使用的是 Swift 的类分派机制(虚函数表 / v-table)

2. 协议存在类型(Protocol Existentials)上的动态分派

swift 复制代码
protocol Storage {
    func save()
}

struct DiskStorage: Storage {
    func save() {
        print("Saved to disk")
    }
}

// 协议作为类型使用,具体类型信息被"擦除"
let storage: Storage = DiskStorage()
storage.save()   // 输出:Saved to disk

当协议被当作类型使用时,Swift 会擦除(erase)具体的类型信息,方法调用要在运行期通过一次查找机制来解析。这种灵活性让你能写出面向抽象的代par码,但会付出一点运行期代价。


四、协议、泛型与分派

协议在 Swift 里既可能走静态分派,也可能走动态分派------取决于你怎么用它

1. 协议作为泛型约束 → 静态分派

swift 复制代码
protocol Greeter {
    func greet() -> String
}

struct EnglishGreeter: Greeter {
    func greet() -> String { "Hello" }
}

// 泛型函数:G 受 Greeter 约束,但具体类型在编译期已知
func greet<G: Greeter>(_ greeter: G) {
    print(greeter.greet())
}

greet(EnglishGreeter())   // 输出:Hello

因为编译器会为每一个具体类型特化(specialize)这个函数,方法调用是静态分派的。这让编译器可以激进内联与优化。

2. 协议作为类型(any) → 动态分派

swift 复制代码
protocol Greeter {
    func greet() -> String
}

struct EnglishGreeter: Greeter {
    func greet() -> String { "Hello" }
}

// 使用 any Greeter:存在类型,类型信息被擦除
func greet(_ greeter: any Greeter) {
    print(greeter.greet())
}

greet(EnglishGreeter())   // 输出:Hello

这里具体类型被擦除了,Swift 必须做运行期查找才能确定调用哪个实现。

💡 一句话记忆:泛型约束走静态,存在类型(any P)走动态。


五、@objcdynamic 如何改变分派

当你给方法加上 @objcdynamic,Swift 会切换到 Objective-C 运行时分派

swift 复制代码
import Foundation

class Player: NSObject {
    // @objc dynamic:走 Obj-C message send 机制
    @objc dynamic func play() {
        print("Playing")
    }
}

let player = Player()
player.play()   // 输出:Playing

它使用的是:

  • Objective-C 消息发送(msgSend)
  • 运行期方法查找
  • 兼容 KVO(键值观察)

这种分派方式比 Swift 的 v-table 分派更慢,应当只在确实需要以下场景时使用:

  • KVO 键值观察
  • 方法交换(method swizzling)
  • Objective-C 互操作

六、分派行为汇总表

模式 分派类型
泛型约束(<T: P> 静态分派
协议作为类型(存在类型 P 动态分派
结构体 / 枚举方法 静态分派
final 类方法 静态分派
可重写的类方法 动态分派
@objc dynamic 方法 Obj-C 消息发送(动态,最慢)

七、Swift 如何选择分派方式

Swift 遵循几条通用规则:

  • 如果方法不可能被重写,Swift 优先用静态分派
  • 如果方法可能被重写,Swift 用动态分派
  • 如果类型信息被擦除(协议存在类型),Swift 用动态分派
  • 如果编译器能特化调用(泛型),Swift 优先用静态分派

Swift 的设计哲学是:只要安全,就尽量用静态分派。 静态分派是默认偏好,动态分派是"被迫"才启用。


八、实践设计指南

适合优先用静态分派的场景

  • 性能敏感的代码
  • 实现算法与数据转换
  • 设计库与可复用组件
  • 紧密循环(tight loop) 内部

优先选择:

  • 结构体与枚举
  • 泛型
  • 不打算被继承时的 final

适合用动态分派的场景

  • 建模多态的领域行为
  • 设计插件式系统
  • 在多个运行期实现之间做抽象
  • 构建依赖注入边界

动态分派能提升灵活性与架构清晰度。


九、常见误区

误区 1:「Swift 永远用静态分派」 Swift 只是偏好静态分派;当运行期多态确实需要时,它会回退到动态分派。

误区 2:「动态分派很慢,应当避免」 动态分派确实比静态分派稍慢一点,但在真实应用里这点成本通常可以忽略不计。架构清晰度往往比微观优化更重要。

误区 3:「协议很慢」 协议只有在被当作类型(存在类型)使用时 才走动态分派。配合泛型使用时,协议是静态分派且高度优化的。


十、总结

分派是 Swift 性能模型与抽象模型的基石概念。理解 Swift 在何时、以何种方式选择静态 vs 动态分派,能帮你:

  • 设计更好的 API
  • 避免意外的性能成本
  • 构建更可预测的架构
  • 理性地思考优化行为

你不需要优化每一个调用点------但知道分派是怎么工作的,能让你做出有意识的设计选择,而不是被编译器"随机"安排。


我的见解与扩展场景

1. 这不是"二选一",而是一种"默认偏好"

Swift 的分派选择本质上是一个保守优化:编译器默认尝试静态分派,只有当存在"无法在编译期确定实现"的证据(可重写、类型擦除)时才退化为动态。理解这一点,你就不会再纠结"我到底该用 struct 还是 class"------答案取决于你是否需要多态,而不是性能恐慌。

2. 协议为何"既快又慢"

这是初学者最容易困惑的点。同一个 Greeter 协议:

  • 用在泛型 <T: Greeter> 上 → 编译期单态化(monomorphization),零开销,和直接调 EnglishGreeter.greet() 一样快;
  • 用在 any Greeter 上 → 装箱成存在容器(existential container),多一次 v-table 查找。

所以"协议慢"是个伪命题,是用法决定了快慢 。在性能热点里,把 any Protocol 改成泛型约束往往能直接消除动态分派开销,而不需要改动业务逻辑。

3. 与 Swift 6 严格并发的隐性关联

静态分派在并发安全上也更友好:值类型 + 静态分派天然避免了许多跨 actor 共享可变状态的隐患。当你纠结某段代码为什么过不了 Sendable 检查时,回过头看是不是因为用了类继承 + 动态分派把可变状态"藏"进了抽象层------很多时候换成值类型 + 泛型,问题会自然消失。

4. 扩展场景:写框架时的"分派预算"

在写 SDK / 框架时,我习惯做一张"分派预算表":对外暴露的公开 API 大多需要动态分派(为了可扩展、可重写),但框架内部的算法与数据转换路径尽量保持 struct + final + 泛型,把动态分派限制在少数几个"扩展点"上。这样既给了用户灵活性,又守住了性能底线。

5. 一个可动手验证的小实验

你可以用 Swift 的 -O 优化 + 反汇编(或 Compiler Explorer)观察:把 final 去掉、或把泛型函数改成 any 参数,调用点从 call 直接调用变成了 vtable 间接跳转。亲眼看到这层差异,比读十遍文章都管用。

swift 复制代码
// 实验:对比以下两种写法在 -O 下的生成的调用指令
func direct(_ c: Counter) { c.increment(1) }        // 静态:可能直接内联
func erased(_ c: any Incrementable) { c.increment(1) } // 动态:存在容器 + vtable 查找

参考资料

相关推荐
云逸_4 天前
Cinder:基于 Swift Macro 与 Mach-O Section 的解耦注册机制
ios·swift
末代iOS程序员华仔4 天前
从 Figma 到上架:使用 Codex 与 MCP 全流程实现 AI 聊天 App
flutter·ios·swift
霍霍哈嗨5 天前
swiftUI框架基础
ios·swiftui·swift
末代iOS程序员华仔5 天前
iOS 开发到上架 App Store 全流程详解
ios·objective-c·swift
东坡肘子7 天前
当每一次写入都有了新价格 -- 肘子的 Swift 周报 #144
人工智能·swiftui·swift
zzb15807 天前
Zed 配置 Swift / iOS 开发
开发语言·ios·swift
Geek-Chow8 天前
Mobile App Certificate Pinning: Underlying Principle and a Swift Example
开发语言·ios·swift·安全架构
碎_浪10 天前
Mac 壁纸被 MDM 锁住?我做了个 2MB 的开源方案
macos·开源·swift
小牛不牛的程序员11 天前
开源一个 macOS 剪贴板「意图识别」工具——不只是存储,更是理解你复制了什么
macos·swift