本文翻译自Messaging
发送消息
本章描述了消息表达式如何被转化为objc_msgSend函数调用,还有如何通过名字索引到方法。之后解释了你如何利用objc_msgSend,还有----如果需要----如何规避动态绑定过程。
objc_msgSend函数
在Objective-C中,消息直到运行时才会被绑定到方法实现上。编译器将消息表达式进行转换:
[receiver message]
转换为消息函数的调用,objc_msgSend。此函数携带着消息中的接受者和方法名(即选择器)作为函数的两个必要参数:
objc_msgSend(receiver, selector)
消息中传入的其他参数也会被objc_msgSend进行处理:
objc_msgSend(receiver, selector, arg1, arg2, ...)
这个消息函数为动态绑定做了所有必要的事情:
- 首先,消息函数查找选择器指向的程序(即方法实现)。由于在不同的类中,相同的方法可以有不同的实现,因此确定的实现取决于查找的接收者的类。
- 之后要调用此实现程序,传入接收对象(其数据的指针),方法指定的其他参数也一同传入。
- 最后,消息函数将实现程序调用后的返回值传回来,就好像自己的返回值一样。
注意:
编译器会自动调用此消息函数。你一定不要在代码中直接调用它。
发送消息的关键在于编译器为每个类和对象构建的结构体。每个类的结构体包含以下两个必要的元素:
- 一个指向父类的指针
- 一个类的分发表。此表包含一个入口,可以把方法选择器和指定类的方法地址连接在一起。setOrigin:: 方法的选择器和 setOrigin:: 的地址(实现程序)连接在了一起,display 方法的选择器和 display 的地址连接在了一起,等等。
当一个新对象被创建出来时,其内存已经被分配好,其实例变量也已经初始化。此对象中的第一个变量是一个指向它所属类结构体的指针。这个指针,叫做isa,给予此对象访问其所属类的能力,并且通过这个类,还可以访问到所有其继承到的类。
注意:
虽然不能严格算是编程语言的一部分,但对于一个对象来说,在与Objective-C运行时系统协同工作时,isa指针却是十分必要的。一个对象”等价于“一个包含了定义的所有字段的objc_object结构体(定义在objc/objc.h中)。可是,你几乎用不到自己来创建根对象,继承了NSObject和NSProxy类的对象早已自动包含了isa指针。
这些类的元素和对象结构体如插图3-1中所示:
当消息发送给某个对象时,发送消息函数会沿着对象的isa指针找到该类的结构体,在其中的分发表中查找方法的选择器。如果在这里找不到选择器,objc_msgSend会沿着其父类的指针继续,并试图在父类的分发表中查找该选择器。连续的失败会导致objc_msgSend会沿着类的层级一直向上爬,直到到达NSObject类。一旦锁定了选择器,此函数就会在分发表中进入此方法中进行调用,并且传入接收者对象的数据结构。
以上就是方法实现在运行时被查找的方式----或者,用面向对象的行话来说,那个方法被动态绑定到了消息上。
为了加速消息发送过程,一旦被使用,运行时系统会缓存该方法的选择器和地址。在每个类中都包含一个独立的缓存区域,并且它同样可以包含继承来的方法选择器。在查找分发表之前,消息发送程序会首先检查接收者类中的缓存(理论上来说,一个方法只要被用过一次就极有可能再次被使用)。如果方法选择器在缓存中,消息发送过程就只会比直接的函数调用稍慢一些。一旦程序已经运行足够的时间来给缓存”热身“,几乎所有发送的消息就都可以找到对应的缓存方法。随着程序的运行,缓存会动态增长空间来缓存新保存的消息。
使用隐藏参数
当objc_msgSend找到了方法的实现程序后,它便调用此程序且将消息中包含的所有参数传递进去。它还会传入两个隐藏参数:
- 接收者对象
- 方法的选择器
这些参数给每一个方法实现提供了代码调用时方法表达式中的两个明确信息。它们被叫做”隐藏“,是因为在定义的方法中没有声明二者。它们只在代码编译时被插入到实现中。
虽然这些参数没有明确声明,源码还是可以索引到它们(就像源码可以索引到接收者内部的实例变量一样)。一个方法把接收者对象称作self,把其自身的选择器称作 _cmd。在下面的例子中,_cmd代表了strange方法的选择器,self则代表了接收strange消息的对象。
- strange
{
id target = getTheReceiver();
SEL method = getTheMethod();
if ( target == self || method == _cmd )
return nil;
return [target performSelector:method];
}
在这两个参数中,self更为有用。实际上,对于方法定义来说,self才是将接收者对象的实例变量变为可用的方式。
获取方法地址
规避动态绑定过程的唯一方法,就是获取方法的地址,然后直接进行函数调用。这种情况也许非常罕见,也许只有当某个指定方法要连续执行许多次,并且你希望避免每次执行之前进行的消息发送过程。
有一个定义在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语言本身的特性。