当你在 Swift Protocol 前加上 @objc 后,它在 Objective-C (OC) 端的使用体验几乎与原生的 NSProtocol 一致。由于编译器生成了桥接元数据,OC 可以通过标准的 委托模式(Delegate Pattern) 或 运行时反射 来调用它。
以下是具体的调用流程和底层细节:
1. 基础调用:委托模式 (Delegate)
这是最常见的场景。你只需像使用 OC 协议一样引用它。
-
Swift 端定义:
Swift
@objc protocol JSProtocol: AnyObject { func didCompleteTask(status: Int) @objc optional func optionalMethod() } @objc class TaskManager: NSObject { @objc weak var delegate: JSProtocol? } -
OC 端调用:
由于编译器生成了
-Swift.h,你只需导入它,然后像往常一样调用:Objective-C
#import "YourProject-Swift.h" - (void)execute:(TaskManager *)manager { // 直接调用 required 方法 [manager.delegate didCompleteTaskWithStatus:200]; // 检查并调用 optional 方法 if ([manager.delegate respondsToSelector:@selector(optionalMethod)]) { [manager.delegate optionalMethod]; } }
2. 命名转换规则 (Renaming)
Swift 方法名在映射到 OC 时会发生变化,这会影响你在 OC 端的写法:
-
参数标签: 第一个参数后的标签会成为方法名的一部分。
- Swift:
func process(data: Data, count: Int) - OC:
- (void)processWithData:(NSData *)data count:(NSInteger)count;
- Swift:
-
显式命名: 如果你想在 OC 中使用特定的名称,可以使用
@objc(...):- Swift:
@objc(execWithID:) func execute(id: Int) - OC:
[obj execWithID:123];
- Swift:
3. 底层调用链:从 objc_msgSend 到 Swift 实现
当你从 OC 端发起调用时,发生了一系列底层的“接力”:
-
消息派发: OC 调用
objc_msgSend(delegate, @selector(didCompleteTaskWithStatus:), 200)。 -
查找 IMP: Runtime 在该 Swift 类的 OC 元数据(由
@objc插入)中查找到对应的 Thunk(存根) 函数地址。 -
进入 Thunk: 这是一个编译器生成的中间函数。
- 参数转换: 它将
NSInteger转回 Swift 的Int,将NSData转回Data。 - Self 转换: 确保
self符合 Swift 的寄存器约定。
- 参数转换: 它将
-
跳转实现: Thunk 内部调用真正的 Swift 原生函数实现。
4. 关键限制与差异
虽然 @objc 开启了桥接,但由于 OC 的局限性,以下行为会发生变化:
| 特性 | Swift 原生 Protocol | 标记 @objc 后的 Protocol |
|---|---|---|
| 可选方法 | 不支持(需通过协议扩展模拟) | 支持 @objc optional |
| 遵循对象 | Struct, Enum, Class | 仅限 Class (必须继承自 AnyObject) |
| 派发方式 | 协议见证表 (PWT) 派发 | 消息转发 (Message Dispatch) |
| 性能 | 较高(编译器可优化) | 较低(运行时动态查找) |
5. 动态性增强:respondsToSelector:
标记为 @objc 的协议会自动获得 OC 的动态检查能力。在 Swift 中,你无法对非 @objc 协议使用 respondsToSelector:,但在 OC 中,这是调用 @optional 方法的强制性标准动作。
💡 深度启发:为什么要小心使用?
在大型混编项目中,每增加一个 @objc protocol,编译器都会在生成的 -Swift.h 中增加一段代码,并在二进制文件中增加协议描述符。如果协议非常庞大,这会增加 App 的包体积(Binary Size) 和 启动时的类加载耗时。