当你在 Swift 方法前加上 @objc 标记时,编译器(Swiftc)不再仅仅生成一段高效的二进制机器码,而是会在你的二进制文件中注入一套复杂的“元数据链”,以确保 Objective-C Runtime 能够理解并调用这段逻辑。
编译器主要插入了以下四类关键信息:
1. 注册选择子(Selector Registration)
在 Objective-C 中,方法调用的核心是 Selector(SEL) 。
- 编译器行为:编译器会在生成的二进制文件的
__objc_methname段中存入方法名的字符串(如myMethodWithParam:)。 - 映射逻辑:如果 Swift 方法名与 OC 规范冲突(比如带默认参数或重载),编译器会通过一套命名规则合成一个合法的 OC 选择子,并将其关联到该 Swift 方法。
2. 插入存根函数(Method Thunks)
这是 @objc 最核心的底层机制。Swift 的原生调用约定(Calling Convention)与 OC 的 objc_msgSend 完全不同。
-
问题:Swift 可能会通过寄存器传递参数,且不包含
self和_cmd这两个 OC 隐式参数。 -
解决方案:编译器会自动生成一个 Entry Point(进入点) ,也叫 Thunk。
- 当 OC 调用该方法时,实际上是先进入这个 Thunk 函数。
- Thunk 负责将参数从 OC 格式转换(转换)为 Swift 格式。
- 最后,Thunk 内部再跳转到真实的 Swift 实现函数。
3. 生成方法描述符(Method Descriptors)
为了让 class_copyMethodList 等 Runtime API 能找到这个方法,编译器必须在类的元数据中登记。
编译器会插入一个 method_t 结构体:
- Name: 指向
__objc_methname中的字符串。 - Types: 包含方法的 Type Encoding(类型编码,如
v16@0:8),描述参数和返回值类型。 - IMP (Implementation) :指向上面提到的那个 Thunk 函数的内存地址,而不是直接指向 Swift 原生函数。
4. 协议见证映射(Protocol Witness Mapping)
如果该方法是为了满足某个 @objc protocol 的要求:
- 编译器会在
__objc_protolist段中记录该方法所属的协议信息。 - 它会确保在运行时,通过协议查询(
conformsToProtocol:)时,该方法能被识别为协议要求的实现。
5. 内存布局中的“双面身份”
通过注入这些信息,该方法在内存中实际上拥有了“双重身份”:
| 身份 | 派发机制 | 性能 | 查找方式 |
|---|---|---|---|
| Swift 身份 | V-Table 或静态派发 | 极高(支持内联) | 编译时已知或偏移量查找 |
| Objective-C 身份 | Message Dispatch | 较低(需 Hash 查找) | 通过 objc_msgSend 运行时查找 |
💡 深度启发:无声的体积增长
在大型项目中,过度使用 @objc(或在 Swift 4 之前使用 @objcMembers)会导致二进制体积显著增大。因为编译器每为一个方法插入 @objc,就意味着多出了一套 字符串、方法结构体和 Thunk 代码块。