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

479 查看

作者:冬瓜
原文链接:Guardia · 瓜地

Effective Objective-C 2.0 – 52 Specific Ways to Improve Your iOS and OS X Programs一书中,tip 11主要讲述了Objective-C中的消息传递机制。这也是Objective-C在C的基础上,做的最基础也是最重要的封装。

Static Binding And Dynamic Binding

C中的函数调用方式,是使用的静态绑定(static binding),即在编译期就能决定运行时所应调用的函数。而在Objective-C中,如果向某对象传递消息,就会使用动态绑定机制来决定需要调用的方法。而对于Objective-C的底层实现,都是C的函数。对象在收到消息之后,调用了哪些方法,完全取决于Runtime来决定,甚至可以在Runtime期间改变。

一般地,对对象发送消息,我们使用这种写法:

其中someObject为接收者(receiver),messageName为选择子(selector)。当Compiler看的这条语句时,会将其转换成为一条标准的消息传递的C函数,objc_msgSend,形如:

其中,SEL也就是之前对应的选择子,即为此文讨论的重点。我们对应的写出之前代码在Compiler处理后的C语句:

@selector()

对于SEL类型,也就是我们经常使用的@selector(),在很多的书籍资料中的定义是这样:

而至于这个objc_selector的结构体是如何定义的,这就要取决于我们Runtime框架的类型,在iOS开发中,我们使用的是Apple的(GNU也有Runtime的framework)。在OS X中SEL被映射成为一个C字符串(char[]),这个字符串也就是方法名。

我们在lldb中,进行测试:

11208988-ad7ee751bf89c344 (1)

(图释:test是在DGObjectClass中已经定义的方法名,而not_define_testnot_define_test_2没有定义)

第一行我们验证了@selector是一个char[]类型。其他的结果我们可以总结出:@selector()选择子只与函数名有关。而且还有一个规律,那就是倘若选择子方法已经在编译期由Compiler进行静态绑定,则其存储的地址就会更加的具体。

发送消息所依托的选择子只与函数名有关,我们便可以猜想到为什么Objective-C中没有像C++、C#那样的函数重载特性,因为选择子并不由参数和函数名共同决定

那么为什么要有这个选择子呢?在从源代码看 ObjC 中消息的发送一文中,作者Draveness对其原因进行了推断:

  1. Objective-C 为我们维护了一个巨大的选择子表
  2. 在使用 @selector() 时会从这个选择子表中根据选择子的名字查找对应的 SEL。如果没有找到,则会生成一个 SEL 并添加到表中
  3. 在编译期间会扫描全部的头文件和实现文件将其中的方法以及使用 @selector() 生成的选择子加入到选择子表中

objc_msgSend

在选择子拿到对应的地址后,objc_msgSend会依据接收者与选择子的类型来调用适当方法。为了学习此过程,我从opensource.apple.com的git仓库中clone了Runtime源码,并在x86_64架构下macOS环境进行运行。

另外,我在整个工程中增加了一个Class:

并在main入口函数中进行改动:

然后我们在objc-runtime-new.mm中,进行debug。为了研究清楚Runtime是如何查询到调用函数,我们在lookUpImpOrForward下断点。当程序执行[obj test]后,我们发现到达断点位置,并且观察此时的调用栈情况:

12208988-16fcf54446702ed9

objc_msgSend并不是直接调用查询方法,而是先调用了_class_lookupMethodAndLoadCache3这个函数。看下它的源码:

_class_lookupMethodAndLoadCache3就好像一个中转函数,并给出了在查询IMP指针前默认参量的几个布尔值。而由于我们的方法没有进行方法转发,则直接调用了_class_lookupMethodAndLoadCache3这个函数。而当对象在收到无法解读的消息之后,即启动消息转发机制,这时候应该会进入lookUpImpOrNil这个方法。这也是objc_msgSend的一种优化方式。

这里还要注意一点,就是关于Cache的默认参数是NO,因为在objc_msgSend中已经进行过缓存查询。以下是objc_msgSend的汇编实现:

趁热打铁,再来看一下MethodTableLoopup这个宏的实现:

而在objc-msg-x86_64.s中有多个以objc_msgSend为前缀的方法,这个是根据返回值类型和调用者类型分别处理的,我列举三个常用的

OBJC_MSGSEND_STRET 待发送的消息要返回结构体前提是只有当CPU的寄存器能够容纳的下消息返回类型。
objc_msgSend_fpret 消息返回的是浮点数。因为某些架构的CPU调用函数,需要对浮点数寄存器做特殊处理。
objc_msgSendSuper 需要向superClass发送消息时调用。

lookUpImpOrForward

之后我们随着调用栈往上看,在接受到消息入口的命令后,Runtime要开始进行查找方法的操作,源码如下: