objc_msgSend 函数支撑了我们使用 Objective-C 实现的一切。Gwynne Raskind,Friday Q&A 的读者,建议我谈谈 objc_msgSend 的内部实现。要理解某件事还有比自己动手实现一次更好的方法吗?咱们来自己动手实现一个 objc_msgSend。
Tramapoline! Trampopoline! (蹦床)
当你写了一个发送 Objective-C 消息的方法:
[obj message]
编译器会生成一个 objc_msgSend 调用:
objc_msgSend(obj, @selector(message));
之后 objc_msgSend 会负责转发这个消息。
它都做了什么?它会查找合适的函数指针或者 IMP,然后调用,最后跳转。任何传给 objc_msgSend 的参数,最终都会成为 IMP 的参数。 IMP 的返回值成为了最开始被调用的方法的返回值。
因为 objcmsgSend 只是负责接收参数,找到合适的函数指针,然后跳转,有时管这种叫做 trampoline(译注:[蹦床](https://en.wikipedia.org/wiki/Trampoline(computing)). 更通用的来说,任何一段负责把一段代码转发到另一处的代码,都可以被叫做 trampoline。
这种转发的行为使 objc_msgSend 变得特殊起来。因为它只是简单的查找合适的代码,然后直接跳转过去,这相当的通用。传入任何参数组合都可以,因为它只是把这些参数留给 IMP 去读取。返回值有些棘手,但最终都可以看成 objc_msgSend 的不同变种。
不幸的是,这些转发行为都不能用纯 C 实现。因为没有方法可以将传入 C 函数的泛参(generic parameters)传给另一个函数。 你可以使用变参,但是变参和普通参数的传递方法不同,而且慢,所以这不适合普通的 C 参数。
如果要用 C 来实现 objc_msgSend,基本样子应该像这样:
id objc_msgSend(id self, SEL _cmd, ...)
{
Class c = object_getClass(self);
IMP imp = class_getMethodImplementation(c, _cmd);
return imp(self, _cmd, ...);
}
这有点过于简单。事实上会有一个方法缓存来提升查找速度,像这样:
id objc_msgSend(id self, SEL _cmd, ...)
{
Class c = object_getClass(self);
IMP imp = cache_lookup(c, _cmd);
if(!imp)
imp = class_getMethodImplementation(c, _cmd);
return imp(self, _cmd, ...);
}
通常为了速度,cache_lookup 使用 inline 函数实现。
汇编
在 Apple 版的 runtime 中,为了最大化速度,整个函数是使用汇编实现的。在 Objective-C 中每次发送消息都会调用 objc_msgSend,在一个应用中最简单的动作都会有成千或者上百万的消息。
为了让事情更简单,我自己的实现中会尽可能少的使用汇编,使用独立的 C 函数抽象复杂度。汇编代码会实现下面的功能:
id objc_msgSend(id self, SEL _cmd, ...)
{
IMP imp = GetImplementation(self, _cmd);
imp(self, _cmd, ...);
}
GetImplementation 可以用更可读的方式工作。
汇编代码需要:
1. 把所有潜在的参数存储在安全的地方,确保 GetImplementation 不会覆盖它们。
2. 调用 GetImplementation。
3. 把返回值保存在某处。
4. 恢复所有的参数值。
5. 跳转到 GetImplementation 返回的 IMP。
让我们开始吧!
这里我会尝试使用 x86-64 汇编,这样可以很方便的在 Mac 上工作。这些概念也可以应用于 i386 或者 ARM。
这个函数会保存在独立的文件中,叫做 msgsend-asm.s。这个文件可以像源文件那样传递给编译器,然后会被编译并链接到程序中。
新闻热点
疑难解答