1-19.【OC】【基础】Objective-C 的方法调用和 C 函数调用,在 ABI 层面有什么本质不同?

2 阅读3分钟

ABI(应用二进制接口) 的层面来看,Objective-C 的方法调用与 C 函数调用的区别,本质上是**“直接跳转”“间接分发”**的对立。

在二进制指令级别,这两者的处理方式决定了它们的执行性能、动态能力以及参数传递的复杂程度。


1. 符号解析与寻址方式 (Addressing)

  • C 函数调用(Static/Direct):

    • ABI 行为: 在链接期,C 函数的符号(例如 _add)会被解析为一个绝对或相对的内存偏移地址
    • 指令: 汇编层面通常是一条简单的 bl (Branch with Link) 或 call 指令。
    • 结果: CPU 执行到这一行时,不需要思考,直接跳转到目标地址执行。
  • Objective-C 方法调用(Dynamic/Dispatch):

    • ABI 行为: 编译器不生成跳转到方法的指令,而是生成一条跳转到 objc_msgSend 的指令。
    • 指令: 它将 receiver(对象指针)和 selector(方法名常量)作为前两个参数传递。
    • 结果: 真正的寻址发生在运行时objc_msgSend 必须遍历类的数据结构才能找到真正的函数指针。

2. 寄存器使用与参数传递 (Calling Convention)

虽然两者都遵循基础的调用约定(如 ARM64 的 x0-x7 寄存器传参),但 Obj-C 有其特殊的“隐占参数”。

特性C 函数调用Objective-C 方法调用
隐式参数self (receiver) 和 _cmd (selector)
寄存器占用x0 是第一个业务参数x0self 占用,x1_cmd 占用
业务起始位从第一个参数开始从第三个参数(x2 寄存器)开始

本质区别: 每一个 Objective-C 方法在 ABI 层面都强制比对应的 C 函数多出两个参数。这意味着如果你有大量的高频小函数,Obj-C 的寄存器压力和压栈开销会略高于 C。


3. 消息派发的“蹦床”机制 (Trampoline)

objc_msgSend 在汇编层面被称为 Trampoline(蹦床)

  1. 它不会破坏调用者的堆栈。
  2. 它在内部完成 IMP 的查找后,会直接执行一条 br(Branch)指令跳转到目标函数。
  3. 魔法所在: 目标方法执行完后,会直接返回到最初调用 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 交换机。