Effective Objective-C 2.0 读书笔记------ objc_msgSend
文章目录
- [Effective Objective-C 2.0 读书笔记------ objc_msgSend](#Effective Objective-C 2.0 读书笔记—— objc_msgSend)
引入------静态绑定和动态绑定
我们知道OC实际上是在C的基础上引入面向对象的内容,我们先来理解在C语言之中函数调用的方式------静态绑定(static binding)也就是编译器在编译的时候就能够知道运行时所调用的函数,以书中的代码为例:
objc
#include <stdio.h>
void printHello() {
printf("Hello, world!\n");
}
void printGoodbye() {
printf("Goodbye, world!\n");
}
void doTheThing(int type) {
if (type == 0) {
printHello();
} else {
printGoodbye();
}
}
在这段代码中,函数 printHello
和 printGoodbye
的调用是直接的,编译器在编译时就能确定这些函数的调用路径。在这个过程中,函数名直接指向特定的地址,编译器无需做任何动态决策。所有的函数调用都在编译时已经解析好了,这就是静态绑定。
再来看下一个例子,如果我们将代码改写为以下的内容
objc
void printHello() {
printf("Hello, world!\n");
}
void printGoodbye() {
printf("Goodbye, world!\n");
}
void doTheThing(int type) {
void (*fnc)(); // 定义一个函数指针
if (type == 0) {
fnc = printHello; // 如果type为0,指向printHello函数
} else {
fnc = printGoodbye; // 否则,指向printGoodbye函数
}
fnc(); // 调用通过指针指定的函数
}
这种方法就是动态绑定 (dynamic binding)。在编译时,编译器并不知道 fnc
最终会指向哪个函数,它只能知道 fnc
是一个指向 void()
类型的函数指针,但不能确定它指向的函数直到程序运行时。也就是说,函数的调用和方法的选择是在程序运行时才决定的,因为编译器无法在编译时确定到底调用哪个函数。
OC之中动态绑定的实现
在Objective- C中,如果向某对象传递消息,那就会使用动态绑定机制来决定需要调用的方法。在底层,所有方法都是普通的C语言函数,然而对象收到消息之后,究竟该调用哪个方法则完全于运行期决定,甚至可以在程序运行时改变,这些特性使得Objective- C成为 一门真正的动态语言。
我们模拟给对象发送通知:
objc
id returnValue = [someObject messageName:parameter];
编译器看到这条命令之后,就会自动的将其转化一条标准的C语言函数调用objc_msgSend
,objc_msgSend
是 Objective-C 的一个底层函数,它是 Objective-C 动态消息传递机制的核心。通过它,消息被发送给对象,进而调用对象的某个方法。
方法签名
objc_msgSend
的函数签名如下(简化版):
objc
id objc_msgSend(id self, SEL _cmd, ...);
self
:消息的接收者,即对象。_cmd
:选择子- 后面的
...
:是方法参数,消息的实际内容。
所以刚刚上面的代码,就可以转化为以下函数
objc
id returnValue = objc_msgSend (someObject, @selector (messageName:), parameter) ;
接下来就是objc_msgSend
根据接受者类型以及选择子来调用对应的方法,借用书中的原话:
方法需要在接收者所属的类中搜寻其"方法列表"(list of methods)。如果能找到与选择子名称相符的方法,就跳至其实现代码。若是找不到,那就沿着继承体系继续向上查找,等找到合适的方法之后再跳转。如果最终还是找不到相符的方法,那就执行 "消息转发"(message forwarding)操作。
方法列表
这里说到了方法列表,就顺带讲一下:
在 Objective-C 中,每个类都有一套方法列表,用于存储该类的所有实例方法、类方法及它们的相关信息。这些方法列表(Method List)用数组的形式存储了与类相关的所有方法,并且可以通过运行时(Runtime)机制进行动态查找和调用。
我们知道方法有两种,类方法 和实例方法,那么方法列表也可以分成两种:
实例方法(Instance Methods):这些方法是类的实例(对象)调用的。
类方法(Class Methods):这些方法是类本身(而非类的实例)调用的。
获取实例方法列表
使用 class_copyMethodList
函数可以获取某个类的实例方法列表。返回的是一个 Method
数组,数组中包含了该类的所有实例方法。
objc
unsigned int methodCount = 0;
Method *methods = class_copyMethodList([MyClass class], &methodCount);
for (unsigned int i = 0; i < methodCount; i++) {
Method method = methods[i];
SEL methodSelector = method_getName(method); // 获取方法的选择子
const char *methodTypeEncoding = method_getTypeEncoding(method); // 获取方法类型编码
NSLog(@"Method name: %s", sel_getName(methodSelector));
}
free(methods);
class_copyMethodList
:返回类的实例方法列表。method_getName
:获取方法的选择子。method_getTypeEncoding
:获取方法的类型编码。sel_getName
:将选择器转换为字符串。
获取类方法列表
获取类方法列表的过程和获取实例方法列表类似,只不过你需要使用 class_copyMethodList
获取的是类本身(而不是类的实例)的方法列表。
objc
unsigned int methodCount = 0;
Method *methods = class_copyMethodList(object_getClass([MyClass class]), &methodCount);
for (unsigned int i = 0; i < methodCount; i++) {
Method method = methods[i];
SEL methodSelector = method_getName(method);
const char *methodTypeEncoding = method_getTypeEncoding(method);
NSLog(@"Class method name: %s", sel_getName(methodSelector));
}
free(methods);
object_getClass
:获取类的元类(meta-class
),元类包含了类方法。
其他方法
在使用objc_msgSend
的时候,我们注意到我们的返回值是OC对象,但如果我们这个函数返回的是其他内容,例如:结构体,浮点数和超类的极端情况出现时,objc_msgSend
可能就有局限性了。那么自然有其他的方法来解决这些问题
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
等效的函数,用于处理发给超类的相应消息。
尾调用优化
尾调用优化(TCO)是一种编译器优化技术。它的核心思想是 如果一个函数的最后一个操作是调用另一个函数,并且这个函数的返回值不会被进一步使用,那么编译器可以避免为这个函数创建新的栈帧。
尾调用的条件是:
- 最后一个操作是直接调用另一个函数。
- 返回值没有进一步的操作(比如乘法、加法等)。
具体是否使用尾调用优化的情景,可以看这篇文章
正常我们调用一个递归的函数,CPU会不断向调用堆栈之中推入栈帧,如下图
每次我们通过 objc_msgSend
调用一个方法时,都会为这次调用创建一个新的栈帧。栈帧包含了当前方法调用的信息,如参数、返回地址等。由于 OC 的动态特性,objc_msgSend
需要在调用之前处理很多工作,比如查找方法的实现、解析参数等,这些都需要在栈中存储信息。
书中的原话:
只有当某个函数的最后一个操作仅仅是调用另一个函数,并且不使用该函数的返回值时,才可以执行"尾调用优化"(Tail Call Optimization)。
在 Objective-C 中,
objc_msgSend
的尾调用优化非常关键。如果没有进行尾调用优化,每次调用 Objective-C 方法时,都需要为调用objc_msgSend
函数准备一个新的"栈帧"。这些栈帧会在 "栈踪迹"(stack trace)中可见。如果不进行尾调用优化,调用栈会不断增长,可能会导致"栈溢出"(stack overflow)现象。栈溢出通常发生在递归调用或大量函数调用没有得到优化时,导致栈空间耗尽。
以下是使用尾调用优化时,函数栈帧的变化,都是共用同一个栈帧
总结
消息其实就是由接受者,选择子和方法参数所构成,给对象发送消息其实就是相当于在该对象之中调用方法。通过 objc_msgSend
,方法的调用变得非常灵活,可以在运行时根据对象和方法选择器找到并执行相应的方法。这个机制为我们后面学习的动态方法解析、消息转发、方法交换等强大的特性做了铺垫。