天天看点

Objective-C Runtime Programming Guide

Objective-C Runtime Programming Guide

与运行时交互

Objective-C程序在三个不同的层次与运行时系统交互:

  • 通过Objective-C源代码;
  • 通过在基础框架的NSObject类中定义的方法;
  • 通过直接调用运行时函数。

Objective-C Source Code

在大多数情况下,运行时系统在后台自动工作。您只需编写和编译Objective-C源代码就可以使用它。

当您编译包含Objective-C类和方法的代码时,编译器将创建实现语言动态特性的数据结构和函数调用。数据结构捕获类和类别定义以及协议声明中的信息;它们包括在用Objective-C编程语言定义类和协议时讨论的类和协议对象,以及方法选择器、实例变量模板和从源代码中提取的其他信息。主运行时函数是发送消息的函数,如消息传递中所述。它由源代码消息表达式调用。

NSObject方法

Cocoa中的大多数对象都是NSObject类的子类,因此大多数对象都继承它定义的方法。(一个值得注意的例外是NSProxy类;有关更多信息,请参阅消息转发。)因此,它的方法建立了每个实例和每个类对象固有的行为。然而,在一些情况下,NSObject类只定义了一个模板,用于说明应该如何完成某件事;它本身并没有提供所有必需的代码。

例如,NSObject类定义了一个description实例方法,该方法返回一个描述类内容的字符串。这主要用于调试GDB print object命令打印从该方法返回的字符串。这个方法的NSObject实现不知道类包含什么,所以它返回一个带有对象名称和地址的字符串。NSObject的子类可以实现此方法以返回更多详细信息。例如,基础类NSArray返回它包含的对象的描述列表。

有些NSObject方法只是查询运行时系统的信息。这些方法允许对象执行自省。此类方法的示例有类方法,它要求对象标识其类;isKindOfClass:和isMemberOfClass:,用于测试对象在继承层次结构中的位置;respondsToSelector:,指示对象是否可以接受特定消息;conformToprotocol:,指示对象是否声明实现在特定协议中定义的方法;methodForSelector:,它提供方法实现的地址。像这样的方法给对象提供了自我反省的能力。

Runtime Functions

运行时系统是一个动态共享库,其公共接口由位于目录/usr/include/objc中的头文件中的一组函数和数据结构组成。这些函数中的许多都允许您使用纯C来复制编写Objective-C代码时编译器所做的工作。其他的则是通过NSObject类的方法导出的功能的基础。这些函数使得开发运行时系统的其他接口和生成扩展开发环境的工具成为可能;在Objective-C中编程时不需要它们。但是,在编写Objective-C程序时,一些运行时函数可能会有用。所有这些函数都记录在Objective-C运行时参考中。

Messaging

本章介绍如何将消息表达式转换为objc_msgSend函数调用,以及如何按名称引用方法。然后解释如何利用objc_msgSend,以及如果需要,如何绕过动态绑定。

The objc_msgSend Function

在Objective-C中,消息直到运行时才绑定到方法实现。编译器转换消息表达式,

调用消息传递函数objc_msgSend。此函数将接收方和消息中提到的方法的名称(即方法选择器)作为其两个主要参数:

消息中传递的任何参数也将传递给objc_msgSend:

消息传递函数执行动态绑定所需的所有操作:

  • 它首先找到选择器引用的过程(方法实现)。由于同一方法可以通过不同的类实现,因此它找到的精确过程取决于接收方的类。
  • 然后调用过程,将接收对象(指向其数据的指针)以及为该方法指定的任何参数一起传递给它。
  • 最后,它将过程的返回值作为自己的返回值传递。
注意:编译器生成对消息传递函数的调用。永远不要在编写的代码中直接调用它。

消息传递的关键在于编译器为每个类和对象构建的结构。每个类结构都包含以下两个基本元素:

  • 指向超类的指针
  • 一个类调度表。此表中的条目将方法选择器与其标识的方法的类特定地址相关联。setOrigin::方法的选择器与setOrigin::(实现的过程)的地址相关联,display方法的选择器与display的地址关联,依此类推。

创建新对象时,将为其分配内存,并初始化其实例变量。对象变量中的第一个变量是指向其类结构的指针。这个名为isa的指针让对象访问其类,并通过该类访问它继承的所有类。

注意:虽然isa指针不是严格意义上的语言的一部分,但它是对象与Objective-C运行时系统一起工作所必需的。在结构定义的任何字段中,对象都需要与struct objc_对象(在objc/objc.h中定义)“等效”。但是,您很少需要创建自己的根对象,并且从NSObject或NSProxy继承的对象自动具有isa变量。

这些类元素和对象结构如图3-1所示。

Objective-C Runtime Programming Guide

当消息发送到对象时,消息传递函数跟随对象的isa指针指向类结构,在该类结构中查找调度表中的方法选择器。如果在那里找不到选择器,objc_msgSend会跟随指向超类的指针并尝试在其调度表中查找选择器。连续的失败导致objc_msgSend爬升类层次结构,直到到达NSObject类。一旦找到选择器,函数就会调用表中输入的方法,并将接收对象的数据结构传递给它。

这就是在运行时选择方法实现的方式,或者用面向对象编程的行话来说,方法是动态绑定到消息的。

为了加快消息传递过程,运行时系统在使用方法时缓存选择器和地址。每个类都有一个单独的缓存,它可以包含继承方法的选择器以及类中定义的方法的选择器。在搜索调度表之前,消息传递例程首先检查接收对象类的缓存(理论上,曾经使用过一次的方法可能会再次使用)。如果方法选择器在缓存中,则消息传递只比函数调用稍慢。一旦一个程序运行足够长的时间来“预热”它的缓存,它发送的几乎所有消息都会找到一个缓存方法。缓存动态增长以适应程序运行时的新消息。

Using Hidden Arguments

当objc_msgSend 查找实现方法的过程,在过程中找到并调用所有的过程参数时,它将调用该过程中的所有参数。它还向过程传递两个隐藏参数:

  • 接收对象
  • 方法的选择器

这些参数为每个方法实现提供关于调用它的消息表达式的两部分的显式信息。它们被称为“隐藏”,因为它们没有在定义方法的源代码中声明。它们在代码编译时被插入到实现中。

Although these arguments aren’t explicitly declared, source code can still refer to them (just as it can refer to the receiving object’s instance variables). A method refers to the receiving object as self, and to its own selector as _cmd. In the example below, _cmd refers to the selector for the strange method and self to the object that receives a strange message.

虽然这些参数没有显式声明,但是源代码仍然可以引用它们(就像它可以引用接收对象的实例变量一样)。方法将接收对象引用为self,并将其自己的选择器引用为_cmd。在下面的示例中,_cmd表示奇怪方法的选择器,self指向接收到奇怪消息的对象.

- strange
{
    id  target = getTheReceiver();
    SEL method = getTheMethod();
 
    if ( target == self || method == _cmd )
        return nil;
    return [target performSelector:method];
}
           

self is the more useful of the two arguments. It is, in fact, the way the receiving object’s instance variables are made available to the method definition.

Self是两个参数中比较有用的。实际上,这是接收对象的实例变量可用于方法定义的方式。

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语言本身的特性。

Dynamic Method Resolution

本章介绍如何动态提供方法的实现。

Dynamic Method Resolution

在某些情况下,您可能希望动态地提供方法的实现。例如,Objective-C声明属性特性(请参阅Objective-C编程语言中的声明属性)包括@dynamic指令:

它告诉编译器将动态提供与属性关联的方法。

可以实现resolveInstanceMethod:和resolveClassMethod:方法,分别为实例和类方法的给定选择器动态提供实现。

Objective-C方法只是一个C函数,它至少有两个参数-self和_cmd。可以使用函数class_addMethod将函数作为方法添加到类中。因此,考虑到以下功能:

void dynamicMethodIMP(id self, SEL _cmd) {
    // implementation ....
}
           

您可以使用resolveInstanceMethod动态地将其添加到类中,作为方法(称为ResolveThisMethod):如下所示:

@implementation MyClass
+ (BOOL)resolveInstanceMethod:(SEL)aSEL
{
    if (aSEL == @selector(resolveThisMethodDynamically)) {
          class_addMethod([self class], aSEL, (IMP) dynamicMethodIMP, "[email protected]:");
          return YES;
    }
    return [super resolveInstanceMethod:aSEL];
}
@end
           

转发方法(如消息转发中所述)和动态方法解析在很大程度上是正交的。在转发机制生效之前,类有机会动态解析方法。

如果调用了respondsToSelector:或instanceRespondToSelector:,则动态方法解析器将有机会首先为选择器提供IMP。如果实现resolveInstanceMethod:但希望通过转发机制实际转发特定的选择器,则为这些选择器返回NO。

Dynamic Loading

Objective-C程序可以在运行时加载和链接新的类和类别。新代码被合并到程序中,并与开始时加载的类和类别相同。

动态加载可以用来做很多不同的事情。例如,系统首选项应用程序中的各个模块是动态加载的。

在Cocoa环境中,通常使用动态加载来定制应用程序。其他人可以编写程序在运行时加载的模块,就像Interface Builder加载自定义调色板和OSX系统首选项应用程序加载自定义首选项模块一样。可加载模块扩展了应用程序的功能。他们以你所允许的方式为之做出贡献,但却无法预料或定义你自己。您提供框架,但其他人提供代码。

尽管有一个运行时函数可以在Mach-O文件(objc_loadModules,在objc/objc load.h中定义)中执行Objective-C模块的动态加载,但是Cocoa的NSBundle类为动态加载提供了一个非常方便的接口,该接口面向对象并与相关服务集成。有关NSBundle类及其用法的信息,请参阅《基础框架参考》中的NSBundle类规范。有关Mach-O文件的信息,请参阅OS X ABI Mach-O文件格式参考。

Message Forwarding

向不处理该消息的对象发送消息是错误的。但是,在宣布错误之前,运行时系统会给接收对象第二次处理消息的机会。

Forwarding

如果将消息发送到不处理该消息的对象,则在宣布错误之前,运行时会向该对象发送一个forwardInvocation:message,其中NSInvocation对象作为其唯一参数,NSInvocation对象将封装原始消息及其传递的参数。

您可以实现forwardInvocation:方法来给消息提供默认响应,或者以其他方式避免错误。顾名思义,forwardInvocation:通常用于将消息转发到另一个对象。

要了解转发的范围和意图,请设想以下场景:首先,假设您正在设计一个可以响应名为negotiate的消息的对象,并且希望其响应包含另一种对象的响应。通过将协商消息传递给所实现的协商方法主体中的其他对象,可以很容易地完成此操作。

更进一步,假设您希望对象对协商消息的响应与在另一个类中实现的响应完全相同。实现这一点的一种方法是让您的类从另一个类继承该方法。然而,这样安排事情可能是不可能的。您的类和实现协商的类位于继承层次结构的不同分支中可能有很好的原因。

即使您的类不能继承协商方法,您仍然可以通过实现该方法的一个版本来“借用”该方法,该方法只需将消息传递给另一个类的实例:

- (id)negotiate
{
    if ( [someOtherObject respondsTo:@selector(negotiate)] )
        return [someOtherObject negotiate];
    return self;
}
           

这种方式可能会有点麻烦,特别是如果有很多消息需要对象传递给另一个对象。你必须实现一个方法来覆盖你想从另一个类借用的每个方法。此外,如果在编写代码时不知道要转发的完整消息集,则不可能处理这种情况。该集合可能依赖于运行时的事件,并且在将来实现新方法和类时可能会发生变化。

The second chance offered by a forwardInvocation: message provides a less ad hoc solution to this problem, and one that’s dynamic rather than static. It works like this: When an object can’t respond to a message because it doesn’t have a method matching the selector in the message, the runtime system informs the object by sending it a forwardInvocation: message. Every object inherits a forwardInvocation: method from the NSObject class. However, NSObject’s version of the method simply invokes doesNotRecognizeSelector:. By overriding NSObject’s version and implementing your own, you can take advantage of the opportunity that the forwardInvocation: message provides to forward messages to other objects.

转发调用提供的第二个机会:消息提供了一个不那么特别的解决方案,它是动态的,而不是静态的。它的工作原理如下:当对象无法响应消息,因为它没有与消息中的选择器匹配的方法时,运行时系统通过发送转发调用:消息通知对象。每个对象都从NSObject类继承一个forwardInvocation:方法。但是,NSObject的方法版本只是调用doesnotrecognitE选择器:。通过重写NSObject的版本并实现自己的版本,您可以利用forwardInvocation:message提供的机会将消息转发到其他对象。

要转发消息,forwardInvocation:方法只需执行以下操作:

  • 确定消息的位置,
  • 然后把它和它的原始参数一起送到那里。

可以使用invokeWithTarget:方法发送消息:

- (void)forwardInvocation:(NSInvocation *)anInvocation
{
    if ([someOtherObject respondsToSelector:
            [anInvocation selector]])
        [anInvocation invokeWithTarget:someOtherObject];
    else
        [super forwardInvocation:anInvocation];
}
           

转发的邮件的返回值将返回给原始发件人。所有类型的返回值都可以传递给发送方,包括ids、结构和双精度浮点数字。

forwardInvocation:方法可以充当未识别消息的分发中心,将它们分发给不同的接收方。或者它可以是一个中转站,将所有消息发送到同一个目的地。它可以将一条消息转换成另一条消息,或者简单地“吞下”一些消息,这样就不会有响应,也不会出错。forwardInvocation:方法还可以将多个消息合并到单个响应中。forwardInvocation:做什么取决于实现者。然而,它为链接转发链中的对象提供了机会,为程序设计提供了可能性。

注意:forwardInvocation:方法只有在消息没有调用名义接收方中的现有方法时才能处理它们。例如,如果您希望您的对象将协商消息转发到另一个对象,则它不能有自己的协商方法。如果是这样,消息就永远不会到达forwardI位置:。