本章介绍了消息表达式如何转化为 objc_msgSend 函数调用,以及如何通过名称引用方法。然后解释了如何利用 objc_msgSend,以及如何——如果需要这么做——能够绕开动态绑定。
3.1 The objc_msgSend Function objc_msgSend方法
在Objective-C中,消息直到运行时才会与方法实现绑定到一起。编译器会将一个消息表达式
[receiver message]
转为消息方法的调用,objc_msgSend。这个函数将接收者和消息中提到的方法名称——也就是方法选择器——作为它的两个主要参数:
objc_msgSend(receiver, selector)
消息中传递的任意参数也可以由objc_msgSend处理:
objc_msgSend(receiver, selector, arg1, arg2, ...)
消息方法完成了动态绑定所需要的每一件事情:
- 它首先找到选择器所指的过程(方法实现)。由于同一个方法可以由不同的类来实现,它找到的精确过程取决于接收者的类。
- 然后,它调用这个过程,将接收对象(的数据指针)和为该方法指定的所有参数传递给这个过程。
- 最后,它将这个过程的返回值作为它自己的返回值传递过来。
注意: 编译器生成对消息函数的调用。你永远不要在你写的代码中直接调用它。
消息通信的关键在于编译器为每个类和对象构建的结构 。每个类结构包括下面两个基本元素:
父类的指针。
一个 dispatch table 。这个表的每一个条目都关联了方法选择器和每个类不同的辨识方法的具体地址。比如, setOrigin:: 方法的选择关联到 setOrigin::(程序实现)的地址, display 方法的选择器关联到 display 的地址,等等。
当创建一个新的对象,为它分配了内存,它的实例变量也初始化了。对象的第一个变量就是其类结构的指针。这个指针,被称为“isa”,让对象可以访问它的类,以及通过这个类访问它继承的所有类。
注意: 虽然语法上不是严格要求,但是对象 isa 指针需要与Objective-C运行时系统共同工作。在这个结构定义的任何字段中,对象都需要“等价于”一个 struct objc_object (定义在 objc/objc.h 中)。然而,你几乎不会,如果真有,需要创建你自己的根对象,继承自 NSObject 或 NSProxy 的对象自动拥有了 isa 变量。
类和对象结构的元素图示见图3-1。
Figure 3-1 Messaging Framework
当消息被发送到一个对象时,消息通信方法按照对象的 isa 指针找到类结构,并在dispatch表中查找方法选择器。如果没有在那里找到选择器,objc_msgSend 按照执照找到父类,并尝试在父类的dispatch表中找到选择器。连续的失败会导致 objc_msgSend 方法按类的层级一层层往上找,直到到达 NSObject 类。一旦它定位到选择器,这个函数会调用表中的方法,并向其传递接收对象的数据结构。
这是在运行时选择方法实现的途径——或者,按照面向对象编程的术语,这叫做方法与消息动态绑定。
为了加快消息传递的速度,运行时系统缓存了使用过的选择器和方法地址。每个类有自己单独的缓存空间,可以包含继承的方法的选择器和类里面定义的方法的选择器。在查找dispatch表之前,消息通信通常首先会检查接受对象的类的缓存空间(理论上用过一次的方法很可能再次使用)。如果方法选择器在缓存中,消息通信只是比方法调用稍微慢一点点。一旦程序运行了足够长的时间让它的缓存“热”起来,几乎所有她发送的消息对可以找到一个缓存的方法。在程序运行时,缓存会动态的适应新的消息通信。
3.2 Using Hidden Arguments 使用隐藏的参数
当objc_msgSend找到实现方法的程序,它就会调用这段程序并将消息中的所有参数传给它。它还会传递两个隐藏的参数:
- 接收的对象
- 方法的选择器
这些参数给予每个方法实现关于调用它的消息表达式的两个准确的信息。它们被称为“隐藏的”是因为它们没有在定义方法的源代码中声明。它们是在代码编译时插入到实现中的。
尽管这些参数没有明确的声明,源代码仍然能引用它们(正如它可以引用接收对象的实例变量)。一个方法认为接收对象是 self,它自己的选择器是 _cmd。在下面的例子中,_cmd 指的是 strange 方法的选择器,而 self 是接收特定消息的对象。
- strange
{
id target = getTheReceiver();
SEL method = getTheMethod();
if ( target == self || method == _cmd )
return nil;
return [target performSelector:method];
}
self 在两个参数中更有用。实际上,这正是接收对象的实例变量对于消息定义可用的方法。
3.3 Getting a Method Address 获取方法地址
规避动态绑定的的唯一方法是,获得方法的地址并且如果这是一个函数就直接调用它。在少数的情况下,当特定方法被连续执行了许多次时,而你想要避免每次方法执行都有消息通信的开销,这么做是合适的。
通过 NSObject 类中定义的方法,methodForSelector:,你可以请求到指向实现方法的程序的指针,然后使用这个指针调用这段程序。 methodForSelector: 返回的指针必须小心的描述为适当的函数类型。返回类型和参数类型都应该包含在描述中。
下面的例子展示了如何调用实现了 setFilled: 方法的程序:
void (*setter)(id, SEL, BOOL);
int i;
setter = (void (*)(id, SEL, BOOL))[target
methodForSelector:@selector(setFilled:)];
for ( i = 0 ; i < 1000 ; i++ )
setter(targetList[i], @selector(setFilled:), YES);
传给程序的前两个参数是接收对象(self)和方法选择器(_cmd)。这些参数在方法语法中是隐藏的,但是当方法作为一个函数被调用时必须明确的给出。
使用 methodForSelector: 规避动态绑定节约了大部分消息通信所需的时间。然而,这个节约只在特定方法被重复多次调用时是有意义的,如上面展示的for循环中。
注意 methodForSelector: 是由Cocoa运行时系统提供的;它并不是Objective-C语言自身的特性。