Swift 与 Objective-C (OC) Protocol 的桥接是 iOS 混编开发的核心。其底层原理可以概括为 “编译器自动合成转换器(Thunking)” 与 “运行时元数据对齐” 。
由于 Swift 的协议支持泛型和关联类型(Static Dispatch),而 OC 协议纯粹依赖消息转发(Dynamic Dispatch),两者之间存在巨大的鸿沟。
1. 桥接的前提:@objc 关键字
在 Swift 中,一个协议要能被 OC 识别,必须满足以下条件:
- 使用
@objc标记。 - 协议必须是 类类型(Class-only) ,即继承自
AnyObject或其他 OC 协议。 - 协议中的成员(属性、方法)必须能被 OC 表达(例如不能有 Swift 独有的元组、泛型、枚举等)。
2. 核心原理:双向视图 (Witness Table vs. Method List)
Swift 和 OC 对“协议实现”的存储方式截然不同:
- OC 协议:实现逻辑直接存在于类的
method_list中。 - Swift 协议:实现逻辑存在于 协议见证表 (Protocol Witness Table, PWT) 中。
当 OC 调用 Swift 实现的协议方法时:
- 编译器介入:Swift 编译器会为实现该协议的 Swift 类自动生成一个 “OC 可见的方法(Thunk)” 。
- 存入方法列表:这个 Thunk 方法会被注册到该类的 OC 运行时方法列表中。
- 消息转发:当 OC 调用
[obj performProtocolMethod]时,通过标准的objc_msgSend找到这个 Thunk。 - 内部跳转:Thunk 内部会负责处理参数转换(如
String转NSString),然后跳转到真正的 Swift 实现。
3. 运行时的元数据映射
当你在混编环境中使用 conformsToProtocol: 时,底层发生了什么?
- OC 视角:OC 运行时只认它在全局哈希表中注册的
protocol_t。 - Swift 视角:Swift 运行时拥有更复杂的协议描述符。
桥接过程:
当你给 Swift 协议加上 @objc 时,Swift 编译器会同时在二进制文件的特定段(Section)生成一个对应的 OC 协议元数据。这样,当 objc_getProtocol("MyProtocol") 被调用时,Runtime 能够找到对应的结构体,使 isKind(of:) 或 conforms(to:) 逻辑在双向环境下都能成立。
4. 关键局限:为什么 Swift 独有特性无法桥接?
这就是为什么带 关联类型(associatedtype) 的 Swift 协议无法标记为 @objc 的原因:
- 关联类型依赖于编译时的类型擦除(Type Erasure) :OC 的消息分发无法在运行时确定这些类型的大小或布局。
- 静态派发 vs 动态派发:Swift 协议默认可以通过静态派发优化,但
@objc强迫协议回退到动态派发模式,这会牺牲一定的性能。
5. 总结:桥接前后的变化
| 特性 | 纯 Swift Protocol | 桥接后的 @objc Protocol |
|---|---|---|
| 派发方式 | 主要是 PWT 派发,支持静态优化 | 完全依赖 objc_msgSend |
| 可选方法 | 只能通过协议扩展模拟 | 支持原生 @optional |
| 内存限制 | 可用于 Struct / Enum / Class | 仅限 Class |
| 关联类型 | ✅ 支持 | ❌ 不支持 |
💡 深度启发:桥接的选择
如果你在做一个纯 Swift 框架,千万不要为了兼容而到处写 @objc,因为这会让你失去 Swift 的面向协议编程 (POP) 中最重要的泛型能力。