为何objc_msgSend必须用汇编实现

原文连接: Why objc_msgSend Must be Written in Assembly数组

老是看到有人说用汇编实现objc_msgSend是为了速度快,固然这个不能否认。可是难道没有别的缘由?因而就看到了这篇文章,遂翻译之!=。=缓存

我本身的理解就是,用汇编实现,是为了应对不一样的“Calling convention”,把函数调用前的栈和寄存器的参数、状态设置,交给编译器去处理。函数

先看看原文吧。翻译

开始指针

对于Objective-C来讲,调用一个对象实例的方法,也叫做向这个对象实例“发送消息”,而每条“消息”,在编译阶段都会转变为一次对objc_msgSend函数的调用,调用的参数不只有本来消息的全部参数,还有消息的接收者receiver和对应的方法selector。举个例子,下面的语句:对象

    [receiver message:foo beforeDate:bar];blog

将会被编译成:继承

    objc_msgSend(receiver, @selector(message:beforeDate:), foo, bar);ip

对于objc_msgSend函数的实现原理,前人已经作了大量的探索。因此,本文将会把重点放在objc_msgSend的一个以前没有太受到关注的点上,那就是:get

objc_msgSend是不可能用Objective-C、C或者C++实现的。

THE RETURN TYPE - 返回类型

先看看以下两行代码: 

    NSUInteger n = [array count];

    id obj = [array objectAtIndex:6];

直观上看,将会被编译成:

    NSUInteger n = objc_msgSend(array,  @selector(count));

    id obj = objc_msgSend(array, @selector(objectAtIndex:), 6);

可是实际上这是不可能的,由于没有函数能够同时知足这两个调用。并且它的返回值也不能同时是NSUInteger和id。

并且,上面的代码也是没法编译经过的。那么,加上类型转换怎么样?

    NSUInteger n = (NSUInteger (*)(id, SEL))objc_msgSend(array,  @selector(count));

    id obj = (id (*)(id, SEL, NSUInteger))objc_msgSend(array, @selector(objectAtIndex:), 6);

 

这下能够编译经过了,虽然看起来不直观。。。

objc_msgSend是一个Public的函数,在<objc/message.h>里声明,若是你想直接调用它,就必须按照上面的格式加上强制类型转换,要否则是没法编译经过的。可是objc_msgSend究竟是如何实现,来支持各类返回类型的?本文后面会讲到。

THE IMP - 方法对应的函数指针

objc_msgSend函数的本质很简单,传入一个接受者对象实例receiver和方法名selector,它就会按照如下步骤执行:(译者注:只是最粗略的步骤=。=)

  • 获取receiver得类Class
  • 在Class的方法列表method table里面查找对应selector的方法实现
  • 找到的话就调用,返回
  • 找不到就在其父类中找,重复前面的步骤(直到没有父类为止)

整个流程很简单,沿着继承链,向上找到方法selector对应的函数指针便可,也就是IMP。同时,在每层Class中都有缓存,加快后续的方法查找。可是,这也只是objc_msgSend的实现细节,因此,接着往下看。

THE ARG TYPES AND COUNT - 参数类型和数量

简单来讲,当objc_msgSend找到对应的函数指针后,只要用传入的参数调用这个函数便可。剩下来的就是找到一种方法,能够调用任意参数类型、数量的任意函数。

参数的数量很容易计算。而后咱们能够把全部的参数都放入varargs,而后调用函数时传入便可。可是这样的话,每一个Objective-C的方法都必须在其prologue(译者注:函数执行具体的“任务”前,所作的准备环节)里面把全部的参数从varargs里面提取出来。

这种把参数打包到varargs里面而后又取出来的办法显然是很是糟糕的,同时也是没必要要的。

在C语言中,调用一个函数会被编译成对应的汇编语言指令,首先是设置参数(把参数放到寄存器、栈上),而后用如jump或者call的指令,跳到具体的函数代码地址处。若是咱们想支持任意类型的函数类型,咱们就必须写一个switch语句,把全部的参数组合状况都包含起来,这样才能正确的为任何形式的函数设置参数(译者注:即按照某种“规范”、“约定”,把参数依次存放到“约定”的寄存器、栈上),这显然是没有扩展性的,更是不可能的。

UNWINDING THE CALL - 拆解调用

objc_msgSend的解决办法,主要依据的是:当objc_msgSend被调用时,全部的参数已经被设置好了

换一种方式来讲,就是:在objc_msgSend开始执行时,栈帧(stack frame)的状态、数据,和各个寄存器的组合形式、数据,跟调用具体的函数指针(IMP)时所需的状态、数据,是彻底一致的!

以下这行代码:

    id obj = objc_msgSend(array, @selector(objectAtIndex:), 6);

在调用objc_msgSend时,须要设置三个参数,分别是被调用方receiver、方法名selector和最后一个整型参数6。这和具体的方法函数IMP的参数顺序、类型是彻底一致的,也就是说,调用objc_msgSend前,设置的栈、寄存器的状态、数据正是调用具体的方法函数时须要的状态!

 

因此,当objc_msgSend找到要调用的函数实现IMP后,只须要把全部的对栈、寄存器的操做“倒”回到objc_msgSend执行开始的状态(相似于函数执行完成return返回前,作的“收尾处理”工做同样,即epilogue),直接jump/call到IMP函数指针对应的地址,执行指令便可,由于全部的参数已经被设置好了。

同时,当selector对应的IMP执行完成后,返回值也被正确的设置好了(在x86平台上,返回值被设置到了指定的寄存器eax/rax里,在arm上,则是r0寄存器),因此,咱们也没必要担忧前文提到的不一样类型的返回值问题了。

WRAP UP - 总结

把上面提到的全部解释综合起来,就是:在C语言里面调用函数,必须在编译时就知道调用的“状态”;而这些“状态”在运行时是没法得出或正确处理的,因此必须往底层走,用汇编处理。

相关文章
相关标签/搜索