简单来说,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 -> [目标方法]
这会导致两个问题:
- 返回地址偏移:目标方法返回时会回到
objc_msgSend,而不是直接回到原始调用者。 - 调试干扰:栈帧中多出的一层会让回溯变得复杂。
汇编处理后的效果:
由于汇编直接跳转(Jump)而不建立新栈帧,目标方法执行完毕后,会利用 objc_msgSend 之前在链接寄存器(LR)里保存的原始返回地址,直接跳回最初的调用处。在调用栈看来,objc_msgSend 就像是消失了一样。
4. 寄存器保护的绝对控制
C 编译器在编译代码时,为了存取局部变量,往往会“自作主张”地使用一些寄存器。
- 对于
objc_msgSend来说,它绝对不能触碰那些存放方法参数的寄存器(如 ARM64 下的 x0-x7)。 - 在汇编中,我们可以手动确保只使用特定的临时寄存器(如 x9-x12),而 C 语言无法提供这种“绝对不破坏特定寄存器”的编译保证。
总结:汇编实现的三大支柱
| 特性 | C 语言实现 | 汇编实现 |
|---|---|---|
| 参数传递 | 无法处理未知数量和类型的参数透传 | 直接操作寄存器,完美透传 |
| 栈帧管理 | 必须创建栈帧,增加开销 | 尾调用跳转,无额外栈开销 |
| 执行速度 | 依赖编译器优化,有黑盒效应 | 指令级优化,极致速度 |
| 调用栈 | 会多出一层,影响回溯 | 对调用者透明,无缝衔接 |
补充:objc_msgSend 的执行逻辑
- 检测空指针:如果是
nil直接返回。 - 查找缓存:在类的
cache(散列表) 里快速查找 IMP。 - 查找方法列表:如果缓存没中,走 C 语言实现的慢速查找路径(
lookUpImpOrForward)。 - 跳转执行:找到后直接
br(跳转) 到该地址。