logo头像

小玉的技术博客

Objective-C运行时编程指南之消息

本章描述了代码的消息表达式如何转换为对objc_msgSend函数的调用,如何通过名字来指定一个方法,以及如何使用objc_msgSend函数。

objc_msgSend 函数

在Objective-C中,消息是直到运行的时候才和方法实现绑定的。编译器会把一个消息表达式,
[receiver message]
转换成一个对消息函数objc_msgSend的调用。该函数有两个主要参数:消息接收者和消息对应的方法名字——也就是方法选标:
objc_msgSend(receiver, selector)
同时接收消息中的任意数目的参数:
objc_msgSend(receiver, selector, arg1, arg2, ...)
该消息函数做了动态绑定所需要的一切:

  • 它首先找到选标所对应的方法实现。因为不同的类对同一方法可能会有不同的实现,所以找到的方法实现依赖于消息接收者的类型。
  • 然后将消息接收者对象(指向消息接收者对象的指针)以及方法中指定的参数传给找到的方法实现。
  • 最后,将方法实现的返回值作为该函数的返回值返回。
    【注】编译器将自动插入调用该消息函数的代码。您无须在代码中显示调用该消息函数。
    消息机制的关键在于编译器为类和对象生成的结构。每个类的结构中至少包括两个基本元素:
  • 指向父类的指针。
  • 类的方法表。方法表将方法选标和该类的方法实现的地址关联起来。例如,setOrigin::的方法选标和setOrigin::的方法实现的地址关联,display 的方法选标和display的方法实现的地址关联,等等。
    当新的对象被创建时,其内存同时被分配,实例变量也同时被初始化。对象的第一个实例变量是一个指向该对象的类结构的指针,叫做isa。通过该指针,对象可以访问它对应的类以及相应的父类。
    注意:尽管严格来说这并不是Obective-C语言的一部分,但是在Objective-C运行时系统中对象需要有isa指针。对象和结构体struct objc_object(在objc/objc.h中定义)必须“一致”。然而,您很少需要创建您自己的根对象,因为从NSObject或者NSProxy继承的对象都自动包括isa变量。
    类和对象的结构如图 3-1所示。

    当对象收到消息时,消息函数首先根据该对象的isa指针找到该对象所对应的类的方法表,并从表中寻找该消息对应的方法选标。如果找不到,objc_msgSend将继续从父类中寻找,直到NSObject类。一旦找到了方法选标, objc_msgSend则以消息接收者对象为参数调用,调用该选标对应的方法实现。 这就是在运行时系统中选择方法实现的方式。在面向对象编程中,一般称作方法和消息动态绑定的过程。 为了加快消息的处理过程,运行时系统通常会将使用过的方法选标和方法实现的地址放入缓存中。每个类都有一个独立的缓存,同时包括继承的方法和在该类中定义的方法。消息函数会首先检查消息接收者对象对应的类的缓存(理论上,如果一个方法被使用过一次,那么它很可能被再次使用)。如果在缓存中已经有了需要的方法选标,则消息仅仅比函数调用慢一点点。如果程序运行了足够长的时间,几乎每个消息都能在缓存中找到方法实现。程序运行时,缓存也将随着新的消息的增加而增加。

    使用隐藏的参数

    当objc_msgSend找到方法对应的实现时,它将直接调用该方法实现,并将消息中所有的参数都传递给方法实现,同时,它还将传递两个隐藏的参数:
  • 接收消息的对象
  • 方法选标 这些参数帮助方法实现获得了消息表达式的信息。它们被认为是”隐藏“的是因为它们并没有在定义方法的源代码中声明,而是在代码编译时是插入方法的实现中的。
    尽管这些参数没有被显示声明,但在源代码中仍然可以引用它们(就象可以引用消息接收者对象的实例变量一样)。在方法中可以通过self来引用消息接收者对象,通过选标_cmd来引用方法本身。在下面的例子中,_cmd指的是strange方法,self指的收到strange消息的对象。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    - strange
    {
    id target = getTheReceiver();
    SEL method = getTheMethod();
    if ( target == self || method == _cmd )
    return nil;
    return [target performSelector:method];
    }

在这两个参数中,self更有用一些。实际上,它是在方法实现中访问消息接收者对象的实例变量的途径。

获得方法地址

避免动态绑定的唯一办法就是取得方法的地址,并且直接像函数调用一样调用它。当一个方法会被连续调用很多次,而且您希望节省每次调用方法都要发送消息的开销时,使用方法地址来调用方法就显得很有效。 利用NSObject类中的methodForSelector:方法,您可以获得一个指向方法实现的指针,并可以使用该指针直接调用方法实现。methodForSelector:返回的指针和赋值的变量类型必须完全一致,包括方法的参数类型和返回值类型都在类型识别的考虑范围中。
下面的例子展示了怎么使用指针来调用setFilled:的方法实现:

1
2
3
4
5
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语言本身的功能。

支付宝打赏 微信打赏

赞赏是不耍流氓的鼓励