凌晨一点钟

技术感想

(译)我们来构造objc_msgSend

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可以以一种更加容易理解的方式完成所有的工作。

其中的汇编代码需要完成:

  1. 保存所有的参数在安全的地方,这样GetImplementation就不能修改它们。
  2. 调用GetImplementation。
  3. 保存返回值。
  4. 回复所有的参数值。
  5. 跳转到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);
        }
    }

Let's Build objc_msgSend

阅读更多
上一篇Swizzling方法
想对作者说点什么? 我来说一句

没有更多推荐了,返回首页

关闭
关闭