3-10.【OC】【Runtime】objc_msgSend 为什么必须写成汇编,而不能是普通 C 函数?

3 阅读3分钟

简单来说,objc_msgSend 是 Objective-C 动态性的心脏,它之所以必须用**汇编(Assembly)**编写,是因为它需要挑战 C 语言的极限,完成三件 C 语言几乎“不可能完成的任务”。

以下是核心原因的深度解析:


1. 动态参数的“透传” (Perfect Forwarding)

objc_msgSend 是一个变长参数函数。在调用它时,编译器并不知道你要调用的具体方法(IMP)长什么样:它可能有 0 个参数,也可能有 10 个参数;可能是整数,也可能是结构体。

  • C 语言的局限:如果你用 C 语言写一个函数,在函数内部调用另一个函数时,你必须明确写出参数。即使使用 va_list,也会产生巨大的开销,且无法完美地将寄存器中的原始参数原封不动地传递给下一个函数。
  • 汇编的优势:汇编可以直接操作寄存器。objc_msgSend 只需要在查找完方法地址(IMP)后,直接执行一个 br (Branch) 或 jmp (Jump) 指令跳转到目标地址。此时,寄存器里的参数状态和调用 objc_msgSend 时完全一致,目标方法就像直接被原始调用者调用一样,实现了“参数零损耗透传”。

2. 必须极致追求性能 (Performance is King)

作为 Objective-C 中调用频率最高的函数,任何一点点多余的指令都是对 CPU 的犯罪。

  • 无感开销:在汇编中,开发者可以手动优化每一个指令周期,精细控制缓存行(Cache Line)和分支预测。
  • 避免栈帧开销:C 函数在进入时通常会建立自己的栈帧(Stack Frame) ,退出时销毁。而 objc_msgSend 采用了“尾调用优化”,它不需要建立自己的栈帧,找到 IMP 后直接跳过去。这不仅省了空间,更省了时间。

3. 维护调用栈的“透明性”

如果 objc_msgSend 是一个普通的 C 函数,那么在调试时,你的调用栈(Call Stack)会变成这样:

main -> [某个函数] -> objc_msgSend -> [目标方法]

这会导致两个问题:

  1. 返回地址偏移:目标方法返回时会回到 objc_msgSend,而不是直接回到原始调用者。
  2. 调试干扰:栈帧中多出的一层会让回溯变得复杂。

汇编处理后的效果

由于汇编直接跳转(Jump)而不建立新栈帧,目标方法执行完毕后,会利用 objc_msgSend 之前在链接寄存器(LR)里保存的原始返回地址,直接跳回最初的调用处。在调用栈看来,objc_msgSend 就像是消失了一样。


4. 寄存器保护的绝对控制

C 编译器在编译代码时,为了存取局部变量,往往会“自作主张”地使用一些寄存器。

  • 对于 objc_msgSend 来说,它绝对不能触碰那些存放方法参数的寄存器(如 ARM64 下的 x0-x7)。
  • 在汇编中,我们可以手动确保只使用特定的临时寄存器(如 x9-x12),而 C 语言无法提供这种“绝对不破坏特定寄存器”的编译保证。

总结:汇编实现的三大支柱

特性C 语言实现汇编实现
参数传递无法处理未知数量和类型的参数透传直接操作寄存器,完美透传
栈帧管理必须创建栈帧,增加开销尾调用跳转,无额外栈开销
执行速度依赖编译器优化,有黑盒效应指令级优化,极致速度
调用栈会多出一层,影响回溯对调用者透明,无缝衔接

补充:objc_msgSend 的执行逻辑

  1. 检测空指针:如果是 nil 直接返回。
  2. 查找缓存:在类的 cache (散列表) 里快速查找 IMP。
  3. 查找方法列表:如果缓存没中,走 C 语言实现的慢速查找路径(lookUpImpOrForward)。
  4. 跳转执行:找到后直接 br (跳转) 到该地址。