11-5.【OC】【protocol】Protocol extension 默认实现的静态派发原理是什么?

2 阅读3分钟

在 Swift 中,Protocol Extension(协议扩展)提供的默认实现究竟是静态派发还是动态派发,取决于该方法是否在 Protocol 的主定义(Declaration) 中声明。

这是一个非常隐蔽的性能/行为陷阱。以下是其底层派发原理的深度拆解:


1. 核心判定规则

  • 有声明 + 扩展实现动态派发(通过 Witness Table)。类似于类的虚函数,支持多态。
  • 无声明 + 扩展实现静态派发(直接调用)。不支持多态,调用哪个实现取决于指针的静态类型

2. 静态派发的底层原理:直接地址调用

当你在 Protocol Extension 中定义了一个原协议中不存在的方法时,编译器会将其视为一个全局函数,只是它的作用域被限制在该协议下。

  • 编译期绑定:编译器在编译调用方的代码时,会直接查找当前变量声明的类型。如果类型是协议本身,编译器就直接把 Extension 中那个方法的内存地址写死在调用处。
  • 跳过 PWT:这种调用不会去查阅协议见证表(Protocol Witness Table),因此也就无法在运行时发现子类/实现类是否“重写”了这个方法。

3. “隐蔽陷阱”演示

这是一个典型的由于静态派发导致的“非预期行为”:

Swift

protocol Animal {}

extension Animal {
    func breathe() { print("Animal breathing") } // 没在 protocol 声明,属于静态派发
}

struct Cat: Animal {
    func breathe() { print("Cat breathing") }
}

let cat: Cat = Cat()
let animal: Animal = cat

cat.breathe()    // 输出: "Cat breathing"(静态类型是 Cat)
animal.breathe() // 输出: "Animal breathing"(静态类型是 Animal,直接调 Extension 实现)

为什么 animal.breathe() 输出了 Animal?

因为 breathe 没有在 protocol Animal 的大括号里声明。编译器在处理 animal.breathe() 时,只看到 animalAnimal 协议类型,于是直接跳到了协议扩展的实现地址。


4. 动态派发原理:见证表索引 (Witness Table Offset)

如果方法在 Protocol 中声明了,其派发逻辑会完全改变:

  1. PWT 槽位分配:每个声明的方法都会在协议见证表(PWT)中获得一个固定的偏移量(Offset)
  2. 默认实现作为保底:如果实现类没有提供自己的实现,PWT 对应槽位会指向扩展里的默认实现;如果实现了,则指向类自己的实现。
  3. 运行时查找:无论指针的静态类型是什么,程序都会先根据实例找到 PWT,再按偏移量取出真正的函数指针进行跳转。

5. 性能与灵活性的博弈

特性静态派发 (仅 Extension)动态派发 (Declaration + Extension)
派发开销极低(等同于普通函数)中等(查表 + 间接跳转)
多态支持❌ 不支持✅ 支持
编译器优化支持内联 (Inline) 优化很难进行内联优化
适用场景提供不希望被重写的辅助工具方法定义需要多态表现的核心业务逻辑

💡 总结与建议

如果你希望在扩展里写一个“默认实现”,且希望子类能够通过多态来重写它,务必记得在 Protocol 的原始定义中写下该方法的声明