Objective-c方法调用本质上是消息传递。消息包括消息名称name,选择器selector(其实就是函数指针)。传递的消息可以接受参数,也可能有返回值。
C语言的函数调用
要理解OC的消息传递,就该说一下C语言的函数调用方式,毕竟OC是C语言的延伸语言。C语言使用静态绑定Static binding进行函数调用,说人话就是C语言在编译期就能决定运行时要调用的函数。比如:
#import <stdio.h>
void printHello() {
printf("Hello,world!\n");
}
void printGoodbye() {
printf("Goodbye,world!\n");
}
void doSomeThing(int type) {
if (type == 0) {
printHello();
}else {
printGoodbye();
}
return 0;
}
如果不考虑内联函数的情况(C语言中为提高函数调用的效率,直接将函数体对函数调用进行替换的方式),那么在C语言中,编译器在编译期就已经知道程序中有printHello和printGoodbye这两个函数了,于是就可以直接调用这两个函数的指令,实际上这两个函数的地址是被硬编码在指令里面的。
我们把上述例子改变一下:
#import <stdio.h>
void printHello() {
printf("Hello,world!\n");
}
void printGoodbye() {
printf("Goodbye,world!\n");
}
void doSomeThing(int type) {
void (*fnc)();
if (type == 0) {
fnc = printHello;
}else {
fnc = printGoodbye;
}
fnc();
return 0;
}
这时候就使用了动态绑定Dynamic binding,所调用的函数要到运行期才能确定。在第二个例子的代码中,只有一个函数调用指令,并且待调用函数的函数地址并不能硬编码在指令中,而是要等到运行期才能读取。
Objective-c的消息传递
在OC中,传递消息其实就是用动态绑定机制来决定需要调用的方法。在OC底层,所有的方法都是普通的C语言函数,对象收到消息后,调用哪个方法完全由运行时决定,甚至在程序运行的时候发生改变,所以OC是一门动态语言。
一个典型的方法调用或者说给对象发送消息可以这样来写:
id returnValue = [someObj messageName:parameter];
someObj叫做接收者,messageName是选择器,选择器及其参数,一起被称为消息(Message)。编译器看到这条消息后,会把它转化为一条标准的C语言函数调用,这个是整个OC运行时消息传递机制的核心,也就是objc_msgSend,原型如下:
void objc_msgSend(id self,SEL cmd,...)
objc_msgSend是一个参数可变函数,能接受两个或两个以上参数。第一个参数是接受者,第二个参数是选择器,其中SEL为选择器的类型,后面就是消息中的参数了,顺序不变。所以把刚才那个典型的方法调用转换如下:
id returnValue = objc_msgSend(someObj,
@selector(messageName:),
parameter);
objc_msgSend函数会依据接受者和选择器的类型调用适当的方法。此时,objc_msgSend会在接收者所属的类中搜寻一个方法列表,如果能找到和选择器名称相符合的方法,就跳转到其实现的代码。如果找不到,就沿着继承体系继续向上查找,等找到合适的方法后再跳转。如果这时候还是找不到符合的方法,就执行消息转发(message forwarding)操作,这个会在下一篇文章中详解。下面用一个流程图说明上述过程:
在整个过程中,objc_msgSend会将匹配结果缓存在一个快速映射表里,每一个类都有一块这样的缓存。之后同一个类发送相同选择器的消息时,执行起来就会更快。但实际上,这种方式的速度还是不如静态绑定那么快,但大多数情况下,消息发送并不是一个应用程序的性能瓶颈,如果真的出现了性能问题,可以编写单纯的C语言函数,在调用时根据需要,传入OC对象即可。
边界情况
一些边界情况,OC的运行环境会提供另外一些对应的函数处理:
objc_msgSend_stret:处理待发送消息需要返回结构体的情况,但当结构体过大,导致CPU寄存器无法容纳时,会把消息交给另一个函数派发,把返回的机构体通过分配在栈上的某个变量处理。objc_msgSend_fpret:处理待发送消息需要返回浮点数的情况。在某些架构的CPU中,需要对浮点寄存器做一些特殊处理,所以这时候调用objc_msgSend并不合适。objc_msgSendSuper:向父类发送消息。
objc_msgSend找到对应的方法调用实现后,会直接跳转过去,OC中每一个方法都可以理解为是一个简单的C语言函数,原型如下:
<return_type> Class_selecotor(id self,SEL _cmd,...)
每个类的方法列表中的指针都会指向这种函数,选择器就是查表时候所用的“键”。
这里我们发现,方法的原型模样和objc_msgSend有些类似,这不是巧合,而是OC利用了尾调用优化。
尾调用优化
如果某个函数最后一项操作也是调用另一个函数,那么就可以利用尾调用优化了。编译器会生成跳转到另一个函数所需要的指令码,而且不会向调用栈中推入新的栈帧。这里有几个需要解释的地方:
- OC的方法调用会为
objc_msgSend准备栈帧,是一个进栈的过程。 - 当某个函数最后一项操作也是调用另一个函数,并且调用的函数不作为返回值另做他用,才能使用尾调用优化。
比如最后返回
return [self message:someMsg];时可以进行尾调用优化,但return [self message:someMsg]+1;这种情况不行,因为虽然调用了函数,但最终调用的函数是为返回值做准备的。 - 尾调用优化会通过复用栈帧,避免调用方法时不断的进栈造成栈溢出最后程序崩溃。