从 ABI(应用二进制接口) 的层面来看,Objective-C 的方法调用与 C 函数调用的区别,本质上是**“直接跳转”与“间接分发”**的对立。
在二进制指令级别,这两者的处理方式决定了它们的执行性能、动态能力以及参数传递的复杂程度。
1. 符号解析与寻址方式 (Addressing)
-
C 函数调用(Static/Direct):
- ABI 行为: 在链接期,C 函数的符号(例如
_add)会被解析为一个绝对或相对的内存偏移地址。 - 指令: 汇编层面通常是一条简单的
bl(Branch with Link) 或call指令。 - 结果: CPU 执行到这一行时,不需要思考,直接跳转到目标地址执行。
- ABI 行为: 在链接期,C 函数的符号(例如
-
Objective-C 方法调用(Dynamic/Dispatch):
- ABI 行为: 编译器不生成跳转到方法的指令,而是生成一条跳转到
objc_msgSend的指令。 - 指令: 它将
receiver(对象指针)和selector(方法名常量)作为前两个参数传递。 - 结果: 真正的寻址发生在运行时。
objc_msgSend必须遍历类的数据结构才能找到真正的函数指针。
- ABI 行为: 编译器不生成跳转到方法的指令,而是生成一条跳转到
2. 寄存器使用与参数传递 (Calling Convention)
虽然两者都遵循基础的调用约定(如 ARM64 的 x0-x7 寄存器传参),但 Obj-C 有其特殊的“隐占参数”。
| 特性 | C 函数调用 | Objective-C 方法调用 |
|---|---|---|
| 隐式参数 | 无 | self (receiver) 和 _cmd (selector) |
| 寄存器占用 | x0 是第一个业务参数 | x0 被 self 占用,x1 被 _cmd 占用 |
| 业务起始位 | 从第一个参数开始 | 从第三个参数(x2 寄存器)开始 |
本质区别: 每一个 Objective-C 方法在 ABI 层面都强制比对应的 C 函数多出两个参数。这意味着如果你有大量的高频小函数,Obj-C 的寄存器压力和压栈开销会略高于 C。
3. 消息派发的“蹦床”机制 (Trampoline)
objc_msgSend 在汇编层面被称为 Trampoline(蹦床) :
- 它不会破坏调用者的堆栈。
- 它在内部完成 IMP 的查找后,会直接执行一条
br(Branch)指令跳转到目标函数。 - 魔法所在: 目标方法执行完后,会直接返回到最初调用
objc_msgSend的地方,就像objc_msgSend从未存在过一样。这种 ABI 设计保证了消息转发时参数的一致性。
4. 特殊返回类型的处理 (stret / fpret)
这是 ABI 层面最复杂的区别。由于 C 语言对大结构体和浮点数的返回处理各异,Obj-C 必须提供多套派发函数:
objc_msgSend:处理基本类型和指针返回。objc_msgSend_stret:处理返回大结构体的情况(Struct Return)。在某些架构下,需要通过额外的寄存器传递返回值的内存地址。objc_msgSend_fpret:处理特定架构下某些浮点数返回的情况。
C 函数调用在编译时由编译器直接决定使用哪种返回逻辑,而 Obj-C 必须在运行时通过调用不同的派发入口来适配这些 ABI 规范。
5. 编译优化限制
- C ABI: 允许编译器进行内联 (Inlining) 和尾调用优化 (Tail Call Optimization) 。编译器知道函数逻辑不会变,可以直接把代码“搬”过来。
- Obj-C ABI: 由于
Method Swizzling的存在,ABI 必须保证每一次调用都是通过objc_msgSend。这阻断了跨模块的内联优化,导致 CPU 必须频繁经历分支跳转,增加了流水线预测失败(Branch Misprediction)的概率。
总结
C 函数调用的 ABI 是**“点对点”的硬连接,极其高效;而 Objective-C 的 ABI 是“总线制”**的,所有信号必须经过 objc_msgSend 交换机。