介绍
Objective-C将许多决策从便宜时期和链接时期延后到运行时期。只要可能,它都动态的做很多事情。这意味着它不仅需要一个编译器,还需要一个运行时系统来执行编译好的代码。对于Objective-C来说,这个运行时系统就好像一个操作系统,使objective-c能够正常工作。
本文探究NSObject类,以及Objective-C程序如何和运行时系统交互。
通过阅读本文,你应该理解Objective-C的运行时系统如何工作,以及如何利用它。尽管对于写一个Cocoa程序而言,你可能并不需要理解本文。
本文的组织
本文包括以下几章:
- 运行时系统的版本和平台
- 和运行时系统交互
- 发送消息
- 动态方法解析
- 消息转发
- 类型编码
- 声明的属性
参见
Objective-C运行时指南描述了Objective-C运行时库的数据结构和函数。你的程序可以使用这些接口和Objective-C运行时系统进行交互。例如,你可以对一个已经加载的类添加类和方法,或者获取类定义列表。
Runtime的版本和平台
在不同的平台上有很多不同的Objective-C的Runtime的版本。
过去和现代的版本
Objective-C Runtime有2个版本——现代版本和过去版本。现代版本是Objective-C 2.0引入的,包括一系列的新特性。过去版本的编程接口在《Objective-C 1 Runtime Reference》中进行了描述,现代版本的编程接口在《Objective-C Runtime Reference》。
最值得注意的新特性是在现代runtime中实例变量是“non-fragile”的:
- 在过去版本中,如果你改变一个类中实例变量的布局,你必须重新编译这个类的子类;
- 在现代版本中,如果你改变一个类的实例变量的布局,你不需要重新编译这个类的子类;
另外,现代runtime支持 declared properties 的实例变量合成(详情请参阅《The Objective-C Programming Language》)。
平台
iphone程序和运行在OSX v10.5及以上版本的64位的程序使用现代版的runtime,其它程序(OSX桌面的32位程序)使用过去版本的runtime。
和Runtime进行交互
Objective-C程序和Runtime的交互可以分为3个层次:通过Objective-C源代码;通过Foundation framework 的 NSObject类的方法;通过直接调用Runtime函数。
Objective-C 源代码
通常情况下,Runtime系统在屏幕背后自动运行,你仅仅通过写和编译Objective-C代码来使用它。当你编译带有Objective-C的类和方法的代码时,编译器会创建实现语言动态特性的数据结构和函数调用。这些数据结构会捕获类、category、protocol声明中的信息,包括我们在《Defining a Class and Protocols in The Objective-C Programming Language》中讨论的类和protocol对象、selector、实例变量模板,以及源代码中的其他内容。主要的Runtime函数是那些能发送消息的函数,我们会在Messaging一节重点讲解,它被源代码的消息表达式调用。
NSObject方法
Cocoa中大部分对象都是NSObject的子类,所以大部分对象继承了它的方法。(值得注意的一个例外是NSProxy类,详情见《消息转发》这一节。)因此它的方法的行为也能够被每一个对象和每一个类实例所继承。然而,在一些情况下,NSObject类仅仅定义了一个模板,并没有提供代码。
例如说,NSObject类定义了一个description实例方法,这个方法返回描述类内容的字符串。它主要用于调试——GDB的 print-object 会打印description方法返回的字符串。NSObject并不知道这个类的内容,因此它只返回名字和对象的地址,NSObject的子类能够返回更多细节。例如, NSArray的description方法返回它包括的对象的描述列表。
一些NSObject方法仅仅查询Runtime的信息。这些方法允许对象进行反省。一个例子是class方法,它 向对象询问它的类,还有isKindOfClass:方法和isMemberOfClass:方法,它们检测对象在类层次中的位置,respondsToSelector:检测对象是否响应某个消息,conformsToProtocol:检测对象是否实现了特定协议的方法。这些方法给予了对象自省能力。
Runtime方法
Runtime系统是一个动态共享库,它有公共接口,它的头文件位于 /usr/include/objc的文件夹里,有方法和数据结构的集合。当你写Objective-C代码时,这里很多方法允许你使用C代码重现编译器做的事情。其它方法形成了功能基础,这些功能通过NSObject类的方法导出。写Objective-C程序时,一些Runtime方法是很有用的。《Objective-C Runtime Reference》对这些所有的方法进行了介绍。
消息
这一章将会讲解消息传递怎样会转化为objc_msgSend方法调用,以及你怎样通过名字来引用方法,然后讲解你怎样利用objc_msgSend方法,以及在你需要的时候怎样避免动态绑定。
objc_msgSend方法
在Objective-C中,直到运行时消息才会被绑定到方法实现。编译器会把一个消息表达式
[receiver message]
转化为一次对消息函数——objc_msgSend的调用。objc_msgSend函数将消息的接收者和消息中的方法名作为参数。任何在消息中传递的参数也同样会被objc_msgSend处理:
objc_msgSend(receiver, selector, arg1, arg2, ...)
消息函数为动态绑定做了一切:
- 它首先找到选择器引用的过程(方法实现)。由于相同的方法也可以被不同的类实现,因此它最终找到的过程依赖于接收者的类。
- 然后它会调用这个过程,传给它接收的对象(指向它的数据的指针),以及这个方法需要的其它参数。
- 最后,它将过程的返回值作为自己的返回值。
注意:编译器会生成对消息函数的调用,你不应该在自己的代码中直接调用它。
发送消息的关键在于编译器为每个类和对象生成的结构,每个类结构包括2个基本元素:
- 指向superclass的指针。
- 类的分发表。这个表中的条目是方法的选择器和特定类的方法地址的映射。例如setOrigin::的选择器就和setOrigin::的实现的地址进行了关联,display的选择器和display的地址进行了关联,等等。
当一个新对象创建以后,内存会被分配,它的实例变量会被初始化。这其中有一个指针指向它的类结构,这个指针就是 isa 指针,使得对象能够访问它的类,并且通过这个类它能够访问它的所有父类。
注意:严格来说,isa指针并不是语言本身的一部分,但它是对象和Objective-C Runtime系统协作所必需的。对象需要和objc_object结构各个字段都相等。然而,你很少需要创建你自己的root对象,继承自NSObject和NSProxy的对象自动有一个isa变量。
当一个消息发送给一个对象时,消息函数随着isa指针在其类的消息分发表中查找方法选择器。如果找不到选择器,objc_msgSend就会跟随指向父类的指针并在其方法分发表中尝试查找选择器,如果一直失败objc_msgSend会沿着继承体系不断向上,直到到达NSObject类。一旦找到选择器,消息函数会调用该方法并将接收对象的数据结构传递给它。
这就是运行时选择方法实现的方式,也就是面向对象编程术语中方法被动态绑定到消息的方式。
为了加速消息的处理,Runtime系统会缓存选择器和使用过的方法地址。每个类都有一个独立的缓存,它能存储从父类继承来的方法以及自己独特的方法的选择器。在搜索分发表之前,消息路由会检查接收消息的对象的类的缓存(理论上如果一个方法被使用过,那么它可能会被再次使用)。如果方法选择器在缓存中,那么这个消息处理仅仅比方法调用慢一点点。一旦一个程序运行很长时间去逐渐建立这个缓存,那么几乎所有的消息都会找到一个缓存的方法。随着程序的运行,缓存会动态增长,以容纳新的消息。
使用隐藏参数
当objc_msgSend发现实现一个方法的过程时,它会调用这个过程,并通过消息传递给它所有的参数,同时也会传递2个隐藏参数:
- 接收对象
- 方法选择器
这些参数会给方法实现关于和两部分的明确的信息,之所以说是隐藏,是因为他们没有在方法的源代码中明确的声明。它们是在编译的时候插入实现中的。
尽管这些参数没有被明确声明,源代码仍然能够引用他们(就好像它能够引用接收对象实例变量一样)。一个方法用self来引用接收对象,用_cmd来引用选择器。在下面的例子中,_cmd引用stange方法的选择器,self引用接收stange消息的对象。
- strange
{
id target = getTheReceiver();
SEL method = getTheMethod();
if ( target == self || method == _cmd )
return nil;
return [target performSelector: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 runtime系统提供的,不是Objective-C语言的特性。
动态方法解析
本章讲解如何为一个方法动态提供实现。
动态方法解析
在某些场合你可能希望动态地给一个方法提供实现,例如说Objective-C语言的@dynamic关键字:
@dynamic propertyName;
它告诉编译器这个方法将会被动态提供。
你能够实现resolveInstanceMethod:和resolveClassMethod: 分别为一个实例对象和类动态地提供方法。
一个Objective-C方法是一个至少包括2个参数——self和_cmd的C函数。你可以利用函数class_addMethod将一个函数添加到类中,成为类的方法。对于如下函数:
void dynamicMethodIMP(id self, SEL _cmd) {
// implementation ....
}
你可以使用resolveInstanceMethod:把它添加类中成为类的方法,例如:
@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:或者instancesRespondToSelector: ,首先动态解析器可以为选择器提供一个IMP。如果你实现了 resolveInstanceMethod:但只想要特定的选择器被通过转发机制转发,你可以针对那些不想转发的选择器返回NO。
动态加载
Objective-C程序能够在运行时加载和链接新的类和类别(catogery)。新代码将和程序开始时加载的类和类别一样很好的融入到程序中。
动态加载被用来做很多不同的事情。例如在系统偏好中的那些模块就是动态加载的。
在Cocoa环境中,动态加载通常被用来允许程序定制化。你的程序在运行时可以加载别人写的一些模块——就好像Interface Builder加载定制的工具箱以及OSX系统偏好程序加载定制化偏好模块。
被加载的程序能够扩展原来程序的功能。他们以一种你允许但又不能预期和定义它的行为的方式提供这些模块。你定义框架,他们实现代码。
尽管在Mach-O文件(objc_loadModules, 在objc/objc-load.h 中定义)中有方法执行Objective-C模块的动态加载,Cocoa的NSBundle类为动态绑定提供了一个更方便的接口——它是面向对象的并且和相关类集成。参阅Foundation framework reference的NSBundle类的说明以获取NSBundle类及其用法。关于Mach-O的信息请参阅《OS X ABI Mach-O File Format Reference 》。
消息转发
向一个对象发送它不能处理的消息会发生错误,然而,在声明出错之前,运行时系统给接收消息的对象
一个机会来处理消息。
转发
当你向一个对象发送它不能处理的消息时,在它声明出错之前,runtime会给它发送forwardInvocation: 消息,这个消息有唯一的参数———一个NSInvocation对象,NSInvocation对象封装了消息本身以及消息带的参数。
你可以实现 forwardInvocation: 方法来给出一个该消息的默认响应,或者通过其他方式避免错误的产生。就像它的字面意思一样,forwardInvocation: 通常用来将消息转发给其它对象。
关于转发的范围和意图,想象下面的场景:首先,假设你设计了一个对象,能够响应 negotiate 消息,你想要它的响应包括另外一个对象的响应。你可以很轻松的实现:你只需要在你实现的 negotiate 方法中将 negotiate 消息传递给另外的对象。
更进一步,假设你想要你的对象对于 negotiate 消息的响应在其他类中实现的。一个办法是使你的对象的类继承自其他类,然而这有时候不可能,例如你的类和实现 negotiate 的类在不同的继承体系中。
尽管你的类不能继承 negotiate 方法,你仍然可以通过实现一个方法,在这个方法中仅仅需要将消息传递给那个类对象,来“借用”这个 negotiate 方法。
- (id)negotiate
{
if ( [someOtherObject respondsTo:@selector(negotiate)] )
return [someOtherObject negotiate];
return self;
}
这种方式有点麻烦,尤其是当你有很多消息要转发给其他对象的时候。你必须去实现一个方法去覆盖每一个你想要从其他对象那里“借”的方法。然而,想要覆盖每一个消息,处理每一种情况是不可能的,而且它依赖于运行时时间,它可能会随着以后实现的方法和类而改变。
forwardInvocation: 提供了一种特别的解决方案,它是动态而非静态的。当一个对象由于缺少匹配消息选择器的方法而不能响应消息的时候,运行时系统会给它发送一个 forwardInvocation: 消息。每一个对象都从NSObject类继承了forwardInvocation: 方法。然而,这个方法的NSObject版本仅仅调用了doesNotRecognizeSelector: 方法。通过重写NSObject版本的这个方法,你就可以利用forwardInvocation: 消息提供的时机将你的消息转发给其他对象。
要转发一个消息,forwardInvocation:需要做的是:
1. 决定消息应该流向哪里;
2. 将原始参数传递给它
这个消息必须要能被invokeWithTarget: 发送:
- (void)forwardInvocation:(NSInvocation *)anInvocation
{
if ([someOtherObject respondsToSelector:
[anInvocation selector]])
[anInvocation invokeWithTarget:someOtherObject];
else
[super forwardInvocation:anInvocation];
}
被转发消息的返回值会返回给原始的发送者。所有类型的返回值都能够被传递给发送者,包括ids、结构和双精度浮点型数字。
forwardInvocation: 的作用就好像一个无法识别的消息的分发中心,将消息分发给不同的接收者。或者是一个转化中心,发送所有的消息到相同的目的地。它能将一个消息翻译成另外一个,也能够“吞噬”消息,没有响应也没有错误。forwardInvocation:也能将很多消息合并成一个响应。forwardInvocation: 如何执行取决于实现者。它提供的这样的机制使得在转发链中链接对象成为了可能。
注意:只有当接收者没有调用已有方法时forwardInvocation: 才会处理消息。也就是说如果你想让你的对象转发negotiate消息给其他对象,那么它就不能有一个negotiate方法。如果它有的话,消息将永远不能到达forwardInvocation:。
更多关于消息转发和调用的信息,请参与《 Foundation framework reference》中NSInvocation类的说明。
转发和多重继承
转发模拟继承,转发能够借用一些多重继承的效果。一个对象通过转发响应了一个消息,就好像从其它对象那里借用或者继承了一个方法实现一样。
在上图中,一个Warrior对象转发negotiate消息给一个Diplomat对象,Warrior对象就好像Diplomat对象一样,看起来它能够响应negotiate消息了,在所有实际情况下它都能够响应(尽管它是一个Diplomat对象)。
转发消息的对象从而从继承体系的两个分支——它自己所在的分支和消息响应者所在的分支“继承”了这个方法。在上面的例子中,看起来好像Warrior对象同时继承自Diplomat类和它自己的超类。
消息转发提供了你想要从多重继承获得的大多数特性。然而两者有一个显著地不同:多重继承将不同的功能组合在一个对象中,它侧重于大、多层面的对象。而转发侧重于将不同的能力分配给不同的对象。它将问题分解为小对象,以一种对消息发送者透明的方式将这些对象联系起来。
代理对象
转发不仅模仿多重继承,而且也使得开发代表或覆盖更多实体对象的轻量级对象成为可能。代理站在另外那个对象的角度,并且向它传递消息。
代理使得向远程对象转发消息成为可能,参数可以正确的拷贝和获取。但它不会拷贝远程对象的功能,仅仅给远程对象一个本地地址,通过这个本地地址它能够接收消息。
其他类别的代理对象也是可能的。例如你有一个对象,这个对象有大量数据——它可能创建了一个复杂的图像或者读取了磁盘上文件的内容。创建这个对象很耗时,因此你可能倾向于稍后创建——当需要它的时候或者系统资源闲置的时候。同时你需要这个对象的占位符,以便程序中的其他对象可以正常工作。
在这种情况下,你可能开始不会创建一个完整的对象,而是一个轻量级的代理。这个对象能够自己处理一些事情,例如回答关于数据的问题,但是它主要的功能还是作为这个大对象的占位符,以及在合适的时候转发消息。当这个代理对象的forwardInvocation: 方法接收到一个发给其他对象的消息时,它会在该对象不存在是创建它以确保对象的存在。所有发给这个大对象的消息都经过这个代理,对象和代理是等价的。
转发和继承
尽管转发酷似继承,但是NSObject类绝不会把两者混淆起来。respondsToSelector: and isKindOfClass: 这两个方法仅仅存在于继承体系中,不会存在于转发链中。例如,一个Warrior对象是否响应 negotiate 方法的代码为:
if ( [aWarrior respondsToSelector:@selector(negotiate)] )
...
答案是否定的,尽管它能够接收 negotiate 消息并且做出反应,通过转发消息给Diplomat对象。
在很多情况下,否定是正确的答案。但也可能不是这样。如果你用转发去创建一个代理对象或者去扩展一个类,转发机制就和继承机制一样透明。如果你想要你的对象表现的好像它们真的继承了你要转发到的对象的行为的话,你就需要重新实现respondsToSelector:方法和isKindOfClass:方法以包含你的转发机制:
- (BOOL)respondsToSelector:(SEL)aSelector
{
if ( [super respondsToSelector:aSelector] )
return YES;
else {
/* Here, test whether the aSelector message can *
* be forwarded to another object and whether that *
* object can respond to it. Return YES if it can. */
}
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:已经包括了,请调用它。
注意:这是一个高级技术,仅适用于没有其他解决方案的情况。他不应该作为继承的替代品。如果你必须要使用该技术,请确认你已经完全理解了你要转发的类和转发到的类的行为。
本节提到的方法都在Foundation framework reference 的 NSObject 类的说明中有描述。关于invokeWithTarget: 的更多信息,请参考 Foundation framework reference 中 NSInvocation 的说明。
类型编码
(都是表格,暂不翻译,原文地址:Objective-C Runtime Programming Guide)
属性声明
当编译器遇到属性声明(参见《The Objective-C Programming Language》中的属性声明),它会为这个类、类别或者协议产生一些描述性的 metadata。你可以通过一些方法访问这些metadata,这些方法能够通过类或者协议的名字查询属性,获取属性的类型,以及拷贝属性的属性。属性声明的列表对每个类和协议都适用。
属性类型和方法
Property 结构定义了属性描述符的handle。
typedef struct objc_property *Property;
你可以使用 class_copyPropertyList 和 protocol_copyPropertyList 方法获取一个类、类别或者协议的属性列表:
objc_property_t *class_copyPropertyList(Class cls, unsigned int *outCount)
objc_property_t *protocol_copyPropertyList(Protocol *proto, unsigned int *outCount)
例如,给定如下类声明:
@interface Lender : NSObject {
float alone;
}
@property float alone;
@end
你可以这样获取属性列表:
id LenderClass = objc_getClass("Lender");
unsigned int outCount;
objc_property_t *properties = class_copyPropertyList(LenderClass, &outCount);
你可以使用 property_getName 来获取属性名:
const char *property_getName(objc_property_t property)
你可以用方法 class_getProperty 和 protocol_getProperty 获取一个类或协议的指定名字的属性的引用:
objc_property_t class_getProperty(Class cls, const char *name)
objc_property_t protocol_getProperty(Protocol *proto, const char *name, BOOL isRequiredProperty, BOOL isInstanceProperty)
你可以用 property_getAttributes 方法获取属性的名字和 @encode 类型的字符串。关于编码类型字符串的细节,请参考 《类型编码》章节,细节请参考 《属性类型字符串》和《属性类型描述举例》。
const char *property_getAttributes(objc_property_t property)
因此,你可以用下面这段代码打印一个类的属性列表:
id LenderClass = objc_getClass("Lender");
unsigned int outCount, i;
objc_property_t *properties = class_copyPropertyList(LenderClass, &outCount);
for (i = 0; i < outCount; i++) {
objc_property_t property = properties[i];
fprintf(stdout, "%s %s\n", property_getName(property), property_getAttributes(property));
}
属性类型字符串
你可以使用 property_getAttributes 方法来获取属性的名字、@encode类型字符串,以及属性的其他属性。
字符串以T开头,然后是 @encode 类型和逗号,最后是V和实例变量的名字。在这之中,属性由以下这些描述符指定:
表格7-1 属性类型编码
(表格不再翻译,原文地址:Objective-C Runtime Programming Guide)