objc_msgSend函数是objective-C的基础。有人问objc_msgSend的内部实现,我想最好的理解方式就是手动实现一次。
踏板
例如随便写个方法:
[obc message];
编译器会根据方法生成一个消息函数:
objc_msgSend(obj, @selector(message));
objc_msgSend完成message方法的调度。
那么objc_msgSend是如何工作的呢?它寻找合适的函数指针或者方法入口并调用。参数由objc_msgSend传递给IMP,IMP函数执行结束后返回值给发起者。因为objc_msgSend在这个过程中只是充当着获取和调用对应的IMP函数,所以它有点像踏板这个角色。
如果用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, ...);
}
汇编
为了实现最快的速度,runtime中的函数都是用汇编实现的。objc_msgSend为所有的Objective-C函数消息服务,即使是最简单的动作都会导致成千上万的消息。
为了简化,我的实现将会在汇编中做最大的简化,所有的实现会在单独的C函数中实现,汇编所做的事情与下面等价:
id objc_msgSend(id self, SEL _cmd, ...) {
IMP imp = GetImplementation(self, _cmd);
imp(self, _cmd, ...);
}
GetImplementation可以以一种更加容易理解的方式完成所有的工作。
其中的汇编代码需要完成:
- 保存所有的参数在安全的地方,这样GetImplementation就不能修改它们。
- 调用GetImplementation。
- 保存返回值。
- 回复所有的参数值。
- 跳转到GetImplementation返回的IMP函数。
好了,让我们开始吧!
这里使用的是x86-64汇编,它能够很方便的在Mac上运行。原理同时适用于i386或者ARM。
这个函数在msgsend-asm.s文件中实现,它以其它的资源文件通过编译,并且链接到其它的项目中去。
首先需要声明一个全局变量,因为历史原因,C函数的全局变量的命名需要添加额外的下划线前缀:
.globl _objc_msgSend
_objc_msgSend:
编辑器将会迅速的链接到最近的可用的objc_msgSend.为了确定这段代码是否工作,在测试程序中进行简单的链接我们的这段代码去获取[obj message]表达式是非常方便的。
整型和指针参数通过%rsi,%rdi,%rdx,%rcx,%r8和%r9寄存器进行传递。任何额外的参数都会通过上述的寄存器传递到堆栈中。
这个函数做的第一件事情就是保存这6个寄存器到堆栈当中,这样它们在之后就能够被恢复:
pushq %rsi
pushq %rdi
pushq %rdx
pushq %rcx
pushq %r8
pushq %r9
除了上述的寄存器,%rax寄存器是一个隐藏的参数。它用于变量参数的调用,在这个过程中它存储了由向量寄存器传递过来的数量,而这些都是函数用于创建变量参数列表。为了防止目标方法可能是一个变量参数方法,我在这里同样保存这个寄存器:
pushq %rax
如果考虑完整些,%xmm寄存器同样需要保存起来,它用于传递浮点类型的参数。但是如果我能够保证GetImplementation不会使用任何的浮点类型,我就能忽略它,这样我就能保持这更简短的代码了。
下面使堆栈对齐。当函数调用时,Mac OS X要求堆栈16字节边界对齐。上述代码都是堆栈边界对齐的,但是如果有代码对它进行明确的处理就更好了,这样你就不用担心是否会发生异常,或者对发生在动态连接库中的崩溃找不着头脑了。为了使堆栈对齐,当我保存了%r12寄存器的原始数据后,我将现有的堆栈指针保存在%r12寄存器。选择%r12寄存器有点随意,其他访问-保存的寄存器也是可以的。重要的是要保证传递到GetImplementation的消息一直存在。
后面我将堆栈的指针与0x10进行and运算,目的是清空最后四个字节:
pushq %r12
mov %rsp, %r12
andq $-0x10, %rsp
现在堆栈指针式边界对齐的。寄存器的传递和保存也都是安全的,随着堆栈的增长,边界也会随着增大。
最后我们调用GetImplementation,它有两个参数,self和_cmd.调用方式是分别从%rsi和%rdi中获取这两个参数。然而,他们像objec_msgSend那样传递参数,但是并没有移动,所以不会发生任何事情去获取它门到指定的位置。所有的执行都是在GetImplementation中进行:
callq _GetImplementation
整型和指针的值是由%rax返回的,所以这里就是返回IMP的地方。当%rax需要恢复到最初的状态的时候,这个返回的IMP需要移动到其他地方进行保存。我随意选去了%r11寄存器进行保存:
mov %rax, %r11
现在开始将这些都放回原处,首先恢复保存在%r12寄存器中的堆栈指针,同时恢复%r12的初始值:
mov %r12, %rsp
popq %r12
然后参数寄存器倒序出栈:
popq %rax
popq %r9
popq %r8
popq %rcx
popq %rdx
popq %rdi
popq %rsi
一切准备就绪,现在参数寄存器恢复了最初的状态。所有的目标方法的参数都在目标方法能够找到的地方。IMP在%r11,所以要执行就需要跳转到%r11:
jmp *%r11
这就简单的模拟了objc_msgSend的实现过程。当返回非正常的值的时候就会有一点问题。复杂结构体(任何因为过于复杂而不能被寄存器返回)是一个典型的例子。在x86-64架构中,复杂结构体使用隐藏的第一个参数的方式返回。当你调用一个函数:
NSRect r = SomeFunc(a, b, c);
它被翻译成:
NSRect r;
SomeFunc(&r, a, b, c);
内存地址使用%rdi传递的值返回。因为objc_msgSend使用%rdi和%rsi去存储self和_cmd,所以它不会返回复杂的数据体的值。这个问题存在与多个不同的平台。runtime提供了一个方法objc_msgSend_stret函数去解决复杂结构体的返回问题,objc_msgSend_stret的工作原理和objc_msgSend很像,但是知道在%rsi中寻找self,在%rdx中寻找_cmd参数。
在返回浮点类型的时候,类似的问题也会出现在一些平台上面。runtime提供了objc_megSend_fpret(在x86-64平台上,objc_msgSend_fpret2主要解决极端特殊的问题)。
遍历方法
我们来看看GetImplementation的实现,上面的汇编实现说明这些代码可以用C语言实现。当然,在真正的runtime中,为了获得最快的运行速度,它们都是直接用汇编实现的。不仅是因为更好的代码可控性,也减少了寄存器保存和还原的次数,就像上面的代码一样。
GetImplementation能够简单的调用class_getMethodImplementation并且完成它,它将所有的工作都交给了Objective-C runtime去实现。尽管这有点繁琐。在真正的objc_msgSend实现中,为了更快的速度,会先在缓存中寻找对应的方法。既然GetImplementation是模拟objc_msgSend,就需要做同样的事情。只有在缓存中找不到相应的方法的时候,它才会返回到runtime中去寻找。
首先我们需要做的事情就是定义一些结构体。缓存方法是类的私有的数据存取方法,所以我们需要自己实现一套。即使是私有的,这些方法或者数据结构都能够在Apple开源出来的Objective-C runtime的实现中找到方案。
首先我们定一个缓存的入口:
typedef struct {
SEL name;
void *unused;
IMP imp;
} cache_entry;
cache的定义:
struct objc_cache {
uintptr_t mask;
uintptr_t occupied;
cache_entry *buckets[1];
};
缓存被实现成一个hash表。这个表非常高效,它总是2的幂的大小。表的索引是selector,bucket的index是根据selector的值计算出来,可能移动到合理和恰当的位置。
下面是特殊的selector和mask的bucket index的计算的宏定义:
#ifndef __LP64__
#define CACHE_HASH(sel, mask) (((uintptr_t)(sel)>>2) & (mask))
#else
#define CACHE_HASH(sel, mask) (((unsigned int)((uintptr_t)(sel)>>0)) & (mask))
#endif
最后,定义类自身的一个结构体,这是类的实际指向:
struct class_t {
struct class_t *isa;
struct class_t *superclass;
struct objc_cache *cache;
IMP *vtable;
};
现在讲这些必须的结构放入到GetImplementation:
IMP GetImplementation(id self, SEL _cmd) {
//获取类,这里使用系统API实现
Class c = object_getClass(self);
//我想访问内部,需要取得class_t struct的指针
struct class_t *classInternals = (struct class_t *)c;
//状态变量
IMP imp = NULL;
//获取缓存指针
struct objc_cache *cache = classInternals->cache;
//计算bucket index,并获取buckets的入口指针
uintptr_t index = CACHE_HASH(_cmd, cache->mask);
cache_entry **buckets = cache->buckets;
/*
* 在缓存中寻找对应的selector。runtime中使用的是线性链表
* 如果没有找到方法的入口,就会返回到比较慢的runtime中进行寻找
* 在实际的objc_msgSend中,所有的代码都是由汇编实现,但是在返回runtime寻找的时候会跳出汇编
* 而进入到runtime中。一旦在缓存中没有找到对应的方法,增加速度的可能性就没有了
*/
for (; buckets[index] != NULL; index = (index + 1) & cache->mask) {
if (buckets[index]->name == _cmd) {
imp = buckets[index]->imp;
break;
}
}
if (imp == NULL) {
//class_getMethodImplementation找到方法后会填充到缓存中
imp = class_getMethodImplementation(c, _cmd);
}
return imp;
}
测试
@interface Test : MSObject
- (void)none;
- (void)param:(int)x;
- (void)params:(int)a : (int)b : (int)c : (int)d : (int)e : (int)f : (int)g;
- (int)retval;
@end
@implementation Test
- (id)init
{
fprintf(stderr, "in init method, self is %p\n", self);
return self;
}
- (void)none
{
fprintf(stderr, "in none method\n");
}
- (void)param: (int)x
{
fprintf(stderr, "got parameter %d\n", x);
}
- (void)params: (int)a : (int)b : (int)c : (int)d : (int)e : (int)f : (int)g
{
fprintf(stderr, "got params %d %d %d %d %d %d %d\n", a, b, c, d, e, f, g);
}
- (int)retval
{
fprintf(stderr, "in retval method\n");
return 42;
}
@end
int main(int argc, char **argv)
{
for(int i = 0; i < 20; i++)
{
Test *t = [[Test alloc] init];
[t none];
[t param: 9999];
[t params: 1 : 2 : 3 : 4 : 5 : 6 : 7];
fprintf(stderr, "retval gave us %d\n", [t retval]);
NSMutableArray *a = [[NSMutableArray alloc] init];
[a addObject: @1];
[a addObject: @{ @"foo" : @"bar" }];
[a addObject: @("blah")];
a[0] = @2;
NSLog(@"%@", a);
}
}