第十一条:理解Objc_msgSend的作用

2017/8/21 posted in  第二章 对象,消息,运行期

在对象中调用方法,在oc中称作"传递消息".消息有“名称"(name)或“选择子"(selector),可以接受参数,而且可能还有返回值。

因为OC是C语言的超集,所以我们用C语言来举例.C语言使用“静态绑定”(static binding),也就是说,在编译期就能决定运行时所应调用的函数。

#import <stdio.h>

void printHello() {
    printf ("Hello, world! \n");
    }
void printGoodbye() {
    printf ("Goodbye, world! \n");

void doTheThing(int type) {
    if (type == 0)  {
        printHello();
    }else{
        printGoodbye();
    }
    return 0;
}

编译器在编译代码的时候就已经知道程序中有printHelloprintGoodbye这两个函数了,于是会直接生成调用这些函数的指令。而函数地址实际上是硬编码在指令之中的。

但是将程序改写为下面这样后:

#import <stdio.h>

void printHello() {
    printf ("Hello, world! \n");
    }
void printGoodbye() {
    printf ("Goodbye, world! \n");

void doTheThing(int type) {
    void(*fnc)()
    if (type == 0)  {
        fnc = printHello;
    }else{
        fnc = printGoodbye;
    }
    fnc();
    return 0;
}

这个时候就要使用“动态绑定”(dynamic binding) 了,因为所要调用的函数直到运行期才能确定。编译器在这种情况下生成的指令与刚才那个例子不同,在第一个例子中,ifelse语句里都有函数调用指令。而在第二个例子中,只有一个函数调用指令,不过待调用的函数地址无法硬编码在指令之中,而是要在运行期读取出来

在oc中如果向某对象传递消息(方法调用),就会使用动态绑定机制来决定需要调用的方法。在底层,所有方法都是普通的C语言函数,然而对象收到消息之后,究竟该调用哪个方法则完全于运行期决定,甚至可以在程序运行时改变,这些特性使得Objective-C成为一门真正的动态语言。

给对象发送消息可以这样来写:


id returnValue = [someObject messageName:parameter];

在本例中,someObject叫做“接收者”(receiver),messageName 叫做“选择子”(selector)。选择子与参数合起来称为“消息”(message)。编译器看到此消息后,将其转换为一条标准的C语言函数调用,所调用的函数乃是消息传递机制中的核心函数,叫做objc_msgSend,其
“原型"(prototype)如下:


void objc_msgSend(id self, SEL cmd, •••)

这是个‘参数个数可变的函数’(variadic function)9,能接受两个或两个以上的参数。第一个参数代表接收者,第二个参数代表选择子(SEL是选择子的类型),后续参数就是消息中的那些参数,其顺序不变。选择子指的就是方法的名字。“选择子”与“方法”这两个词经常交替使用。编译器会把刚才那个例子中的消息转换为如下函数:


id returnValue = objc_msgSend(someObject,
                            ^selector(messageName:),
                            parameter);

objc_msgSend函数会依据接收者与选择子的类型来调用适当的方法。为了完成此操作,该方法需要在接收者所属的类中搜寻其“方法列表”(list of methods),如果能找到与选择子名称相符的方法,就跳至其实现代码。若是找不到,那就沿着继承体系继续向上査找,等找到合适的方法之后再跳转。如果最终还是找不到相符的方法,那就执行“消息转发” (message forwarding)操作。消息转发将在第12条中详解。

其它特殊情况需要由一些函数来处理:

  • objc_msgSendstret。如果待发送的消息要返回结构体,那么可交由此函数处理。只有当CPU的寄存器能够容纳得下消息返回类型时,这个函数才能处理此消息。若是返回值无法容纳于CPU寄存器中(比如说返回的结构体太大了),那么就由另一个函数执行派发。此时,那个函数会通过分配在栈上的某个变量来处理消息所返回的结构体。
  • ObjC_mSgSerid_fpret。如果消息返回的是浮点数,那么可交由此函数处理,在某些架构的CPU中调用函数时,需要对浮点数寄存器(floating-point register)做特殊处理,也就是说,通常所用的ObjC_msgSend在这种情况下并不合适。这个函数是为了处理x86等架构CPU中某些令人稍觉惊讶的奇怪状况。
  • objc_msgSendSuper3 如果要给超类发消息,例如[supermessage:parameter],那么就交由此函数处理。也有另外两个与objc_msgSendstret和objc_MsgSend_fpret等效的函数,用于处理发给super的相应消息。

刚才曾提到,Objc_msgSend等函数一旦找到应该调用的方法之后,就会‘跳转过去’。之所以能这样做,是因为Objective-C对象的每个方法都可以视为简单的C函数,其原型如下:


<return type> Class_selector(id self, SEL _cmd, •••)

真正的函数名和上面写的可能不太一样,笔者用“类”(class)和“选择子”(selector)来命名是想解释其工作原理。每个类里都有一张表格,其中的指针都会指向这种函数,而选择子的名称则是査表时所用的“键”。ObjC_msgSend等函数正是通过这张表格来寻找应该执行的方法并跳至其实现的。请注意,原型的样子和ObjC_msgSend函数很像。这不是巧合,而是为了利用“尾调用优化”技术,令“跳至方法实现”这一操作变得更简单些。

要点

  • 消息由接收者、选择子及参数构成。给某对象"发送消息"(invoke a message)也就相当于在该对象上“调用方法”(call a method)。
  • 发给某对象的全部消息都要由“动态消息派发系统”(dynamic message dispatch system)来处理,该系统会査出对应的方法,并执行其代码。