第11条:理解objc_msgSend的作用
本条要点:(作者总结)
- 消息由接收者、选择子及参数构成。给某对象“发送消息”(invoke a message)(invoke 也是“调用”的意思,此处为了与“call”相区隔,将其临时译为“发送”,也可能理解为“激发”,“触发”) 也就相当于在该对象上“调用方法”(call a method)。
- 发给某对象的全部消息都要由“动态消息派发系统”(dynamic message dispatch system)来处理,该系统会查出对应的方法,并执行其代码。
在对象上调用方法是 Objective-C 中经常使用的功能。用 Objective-C 的术语来说,这叫“消息传递”(pass a message)。消息有“名称”(name)或“选择子”(selector)(选择器、选取器),可以接受参数,而且可能还有返回值。
由于 Objective-C 是 C 的超集,所以最好先理解 C 语言的函数调用方式。C 语言使用“静态绑定”(static binding),也就是说,在编译期就能决定运行时所调用的函数。以下列代码为例:
1 #import <stdio.h> 2 3 void printHello() { 4 printf("Hello, world!\n"); 5 } 6 7 void printGoodbye { 8 printf("Goodbye, world!\n"); 9 } 10 11 void doTheThing(int type) { 12 if (type == 0) { 13 printHell(); 14 } else { 15 printGoodbye(); 16 } 17 return 0; 18 }
如果不考虑 “內联”(inline),那么编译器在编译代码的时候就已经知道程序中有 printHello 与 printGoodbye 这两个函数了,于是会直接生成调用这些函数的指令。而函数地址实际上是硬解码在指令之中的。若是将刚才那段代码写下下面这样,会如何呢?
1 #import <stdio.h> 2 3 void printHello() { 4 printf("Hello, world!\n"); 5 } 6 7 void printGoodbye { 8 printf("Goodbye, world!\n"); 9 } 10 11 void doTheThing(int type) { 12 void (*func)(); 13 if (type == 0) { 14 func = printHell; 15 } else { 16 func = printGoodbye; 17 } 18 func(); 19 return 0; 20 }
这时就得使用“动态绑定”(dynamic binding)了,因为所要调用的函数直到运行期才能确定。编译器在这种情况下生成的指令与刚才那个例子不同,在第一个例子中,if 与 else 语句里面都有函数调用指令。而在第二个例子中,只有一个函数调用指令,不过待调用的函数地址无法硬编码在指令之中,而是要在运行期读取出来。
在 Objective-C 中,如果向某对象传递消息,那就会使用动态绑定机制来决定需要调用的方法。在底层,所有方法都是普通的 C 语言函数,然而对象收到消息之后,究竟该调用哪个方法则完全于运行期决定,甚至可以在程序运行时改变,这些特性使得 Objective-C 成为一门真正的动态语言。
给对象发送消息可以这样来写:
1 id returnValue = [someObject messageName:parameter];
在本例中,someObject 叫做“接收者”(receiver),messageName 叫做“选择子”(selector)。
选择子与参数合起来称为“消息”(message)。编译器看到此消息后,将其转换为一条标准的 C 语言函数调用,所调用的函数乃是消息传递机制中的核心函数,叫做 objc_msgSend,其“原型”(prototype)如下:
1 void objc_msgSend(id self, SEL cmd, ...)
这是个“参数个数可变的函数”(variadic function)(可变参数函数),能接受两个或两个以上的参数。第一个参数代表接收者,第二个参数代表选择子(SEL 是选择子的类型),后续参数就是消息中的那些参数,其顺序不变。选择子指的就是方法的名字。“选择子”与“方法”这两个词经常交替使用。编译器会把刚才那个例子中的消息转换为如下函数:
1 id returnValue = objc_msgSend(someObject, @selectro(messageName:), parameter);
objc_msgSend 函数会依据接收者与选择子的类型来调用适当的方法。为了完成此操作,该方法需要在接收者所属的类中搜寻其“方法列表”(list of methods),如果能找到与选择子名称相符的方法,就跳至其实现代码。若是找不到,那就沿着继承体系继续向上查找,等找到合适的方法之后再跳转。如果最终还是找不到相符的方法,那就执行“消息转发”(message forwarding)操作。
这么说来,想调用一个方法似乎需要很多步骤。所幸 objc_msgSend 会将匹配结果缓存在“快速映射表”(fast map)里面,每个类都有这样一块缓存,若是稍后还向该类发送与选择子相同的消息,那么执行起来就很快了。当然啦,这种“快速执行路径”(fast path)还是不如“静态绑定的函数调用操作”(statically bound function call)那样迅速,不过只要把选择子缓存起来了,也就不会慢很多,实际上,消息派发(message dispatch)并非应用程序的瓶颈所在。假如真是个瓶颈的话,那你可以只编写纯 C 函数,在调用时根据需要,把 Objective-C 对象的状态传进去。
前面讲的这部分内容只描述了部分消息的调用过程,其他 “边界情况”(edge case)(可理解为特殊情况)则需要交由 Objective-C 运行环境中的另一个函数来处理:
- objc_msgSend_stret。如果待发送的消息要返回结构体,那么可交由此函数处理。只有当 CPU 的寄存器能够容纳得下消息返回类型时,这个函数才能处理此消息。若是返回值无法容纳于 CPU 寄存器中(比如说返回的结构体太大了),那么就由另一个函数执行派发。此时,那个函数会通过分配在栈上的某个变量来处理消息所返回的结构体。
- objc_msgSend_fpret。如果消息返回的是浮点数,那么可交由此函数处理。在某些架构的 CPU 中调用函数时,需要对 “浮点数寄存器”(floating-point register)做特殊处理,也就是说,通常所用的 objc_msgSend 在这种情况下并不合适。这个函数是为了处理 x86 等架构 CPU 中某些令人稍觉惊讶的奇怪状况。
- objc_msgSendSuper。如果要给超类发消息,例如[super message:parameter],那么就交由此函数处理。也有另外两个与 objc_msgSend 和 objc_msgSend_fpret 等效的函数,用于处理发给 super 的相应消息。
刚才提到,objc_msgSend 等函数一旦找到应该调用的方法实现(作者在本书中所说的“实现”(implementation)大多是名词,意为“实现代码”或“可执行的指令”)之后,就会 “跳转过去”。之所以能这样做,是因为 Objective-C 对象的每个方法都可以视为简单的 C 函数,其原型如下:
1 <return_type> Class_selector(id self, SEL _cmd, ...)
真正的函数名和上面写的可能不太一样,笔者用 “类”(class)和 “选择子”(selector)来命名是想解释其工作原理。每个类里都有一张表格,其中的指针都会指向这种函数,而选择子的名称则是查表时所用的“键”。objc_msgSend 等函数正是通过这张表格来寻找应该执行的方法并跳至其实现的。请注意,原型的样子和 objc_msgSend 函数很像。这不是巧合,而是为了利用“尾调用优化”(tail-call optimization)(又称“尾递归优化”),技术,令 “跳至方法实现”这一操作变得更简单些。
如果某函数的最后一项操作是调用另外一个函数,那么就可以运用 “尾调用优化”技术。
编译器会生成调转至另一函数所需的指令码,而且不会向调用栈中推入新的 “栈帧”(frame stack)。只有当某函数的最后一个操作仅仅是调用其他函数而不会将其返回值另做他用时,才能执行“尾调用优化”。这项优化对 objc_msgSend 非常关键,如果不这么做的话,那么每次调用 Objective-C 方法之前,都需要为调用 objc_msgSend 函数准备 “栈帧”,大家在 “栈踪迹”(stack trace)中可以看到这种 “栈帧”。此外,若是不优化,还会过早的发生“栈溢出”(stack overflow)现象。
在实际编写 Objective-C 代码过程中,大家无须担心这一问题,不过应该了解其底层工作原理。这样的话,你就会明白,在发送消息时,代码究竟是如何执行的,而且也能理解,为何在调试的时候,栈“回溯”(backtrace)信息中总是出现 objc_msgSend。
END