译者前言

总是看到有人说用汇编实现objc_msgSend是为了速度快,当然这个不可否认。但是难道没有别的原因?于是就看到了这篇文章,遂翻译之!=。=

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

先看看原文吧。

原作者: Ari Grant

原文链接: Why objc_msgSend Must be Written in Assembly

http://arigrant.com/blog/2014/2/12/why-objcmsgsend-must-be-written-in-assembly

开始

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

[receiver message:foo beforeDate:bar];

将会被编译成:

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

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

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_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语言里面调用函数,必须在编译时就知道调用的“状态”;而这些“状态”在运行时是无法得出或正确处理的,所以必须往底层走,用汇编处理。(译者注:这里不知道咋翻译好=。=,原文是:calling a function in C requires the signature to be known for each call-site at compile-time;doing so at run-time is not possible and so one must drop down into assembly and party there instead.)

UPDATE – 后续

有人指出objc_msgSend有可能是用GCC的扩展方法__builtin_apply_args,__builtin_apply,和__builtin_return实现的。这也正指出了一个事实,就是这些builtins方法是非常有必要的,因为单靠语言本身无法实现这些功能。实现objc_msgSend所需要的技巧,也正是实现这些builtins方法所需要的技巧。本文的目的并不是非要将什么是真正的C、什么不是真正的C分个清楚,只是为了指出objc_msgSend特殊罢了。

译者总结

开头也说了,我的理解是:用汇编实现,是为了应对不同的“Calling convention”,把函数调用前的栈和寄存器的参数、状态设置,交给编译器去处理。

嗯,以后不要再说用汇编实现只是为了快了=。=

为什么objc_msgSend必须用汇编实现的更多相关文章

  1. 深入理解Objective-C:优化你的代码

    开篇 只要用到Objective-C,我们每天都会跟方法调用打交道.我们都知道Objective-C的方法决议是动态的,但是在底层一个方法究竟是怎么找到的,方法缓存又是怎么运作的却鲜为人知. 本文主要 ...

  2. runtime——消息机制

    本文授权转载,作者:Sindri的小巢(简书) 从异常说起 我们都知道,在iOS中存在这么一个通用类类型id,它可以用来表示任何对象的类型 —— 这意味着我们使用id类型的对象调用任何一个方法,编译器 ...

  3. objc_msgSend消息传递学习笔记 – 消息转发

    该文是 objc_msgSend消息传递学习笔记 – 对象方法消息传递流程 的基础上继续探究源码,请先阅读上文. 消息转发机制(message forwarding) Objective-C 在调用对 ...

  4. objc_msgSend消息传递学习笔记 – 对象方法消息传递流程

    在Effective Objective-C 2.0 – 52 Specific Ways to Improve Your iOS and OS X Programs一书中,tip 11主要讲述了Ob ...

  5. 如何正确的hook方法objc_msgSend · jmpews

    如何正确的hook方法objc_msgSend 前言 如果希望对 Objective-C 的方法调用进行 log, 一个很好的解决方法就是 hook 方法 objc_msgSend, 当然想到的就是利 ...

  6. u-boot源码汇编段简要分析

    Hi,大家好!我是CrazyCatJack,你们可以叫我CCJ或者疯猫.今天我给大家带来的是u-boot的源代码汇编段分析,以后还会给大家讲解后续的C代码,请持续关注哦^_^ 先简单说一下u-boot ...

  7. GCC 预处理、编译、汇编、链接..

    1简介 GCC 的意思也只是 GNU C Compiler 而已.经过了这么多年的发展,GCC 已经不仅仅能支持 C 语言:它现在还支持 Ada 语言.C++ 语言.Java 语言.Objective ...

  8. GDB调试汇编堆栈过程分析

    GDB调试汇编堆栈过程分析 分析过程 这是我的C源文件:click here 使用gcc - g example.c -o example -m32指令在64位的机器上产生32位汇编,然后使用gdb ...

  9. Beennan的内嵌汇编指导(译)Brennan's Guide to Inline Assembly

    注:写在前面,这是一篇翻译文章,本人的英文水平很有限,但内嵌汇编是学习操作系统不可少的知识,本人也常去查看这方面的内容,本文是在做mit的jos实验中的一篇关于内嵌汇编的介绍.关于常用的内嵌汇编(AT ...

随机推荐

  1. url、base64 编码规则

    UrlEncode 相关: URI所允许的字符分作保留与未保留. 保留字符是那些具有特殊含义的字符. 例如, 斜线字符用于URL (或者更一般的, URI)不同部分的分界符. 未保留字符没有这些特殊含 ...

  2. RestKit:iOS开发必备,告别众多无聊代码

    http://www.csdn.net/article/2014-04-15/2819312-RestKit-frameworkRestKit是一款专为iOS设计的Objective-C框架,旨在与R ...

  3. C#隐私信息(银行账户,身份证号码,名字)中间部分特殊字符替换(*)

    最近做到一个关于银行的一个功能模块,需要将隐私信息银行账号中间部分用*代替,于是写下了,如下代码: /// <summary> /// 将传入的字符串中间部分字符替换成特殊字符 /// & ...

  4. Mac上RoR环境搭建问题

    昨天一晚上折磨的我啊都快疯掉了. 按照http://railstutorial-china.org方法配制,到rvm requirements这个命令执行时就麻烦事来了. WutekiMacBook- ...

  5. encodeURL() vs encodeRedirectURL()

    当用URL重写方式来管理Session的时候,通过以上两个方法把session ID写到URL中.不同点是:两个方法确定是否需要包含session ID的逻辑不同.在调用HttpServletResp ...

  6. nodejs 机制

    http://news.cnblogs.com/n/190903/ http://www.ruanyifeng.com/blog/2013/10/event_loop.html https://www ...

  7. 随时可以给doT模板传任何你想要的值

    我以前一直以为只有传给后台的数据才能用doT模板写入, 其实,随时可以把本地处理的数据,仅仅的一个变量,改头换面成一个it关键字下面的属性. 方法就是在tpl中给它赋值. 要注意的是,首先tpl中的属 ...

  8. ConcurrentHashMap的get、put、size

    ConcurrentHashMap的get操作 get操作的高效之处在于整个get过程不需要加锁,get方法里将要使用的共享变量都定义成volatile. ConcurrentHashMap的Put操 ...

  9. CONCATENATION 引发的性能问题

    背景是在一台11gR2的机器上,开发反映一个批处理比以前慢了3倍.经过仔细查看该SQL的执行计划,发现由于SQL中使用了or,导致CBO走出了一个非常糟糕的CONCATENATION路径. no_ex ...

  10. 【HDOJ】2830 Matrix Swapping II

    简单DP. /* 2830 */ #include <iostream> #include <string> #include <map> #include < ...