高效编写代码的方法(九):了解objc_msgSend
在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底层都发生了什么。
总结
- 1 一个消息包含接受者,选择子和参数。调用一个方法相当于像对象发送一条消息。
- 2 当发送消息是,动态绑定机制会帮助我们查找方法的实现并进行运行。