看 runtime 源码时,一个老生常谈的问题,objc_msgSend 是使用汇编实现的,为什么不使用 C 实现?
为了解决问题,我们直接复现问题:使用 C 实现一个简化的 objc_msgSend,暂不考虑性能、通用性,就是实现一个无参、无返回值的消息发送。
尝试
新建 main.mm
#import <Foundation/Foundation.h>
#import <objc/objc.h>
#import <objc/runtime.h>
@interface Test: NSObject
@end
@implementation Test
- (void)testSth {
NSLog(@"ok\n");
}
@end
// 声明一下我们即将实现的消息函数
void msgSend(void);
int main(int argc, const char * argv[]) {
id s1 = [Test new];
// 调用自定义的 msgSend
auto ptr = (void(*)(id, const char*))msgSend;
ptr(s1, "testSth");
return 0;
}
实现 msgSend 函数
objc_msgSend 汇编逻辑在这篇文章已说明,现在实现一个简单的发送函数。
直接在 main.mm 后面开始写,注意我的电脑是 x86_64,如果是 arm mac 需要看注释适配
# define ISA_MASK 0x00007ffffffffff8ULL
// arm 使用: # define ISA_MASK 0x0000000ffffffff8ULL
void msgSend(void) {
int64_t receiver;
char *sel;
// 取出当前的 self 和 方法名
// for arm,x0 is self,x1 is SEL;
// for x86,rdi is self,rsi is SEL
__asm {
mov receiver, rdi
mov sel, rsi
}
// 获得 isa bits
objc_object *receiver_ptr = (objc_object *)receiver;
int64_t isaBits = (int64_t)receiver_ptr->isa;
isaBits &= ISA_MASK;
// 获得 Class 对象
objc_object *classPtr = (objc_object *)isaBits;
Class ClassObj = (__bridge Class)classPtr;
// 接下来要获取 imp,没必要费功夫去 cache 结构、方法列表查询,
// 为了方便直接用已有的 class_getMethodImplementation 查询
// 系统所有的 SEL(const char*) 都存在一个哈希表内,只存一份。
// 查询方法列表中是否有某个 SEL,为了速度 并不会比较字符串,而是直接比较地址,
// 尽管两个地址上是同样的字符串内容。
// 方法列表中的 SEL 就是全局哈希表中的 SEL。
// 因此如果想调用 class_getMethodImplementation,必须得到唯一的那个 SEL 作为参数。
// 下面两个方法本质相同,返回唯一的 SEL
auto realSEL = sel_registerName(sel);
// auto realSEL = NSSelectorFromString([NSString stringWithCString:sel]);
IMP imp = class_getMethodImplementation(ClassObj, realSEL);
auto ptr = (void(*)(objc_object *, char*))imp;
// 把原来的两个参数放进去,就像是 msgSend 等同于目标 IMP 一样
ptr(receiver_ptr, sel);
// 如果这个方法有多个参数呢?我们如何知道参数数量和类型去强制转换 ptr?
// 通过 method_getTypeEncoding 可以得到返回值和参数类型,
// 接下来要按照 参数数量、类型、返回值类型 穷举所有组合,
// 因为变长参数的存在,理论上有无穷个组合,无法实现。即使只允许有限个参数,也需要上百个 switch case
// 如何解决?
// 我们整个过程其实只是想得到 isa,然后得到 class 对象里面的方法列表,然后匹配 SEL,
// 并不需要知道参数!
// 如果用汇编实现 objc_msgSend(核心的查找 IMP 的逻辑),
// 并在这个过程前后保持参数寄存器、栈内容不变
// (只使用 ABI 中传参用不到的寄存器或者用栈暂存寄存器)
// 获取到 imp 后,直接 call(br) imp,此时所有原参数都原封不动传给了目标函数,
// 目标函数照常按照 ABI 取参数执行。
}
结论
上面代码最后一段已经说明,总结就是:
变长参数,使用 C 需要无数个 case,无法一一穷举,所以不可以使用 C。