笔者翻译自iOS Developer Library Messaging
消息机制
这章讲解了消息表达式如何被转化成 objc_msgSend 函数调用和怎样通过函数名关联方法。然后还解释了你如何利用 objc_msgSend,并且在你需要的时候如何绕过动态绑定。
objc_msgSend 函数
在 Object-C 中,消息是直到运行时才绑定相应的方法实现。编译器将转换消息表达式,
[receiver message]
为一个消息函数调用,objc_msgSend。该函数将接收者(receiver)和在消息中提到的函数名(也叫函数选择器)作为它的两个主要参数:
objc_msgSend(receiver, selector)
消息传递的任何其他参数也将传递给 objc_msgSend:
objc_msgSend(receiver, selector, arg1, arg2, ...)
消息函数将为动态绑定完成所有必须的事情:
- 它首先找到函数选择器指定的程序块(函数方法的实现)。因为相同的方法在不同的类中有不同的实现,它找到的正确的程序块将依赖接收者对应的类。
- 然后它将消息接收者对象(一个指向它的数据的指针)和该方法相关的其他参数传给该代码块,并调用该代码块。
- 最后它将该代码块的返回值作为它自己的返回值。
提示:编译器将生成消息函数的调用。你不应该直接在你所写的代码中调用它。
消息机制的关键在于编译器为每个类和对象建立的结构。每个类结构都包含这两个必要的元素:
- 指向父类的指针。
- 类分配表。这个表有一些数据项,他们将方法选择器和该类定义的方法的地址相对应。方法 setOrigin:: 的选择器和 setOrigin:: 的地址(方法的实现)相对应,方法 display 的选择器和 display 的地址相对应,等等。
当一个对象被创建,它的内存被分配,并且它的实例变量被初始化。在对象中的第一个变量就是指向它的类结构的指针。这个指针叫做 isa,它使对象能访问它所属的类,并且通过这个类能访问所有它继承的类。
提示:尽管 isa 指针严格上不是语言的一部分,但它是一个对象能在 Object-C 运行时系统中工作所必须的。一个对象需要在 struct objc_object(定义在 objc/objc.h中)定义的任何字段域上保持对等。然而,你很少需要去创建自己的根类,而继承自 NSObject 和 NSProxy 的对象将自动带有 isa 变量。
这些类和对象的结构元素如图所示。
当消息被发送到一个对象,消息函数将沿着对象的 isa 指针到类结构,并在分配表(dispatch table)中查找方法选择器。如果它不能在这里找到方法选择器, obj_msgSend 将沿着父类指针并尝试在父类的分配表中找到方法选择器。连续的失败使 obj_msgSend 沿着类的继承关系向上走,直到到达 NSObject 类。一旦它找到了选择器,消息函数将调用分配表中的方法并把接收者对象的数据结构传递给它。
这就是方法实现在运行时被选择的方式——或者,按面向对象编程的行话来说就是,方法被动态绑定给消息。
为了加速消息处理,当方法被调用时,运行时系统缓存了选择器和方法地址。这是一个相对于每个类分开的缓存,它能够包含继承的方法和自身定义的方法的选择器。在搜索分配表之前,消息程序首先检查接收者对象所在类的缓存(基于这样的理论:一个方法一旦被使用,很可能会被再次使用)。如果这个方法选择器在缓存中,消息机制只会稍微比直接函数调用慢一点。一旦程序已经运行了足够长的时间去“热身”它的缓存,几乎它发送的所有消息都能找到一个缓冲的方法。当程序运行时,缓存将动态地成长去适应新消息。
使用隐藏参数
当 obj_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 在这两个参数中更加有用。实际上,它使得消息的接收者对象的实例变量在方法定义中可以被访问。
得到方法地址
绕过动态绑定的唯一方法就是得到一个方法的地址并且把它当做一个函数直接调用它。在极少的情况下,当一个指定的方法将被连续执行很多次并且你想避免每次方法执行时消息机制带来的额外开销时,绕过动态绑定就变得合理了。
使用在 NSObject 类中定义的方法 methodForSeletor:,你能得到一个指向方法实现的程序块的指针,并能使用这个指针去调用该程序块。methodForSeletor:返回的指针必须转换成合适的函数类型。返回值和参数类型都应该包含在转换中。
下面的例子展示了实现了 setFill: 方法的代码块如何被调用:
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 运行时系统提供的,它并不是一个 Object-C 语言自身的特性。