𝟙、理解objc_msgSend的作用
对象上调用方法用OC的术语,叫做“传递消息”。消息有名称或选择子,可以接受参数,而且可能还有返回值。
先理解C语言的函数调用方式。C语言使用“静态绑定”,也就是说,在编译器就能决定运行时所应调用的函数。
而如果待调用的函数地址无法硬编码在指令之中,就要在运行期读取出来,使用“动态绑定”。
在Objective-C中,如果向某对象传递消息,那就会使用动态绑定机制来决定需要调用的方法。在底层,所有方法都是普通的C语言函数,然而对象收到消息后,究竟该调用哪个方法则完全于运行期决定,甚至可以在程序运行时改变,这些特性使得Objecttive-C成为一门真正的动态语言。
给对象发送消息可以这样写:
id returnValue = [someObject messageName:parameter];
在例子中:
someObject:接收者
messageName:选择子(选择器)
选择子与参数合起来称为“消息”
编译器看到此消息后,将其转换成一条标准的C语言函数调用,所调用的函数乃是消息传递机制中的核心函数,叫做objc_msgSend,其“原型”如下:
void objc_msgSend(id self, SEL cmd, ...)
这是个“参数个数可变的函数,能接受两个或两个以上的参数。第一个参数代表接收者,第二个参数代表选择子(SEL是选择子的类型),后续参数就是消息中的那些参数,其顺序不变。选择子指的就是方法的名字。“选择子”与“方法”这两个词经常交替使用。编译器会把刚才那个例子中的消息转换为如下函数:
id returnValue = objc_msgSend(someObject, @selector(messageName:), parameter);
objc_msgSend函数会依据接受者与选择子的函数来调用适当的方法。为了完成此操作,该方法需要在接收者所属的类中搜寻其“方法列表”,如果能找到与选择子名称相符的方法,就跳至其实现代码。若是找不到,那就沿着继承体系继续向上查找,等找到合适的方法之后再跳转。如果最终还是找不到相符的方法,那就执行“消息转发”操作。
这样说来,想调用一个方法似乎需要很多步骤。所幸objc_msgSend
会将匹配结果缓存在“快速映射表”(fast map)里面,每个类都有这样一块缓存,若是稍后还向该类发送与选择子相同的消息,那么执行起来就很快了。当然啦,这种“快速执行路径”(fast path)还是不如“静态绑定的函数调用操作”那样迅速,不过只要把选择子缓存起来了,也就不会慢很多,实际上,消息派发并非应用程序的瓶颈所在。加入真是个瓶颈的话,那你可以只编写纯C函数,在调用根据需要,把Objective-C对象的状态传进去。
前面讲的这部分内容只描述了部分消息的调用过程,其他“边界情况”,则需要交由Objective-C运行环境中的另一些函数来处理
上面曾提到,objc_msgSend等函数一旦找到应该调用的方法实现之后,就会跳转过去,之所以能这样做,是因为Objective-C对象的每个方法都可以视为简单的C函数,其原型如下:
<return_type> Class_selector(id self, SEL _cmd, ...)
真正的函数和上面写的可能不太一样,笔者用“类”和选择子来命名是想解释其工作原理。每个类里都有一张表格,其中的指针都会指向这种函数,而选择子的名称则是查表时所用的“键”。objc_msgSend等函数正是通过这张表格来寻找应该执行的方法并跳至其实现的。请注意,原型的样子和objc_msgSend函数很像。这不是巧合,而是为了利用“尾调用优化”(tail-call optimization)技术,令“跳至方法实现”这一操作变得更简单些。
如果某函数的最后一项操作是调用另外一个函数,那么就可以运用“尾调用优化”技术。编译器会生成调转至另一函数所需的指令码,而且不会向调用堆栈中推入新的“栈帧”(frame stack)。只有当某函数的最后一个操作仅仅是调用其他函数而不会将其返回值另作他用时,才能执行“尾调用优化”。这项优化对objc_msgSend非常关键,如果不这么做的话,那么每次调用Objective-C方法之前,都需要为调用objc_msgSend函数准备“栈帧”,大家在“栈踪迹”中可以看到这种“栈帧”。此外,若是不优化,还会过早“栈溢出”(stack overflow)现象。
在实际编写Objective-C代码的过程中,大家无序担心这一问题,不过应该了解其底层工作原理。这样的话,你就会明白,在发送消息时,代码究竟是如何执行的,而且也能理解,为何在调试的时候,栈“回溯”信息中总是出现objc_msgSend。
要点
1️⃣消息由接受者、选择子及参数构成。给某对象“发送消息”(ivoke a message)也就相当于该对象上“调用方法”(call a method)。
2️⃣发给某对象的全部消息都要由“动态消息派发系统”来处理,改系统会查出对应的方法,并执行其代码。