本文内容来自苹果
Objective-C语言从编译时间和链接时间到运行时间都会推迟尽可能多的决策。只要有可能,它就会动态地完成任务。这意味着该语言不仅需要编译器,还需要运行时系统来执行编译后的代码。运行时系统充当Objective-C语言的一种操作系统; 这正是使语言发挥作用的原因。
本文将介绍NSObject该类以及Objective-C程序如何与运行时系统进行交互。特别是,它检查了在运行时动态加载新类的范例,并将消息转发给其他对象。它还提供了有关如何在程序运行时查找有关对象的信息的信息。
Objective-C运行时有两个版本 - “现代”和“传统”。Objective-C 2.0引入了现代版本,并包含许多新功能。Objective-C 1运行时参考中描述了旧版运行时的编程接口;Objective-C运行时参考中描述了现代版本运行时的编程接口。
1.在传统运行时中,如果更改类中实例变量的布局,则必须重新编译从其继承的类。
2.在现代运行时中,如果更改类中实例变量的布局,则不必重新编译从其继承的类。与运行时交互
Objective-C程序与运行时系统在三个不同的层次上进行交互:通过Objective-C源代码; 通过NSObject框架中定义的方法; 通过直接调用运行时功能。
Objective-C源代码:大多数情况下,运行时系统自动并在幕后工作。您只需编写和编译Objective-C源代码即可使用它。编译包含Objective-C类和方法的代码时,编译器将创建实现该语言动态特性的数据结构和函数调用。数据结构捕获在类和类别定义以及协议声明中找到的信息; 它们包括Objective-C编程语言中定义类和协议中讨论的类和协议对象,以及方法选择器,实例变量模板和从源代码提取的其他信息。主要运行时功能是发送消息的功能,如消息传送中所述。它由源代码消息表达式调用。
NSObject方法:
Cocoa中的大多数对象都是NSObject该类的子类,因此大多数对象都继承它定义的方法。(值得注意的例外是NSProxy类)因此,它的方法建立行为这是每个实例和每个类对象所固有的。然而,在少数情况下,这个NSObject类仅仅定义了一个模板,用来表示应该怎样做; 它不提供所有必要的代码本身。
例如,NSObject该类定义了一个description实例方法,该方法返回描述该类内容的字符串。这主要用于调试 -
GDB print-object命令打印从此方法返回的字符串。NSObject该方法的实现不知道该类包含什么,所以它返回一个字符串与对象的名称和地址。NSObject可以实现此方法的子类可以返回更多详细信息。例如,Foundation类NSArray返回它包含的对象的描述列表。
有些NSObject方法只是查询运行系统的信息。这些方法允许对象执行自身。这种方法的例子是class 方法,它要求一个对象来识别它的类; isKindOfClass: 和 isMemberOfClass:,它测试对象在继承层次结构中的位置;
respondsToSelector:,它表示一个对象是否可以接受一个特定的消息;
conformsToProtocol:,它表明一个对象是否声称实现了特定协议中定义的方法; 和methodForSelector:,它提供了一个方法实现的地址。
运行时功能:
运行时系统是一个动态共享库,其公共接口由位于目录内的头文件中的一组函数和数据结构组成。其中许多函数允许您使用C来复制编译Objective-C代码时编译器的功能。这些功能使开发运行时系统的其他接口成为可能,并生成增强开发环境的工具; 在Objective-C编程时不需要它们。但是,在编写Objective-C程序时,有些运行时功能有时可能会很有用。所有这些函数都记录在Objective-C运行时参考中。
消息:
介绍如何将消息表达式转换为objc_msgSend函数调用,以及如何通过名称引用方法。然后它解释了如何利用objc_msgSend以及如果需要规避动态绑定。
objc_msgSend函数
在Objective-C中,消息直到运行时才会绑定到方法实现。编译器转换一个消息表达式,
[receiver message]
在消息功能中调用,objc_msgSend。该功能需要接收器 并将消息中提到的方法的名称(即方法选择器)作为其两个主要参数:
objc_msgSend(接收器,选择器)
任何参数传递的消息也交给objc_msgSend:
objc_msgSend(接收器,选择器,arg1,arg2,...)
消息传递功能可以完成动态绑定所需的一切:
它首先找到选择器的过程(方法实现)指的是。由于相同的方法可以通过不同的类别实现,所以它找到的确切过程取决于接收者的类别。
然后它调用过程,将接收对象(指向其数据的指针)以及为该方法指定的任何参数传递给过程。
最后,它传递过程的返回值作为它自己的返回值。
注意:编译器生成对消息传递函数的调用。你不应该直接在你写的代码中调用它。
消息传递的关键在于编译器为每个类和对象构建的结构。每个班级结构都包含这两个基本要素:
指向超类的指针。
一个类调度表。该表具有将方法选择器与它们识别的方法的类特定地址相关联的条目。该setOrigin::方法的选择器与(实现的过程)的地址相关联,setOrigin::该display方法的选择器与display地址关联,等等。
当一个新的对象被创建时,它的内存被分配,并且它的实例变量被初始化。对象的变量中的第一个是指向其类结构的指针。这个被调用的指针isa赋予对象对其类的访问权,并且通过该类来访问它所继承的所有类。
注意:尽管不是该语言的一部分,但isa该对象需要指针才能与Objective-C运行时系统一起使用。一个对象需要与“等价”struct objc_object(定义在objc/objc.h)结构定义的任何字段中。但是,如果您曾经很少需要创建自己的根对象以及继承自变量NSObject或NSProxy自动拥有该isa变量的对象。
这些类和对象结构的元素如图3-1所示。
图3-1Messaging Framework

当消息发送到对象时,消息传递函数会跟随对象的消息 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 [targetperformSelector:method];
}
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语言本身的一个特征。
动态方法解析:介绍如何动态地提供方法的实现。
动态方法解析
在某些情况下,您可能想动态地提供方法的实现。例如,Objective-C声明的属性功能(请参阅Objective-C编程语言中的声明属性)包含以下指令:@dynamic
@dynamic propertyName;
它告诉编译器与该属性关联的方法将被动态地提供。
您可以实现这些方法resolveInstanceMethod:,resolveClassMethod:并分别为实例和类方法的给定选择器动态提供实现。
Objective-C方法只是一个C函数,它至少需要两个参数 - self和_cmd。您可以使用该函数将函数添加到类中作为方法class_addMethod。因此,给出以下功能:
void dynamicMethodIMP(id
self,SEL _cmd){
//执行....
}
你可以像这样resolveThisMethodDynamically使用一个方法(叫做)来动态地将它添加到类中resolveInstanceMethod:
@implementation MyClass
+(BOOL)resolveInstanceMethod:(SEL)aSEL
{
if(aSEL ==@selector(resolveThisMethodDynamically)){
class_addMethod([self class],aSEL,(IMP)dynamicMethodIMP,“v @:”);
return YES;
}
return [super resolveInstanceMethod:aSEL];
}
@end
转发方法(如消息转发中所述)和动态方法解决方案在很大程度上是正交的。一个类有机会在转发机制启动之前动态地解析一个方法。如果respondsToSelector:或者instancesRespondToSelector:被调用,动态方法解析器有机会IMP首先为选择器提供一个。如果你实现了,resolveInstanceMethod:但希望特定的选择器通过转发机制实际转发,则返回NO这些选择器。
动态加载
一个Objective-C程序可以加载和链接新的类和类别而它正在运行。新代码被合并到程序中,并且在开始时加载的类和类别的处理方式相同。
动态加载可以用来做很多不同的事情。例如,系统偏好设置应用程序中的各个模块都是动态加载的。
在Cocoa环境中,动态加载通常用于允许应用程序进行自定义。其他人可以编写程序在运行时加载的模块 - 就像Interface Builder加载自定义调色板一样,OS X系统偏好设置应用程序加载自定义首选项模块。可加载模块扩展了您的应用程序可以执行的操作。他们以您允许的方式为其作出贡献,但无法预期或定义您自己。您提供了框架,但其他人提供了代码。
虽然有一个运行时功能可以在Mach-O文件中执行Objective-C模块的动态加载(objc_loadModules,定义objc/objc-load.h),可可的NSBundle类为动态加载提供了一个非常方便的接口 - 一个面向对象并与相关服务集成的接口。有关NSBundle该类NSBundle及其用法的信息,请参阅Foundation框架参考中的类规范。请参阅OS X ABI Mach-O文件格式参考了解有关Mach-O文件的信息。
消息转发
将消息发送给不处理该消息的对象是一个错误。但是,在宣布错误之前,运行时系统为接收对象提供第二次处理消息的机会。
转发
如果您向未处理该消息的对象发送消息,则在宣布错误之前,运行时会向对象发送forwardInvocation:消息NSInvocation对象作为唯一的参数 - NSInvocation对象封装了原始消息和与之一起传递的参数。
你可以实现一个forwardInvocation:方法对消息给出默认响应,或以其他方式避免错误。顾名思义,forwardInvocation:通常用于将消息转发给另一个对象。
要看到转发的范围和意图,想象下列情况:首先,假设您设计的对象可以响应所调用的消息negotiate,并且您希望其响应包含另一种对象的响应。您可以通过将negotiate消息传递给negotiate您实施的方法体内的其他对象来轻松完成此操作。
更进一步,假设您希望对象对negotiate消息的响应完全是另一个类中实现的响应。一种方法可以让你的类继承另一个类的方法。但是,这样安排事情可能是不可能的。为什么你的类和实现的类negotiate在继承层次结构的不同分支中可能有很好的理由。
即使你的类不能继承这个negotiate方法,你仍然可以通过实现一个只将消息传递给另一个类的实例的方法来“借”它:
- (id)negotiate
{
if ( [someOther ObjectrespondsTo:@selector(negotiate)] )
return [some OtherObjectnegotiate];
return self;
}
这种做事的方式可能会变得麻烦一些,尤其是当你想让对象传递给其他对象的许多消息时。您必须实施一种方法来涵盖您想从其他课程借用的每种方法。此外,在你编写代码的时候,你不可能处理你不知道的情况,你可能想要转发的全部消息。该集合可能取决于运行时的事件,并且可能随着未来实现新的方法和类而改变。
forwardInvocation:消息提供的第二次机会为这个问题提供了一个不太临时的解决方案,并且是一个动态而非静态的解决方案。它的工作原理如下:当对象由于没有与消息中的选择器匹配的方法而无法响应消息时,运行时系统会通过发送forwardInvocation:消息来通知对象。每个对象都forwardInvocation:从NSObject类继承一个方法。然而,NSObject该方法的版本只是简单地调用doesNotRecognizeSelector:。通过覆盖NSObject版本并实现自己的版本,您可以利用forwardInvocation:消息提供的机会将消息转发给其他对象。
要转发消息,所有的forwardInvocation:方法需要做的是:
确定消息的去向,并且
用它的原始参数在那里发送。
该消息可以使用以下invokeWithTarget:方法发送:
-(void)forwardInvocation:(NSInvocation *)anInvocation
{
if ([someOthe ObjectrespondsToSelector: [anInvocationselector]])
[anInvocation invokeWithTarget:someOtherObject];
else
[super forwardInvocation:anInvocation];
}
被转发的消息的返回值将返回给原始发件人。所有类型的返回值都可以传递给发送者,包括ids,结构和双精度浮点数。
一种forwardInvocation:方法可以充当未识别消息的分发中心,将它们分包给不同的接收者。或者它可以是一个中转站,将所有消息发送到相同的目的地。它可以将一条消息翻译成另一条消息,或者简单地“吞下”一些消息,因此没有响应,也没有错误。一种forwardInvocation:方法还可以将多个消息合并成单个响应。什么forwardInvocation:是实现者。但是,它提供的链接转发链中对象的机会为程序设计提供了可能性。
注:该forwardInvocation:方法获取处理信息,只有当他们没有在标称接收器调用现有的方法。例如,如果您希望对象将negotiate消息转发给另一个对象,则它不能拥有negotiate自己的方法。如果是这样,该消息将永远达不到forwardInvocation:。
有关转发和调用的更多信息,请参阅NSInvocationFoundation框架参考中的类规范。
转发与多重继承
转发模仿继承,并可用于将多重继承的某些效果提供给Objective-C程序。如图5-1所示,通过转发消息来响应消息的对象似乎借用或“继承”了另一个类中定义的方法实现。
图5-1转发

在这个例子中,Warrior类的negotiate一个实例将消息转发给Diplomat类的一个实例。战士似乎会像外交官一样进行谈判。它似乎对这个negotiate信息作出了回应,并且为了所有实际目的,它确实做出了回应(尽管它确实是一名执行该工作的外交官)。
因此转发消息的对象从继承层次的两个分支- 它自己的分支和响应该消息的对象的分支“继承”方法。在上面的例子中,看起来好像Warrior类继承自Diplomat以及它自己的超类。
转发提供了您通常希望从多重继承中获得的大部分功能。然而,两者之间有一个重要的区别:多重继承在单个对象中组合了不同的功能。它倾向于大型,多面的物体。另一方面,转发将单独的责任分配给不同的对象。它将问题分解成更小的对象,但将这些对象以对消息发送者透明的方式关联起来。
代理对象
转发不仅可以模拟多重继承,还可以开发代表或覆盖更多实体对象的轻量级对象。代理人代表另一个对象并向其发送消息。
代理在“Objective-C编程语言 ”中的 “远程消息传送”中有所讨论。代理负责将消息转发到远程接收方的管理细节,确保参数值在连接中被复制和检索,等等。但它并不试图做其他事情; 它不会复制远程对象的功能,而只是给远程对象一个本地地址,这是一个可以在另一个应用程序中接收消息的地方。
其他种类的代理对象也是可能的。例如,假设你有一个处理大量数据的对象 - 也许它会创建一个复杂的图像或读取磁盘上文件的内容。设置这个对象可能会非常耗时,所以您宁愿懒惰地去做 - 当它真的需要时或系统资源暂时闲置时。同时,为了使应用程序中的其他对象正常工作,您至少需要此对象的占位符。
在这种情况下,你最初可以创建,而不是完整的对象,但它是一个轻量级的替代品。这个对象可以自己做一些事情,比如回答关于数据的问题,但是大多数情况下它只是为更大的对象提供一个位置,并且在时间到了时将消息转发给它。当代理的forwardInvocation:方法首次接收到发往另一个对象的消息时,它将确保该对象存在并且如果没有则会创建该消息。对于较大对象的所有消息都通过代理,所以就程序的其余部分而言,代理和较大的对象将是相同的。
转发和继承
虽然转发模仿继承,NSObject班级从不会混淆两者。类似的方法respondsToSelector:和isKindOfClass:看只能在继承层次,从来没有在转发链。例如,如果Warrior对象被询问是否响应negotiate消息,
if ( [aWarrior respondsToSelector:@selector(negotiate)] )
...
答案是NO,尽管它可以毫无错误地接收negotiate消息,并在某种意义上将它们转发给外交官。(见图5-1)
在很多情况下,NO答案是正确的。但它可能不是。如果使用转发来设置代理对象或扩展类的功能,则转发机制应该与继承一样透明。如果你希望你的对象像他们真正继承了这个行为一样行事他们将消息转发到的对象中,您需要重新实现respondsToSelector:以及isKindOfClass:包含转发算法的方法:
-(BOOL)respondsToSelector:(SEL)aSelector
{
if ( [super respondsToSelector:aSelector] )
return YES;
else {
/ *在这里,测试aSelector消息是否可以*
*被转发到另一个对象,无论这*
*对象可以响应它。如果可以,返回YES。* /
}
return NO;
}
除了respondsToSelector:和isKindOfClass:,该instancesRespondToSelector:方法还应该反映转发算法。如果使用协议,则conformsToProtocol:同样应该将该方法添加到列表中。同样,如果一个对象转发它接收到的任何远程消息,它应该有一个版本methodSignatureForSelector:可以返回最终响应转发消息的方法的准确描述; 例如,如果一个对象能够将消息转发给它的代理,你可以methodSignatureForSelector:按如下方式实现:
-(NSMethodSignature*)methodSignatureForSelector:(SEL)selector
{
NSMethodSignature*signature = [super methodSignatureForSelector:selector];
if (!signature) {
signature = [surrogate methodSignatureForSelector:selector];
}
return signature;
}
您可能会考虑将转发算法放在私密代码中,并且包含所有这些方法forwardInvocation:。
注意: 这是一种先进的技术,仅适用于没有其他解决方案的情况。它不打算作为继承的替代品。如果您必须使用此技巧,请确保您完全理解正在进行转发的班级以及您要转到的班级的行为。