iOS奇思怪巧征服iOS

高效编写代码的方法(九):了解objc_msgSend

2016-05-04  本文已影响3111人  蜂猴

在OC中我们调用方法也叫作给对象发消息,消息包含了名字,选择器,参数及返回值等信息。

C中

一个C语言的例子:

#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;  
} 

在C语言中,在不考虑使用内联函数的情况下,printHello和printGoodbye函数都是已知的。在调用时,编译器直接发出指令去进行调用,函数的地址通过硬解码得到。

现在换一种写法:

#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;  
} 

以上代码则使用了动态绑定的方法,直到运行的时候,fnc函数具体是什么函数是未知的。与第一段代码不同的是,这里获得函数地址的方法不能硬解码获得,而是在运行期间得到。

OC中

OC中,对象调用方法,也叫给对象发送消息,实际上是使用了动态绑定机制。在底层,所有方法都是普通的C语言函数,然而对象收到消息之后,究竟该调用哪个方法则完全于运行期决定,甚至可以在程序运行时改变,这些特性使得Objective-C成为一门真正的动态语言。
通常我们在OC中这样发送消息:

id returnValue = [someObject messageName:parameter];

someObject是消息的接受者,messageName是一个选择器,parameter则为参数。选择器+参数 就是我们所称为的消息。
在底层,编译器将我们的消息转换文标准的C函数形式,如下:

void objc_msgSend(id self,SEL cmd,…)

self 为消息接收者,cmd为选择器,省略号为参数,表示可变长度参数。
因此,以上的消息转换为标准的C函数后如下:

id returnValue = objc_msgSend(someObject,@selector(messageName),paramter)

之所以objc_msgSend方法总能找到正确的函数去执行,原因如下:
其实每个类中都有一张方法列表去存储这个类中有的方法,当发出objc_msgSend
方法时候,就会顺着列表去找这个方法是否存在,如果不存在,则向该类的父类继续查找,直到找到位置。如果始终没有找到方法,那么就会进入到消息转发机制(后续知识,以后章节会介绍) 。
OC runtime还有一个机制在于方法缓存,每调用完这个方法后,一个方法映射就会被缓存起来,如果之后调用相同的方法,那么就能直接从映射表里确定方法的位置,而不用每次都需要查找,这样执行速度会快一点。

几个特殊方法

objc_msgSend_stret
如果待发送的消息要返回结构体,那么可交由此函数处理。只有当CPU的寄存器能够容纳得下消息返回类型时,这个函数才能处理此消息。若是返回值无法容纳于CPU寄存器中(比如说返回的结构体太大了),那么就由另一个函数执行派发。此时,那个函数会通过分配在栈上的某个变量来处理消息所返回的结构体。

objc_msgSend_fpret
如果消息返回的是浮点数,那么可交由此函数处理。在某些架构的CPU中调用函数时,需要对“浮点数寄存器”(floating-point register)做特殊处理,也就是说,通常所用的objc_msgSend在这种情况下并不合适。这个函数是为了处理x86等架构CPU中某些令人稍觉惊讶的奇怪状况。

objc_msgSendSuper
如果要给超类发消息,例如[super message:parameter],那么就交由此函数处理。也有另外两个与objc_msgSend_stret和objc_msgSend_fpret等效的函数,用于处理发给super的相应消息。

以上内容摘抄自网上翻译,因为英文原文这部分实在是不太好理解。
我觉得可以简单的按照字面意思来进行选择,比如你希望函数返回体为结构体,那么就使用objc_msgSend_stret,否则有几率会崩溃。返回值为浮点数时也是相同道理。

上文说过,当找到相应的方法时,会跳转过去。之所以可以这样实现,是因为每一个Objective-C函数都可以看作是一个简单的C函数,原型如下:

<return_type> Class_selector(id self,SEL _cmd,...)

以上Class及selector的命名是为了方便理解。每个类中都有一张类似于字典的方法表,而selector就相当于查找方法的key,objc_msgSend函数就是通过查这张表来实现跳转的。之所以以上原型和objc_msgSend方法长的非常相像,是为了更好使用tail-call技术来时方法的跳转更加优化。

如果某函数的最后一项操作是调用另外一个函数,那么就可以运用“tail-call”技术。
此时编译器会生成调转至另一函数所需的指令码,而不会向调用堆栈中推入新的“栈帧”。tail-call使用的条件比较苛刻,除了要求函数的最后一项操作是调用另外一个函数外,,并且要求另外一个函数不是有返回值的函数类型。tail-call对objc_msgSend非常关键,如果不这么做的话,那么每次调用Objective-C方法之前,都需要为调用objc_msgSend函数准备“栈帧”,若是不优化,还会过早地发生“栈溢出”(stack overflow)现象。

在写OC中,我们其实并不需要了解那么多底层的东西,但是我们需要知道调用一个方法之后,OC底层都发生了什么。

总结

上一篇下一篇

猜你喜欢

热点阅读