运行时机制

来源:互联网 发布:js事件对象 编辑:程序博客网 时间:2024/06/06 07:48

和运行时系统的交互

Objective-C 程序有三种途径和运行时系统交互:通过 Objective-C 源代码;通过 Foundation 框架中类NSObject 的方法;通过直接调用运行时系统的函数。 

通过Objective-C源代码大部分情况下,运行时系统在后台自动运行,您只需编写和编译 Objective-C 源代码。

当您编译Objective-C类和方法时,编译器为实现语言动态特性将自动创建一些数据结构和函数。这些数据结构包含类定义和协议类定义中的信息,如在Objective-C 2.0 程序设计语言中定义类和协议类一节所讨论的类的对象和协议类的对象,方法选标,实例变量模板,以及其它来自于源代码的信息。运行时系统的主要功能就是根据源代码中的表达式发送消息,如"消息”一节所述。 

通过类NSObject的方法

Cocoa程序中绝大部分类都是NSObject类的子类,所以大部分都继承了NSObject类的方法,因而继承了NSObject的行为。(NSProxy类是个例外;更多细节参考““消息转发”一节。)然而,某些情况下,NSObject类仅仅定义了完成某件事情的模板,而没有提供所有需要的代码。 

例如,NSObject 类定义了 description 方法,返回该类内容的字符串表示。这主要是用来调试程序——GDB 中的 print-object 方法就是直接打印出该方法返回的字符串。NSObject 类中该方法的实现并不知道子类中的内容,所以它只是返回类的名字和对象的地址。NSObject 的子类可以重新实现该方法以提供更多的信息。例如,NSArray类改写了该方法来返回 NSArray 类包含的每个对象的内容。

某些 NSObject 的方法只是简单地从运行时系统中获得信息,从而允许对象进行一定程度的自我检查。例如,class 返回对象的类;isKindOfClass:和 isMemberOfClass:则检查对象是否在指定的类继承体系中;respondsToSelector:检查对象能否响应指定的消息;conformsToProtocol:检查对象是否实现了指定协议类的方法;methodForSelector:则返回指定方法实现的地址。

通过运行时系统的函数

运行时系统是一个有公开接口的动态库,由一些数据结构和函数的集合组成,这些数据结构和函数的声明头文件在/usr/include/objc中。这些函数支持用纯C的函数来实现和Objective-C同样的功能。还

有一些函数构成了 NSObject 类方法的基础。这些函数使得访问运行时系统接口和提供开发工具成为可能。尽管大部分情况下它们在 Objective-C 程序不是必须的,但是有时候对于 Objecitve-C 程序来说某些函数是非常有用的。 这些函数的文档参见 Objective-C2.0 运行时系统参考库。

消息

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

获得方法地址

避免动态绑定的唯一办法就是取得方法的地址,并且直接象函数调用一样调用它。当一个方法会被连续调用很多次,而且您希望节省每次调用方法都要发送消息的开销时,使用方法地址来调用方法就显得很有效。

利用 NSObject 类中的 methodForSelector:方法,您可以获得一个指向方法实现的指针,并可以使用该指针直接调用方法实现。methodForSelector:返回的指针和赋值的变量类型必须完全一致,包括方法的参数类型和返回值类型都在类型识别的考虑范围中。

下面的例子展示了怎么使用指针来调用 setFilled:的方法实现: 

void (*setter)(id, SEL, BOOL);

int i; 

setter = (void (*)(id, SEL, BOOL))[targetmethodForSelector:@selector(setFilled:)];

for ( i = 0; i < 1000, i++ )

setter(targetList[i],@selector(setFilled:), YES);

方法指针的第一个参数是接收消息的对象(self),第二个参数是方法选标(_cmd)。这两个参数在方法中是隐藏参数,但使用函数的形式来调用方法时必须显示的给出。

使用 methodForSelector:来避免动态绑定将减少大部分消息的开销,但是这只有在指定的消息被重复发送很多次时才有意义,例如上面的 for 循环。

注意,methodForSelector:是 Cocoa 运行时系统的提供的功能,而不是 Objective-C 语言本身的功能。

objc_msgSend函数

在 Objective-C 中,消息是直到运行的时候才和方法实现绑定的。编译器会把一个消息表达式 

[receiver message]

转换成一个对消息函数objc_msgSend的调用。该函数有两个主要参数:消息接收者和消息对应的方法名字——也就是方法选标:

同时接收消息中的任意数目的参数:

该消息函数做了动态绑定所需要的一切:

§ 它首先找到选标所对应的方法实现。因为不同的类对同一方法可能会有不同的实现,所以找到的方法实现依赖于消息接收者的类型。

§ 然后将消息接收者对象(指向消息接收者对象的指针)以及方法中指定的参数传给找到的方法实现。

§ 最后,将方法实现的返回值作为该函数的返回值返回。注意:编译器将自动插入调用该消息函数的代码。您无须在代码中显示调用该消息函数。

消息机制的关键在于编译器为类和对象生成的结构。每个类的结构中至少包括两个基本元素:

§ 指向父类的指针。

§ 类的方法表。方法表将方法选标和该类的方法实现的地址关联起来。例如,setOrigin::的方法选标和 setOrigin::的方法实现的地址关联,display 的方法选标和 display 的方法实现的地址关联,等等。

当新的对象被创建时,其内存同时被分配,实例变量也同时被初始化。对象的第一个实例变量是一个指向该对象的类结构的指针,叫做 isa。通过该指针,对象可以访问它对应的类以及相应的父类。

注意:尽管严格来说这并不是 Obective-C 语言的一部分,但是在 Objective-C 运行时系统中对象需要有isa 指针。对象和结构体 structobjc_object(在 objc/objc.h 中定义)必须“一致”。然而,您很少需要创建您自己的根对象,因为从 NSObject 或者 NSProxy 继承的对象都自动包括 isa 变量 

当对象收到消息时,消息函数首先根据该对象的isa指针找到该对象所对应的类的方法表,并从表中寻找该消息对应的方法选标。如果找不到,objc_msgSend 将继续从父类中寻找,直到 NSObject 类。一旦找到了方法选标, objc_msgSend 则以消息接收者对象为参数调用,调用该选标对应的方法实现。

这就是在运行时系统中选择方法实现的方式。在面向对象编程中,一般称作方法和消息动态绑定的过程。

为了加快消息的处理过程,运行时系统通常会将使用过的方法选标和方法实现的地址放入缓存中。每个类

都有一个独立的缓存,同时包括继承的方法和在该类中定义的方法。消息函数会首先检查消息接收者对象

对应的类的缓存(理论上,如果一个方法被使用过一次,那么它很可能被再次使用)。如果在缓存中已经

有了需要的方法选标,则消息仅仅比函数调用慢一点点。如果程序运行了足够长的时间,几乎每个消息都

能在缓存中找到方法实现。程序运行时,缓存也将随着新的消息的增加而增加

使用隐藏的参数 

当objc_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 更有用一些。实际上,它是在方法实现中访问消息接收者对象的实例变量的途径。 

动态方法解析

有时候,您需要动态地提供一个方法的实现。例如,Objective-C中属性(Property)(参考Objective-C 2.0程序设计语言中属性小节)前的修饰符@dynamic

表示编译器须动态地生成该属性对应地方法。

您可以通过实现 resolveInstanceMethod:和 resolveClassMethod:来动态地实现给定选标的对象方法或者类方法。 

Objective-C 方法可以认为是至少有两个参数——self 和_cmd—— 的 C 函数。您可以通过class_addMethod 方法将一个函数加入到类的方法中。例如,有如下的函数:

您可以通过 resolveInstanceMethod:将它作为类方法 resolveThisMethodDynamically的实现:

void dynamicMethodIMP(id self, SEL _cmd) {

// implementation ....

}

@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 Build 载入定制的调色板以及系统配置程序载入定制的模块的类似。 这些模块通过您许可的方式扩展了您的程序,而您无需自己来定义或者实现。您提供了框架,而其它的程序员提供了实现。

尽管已经有一个运行时系统的函数来动态加载Mach-O文件中的Objective-C模块(objc_loadModules,在objc/objc-load.h中定义),Cocoa的NSBundle类为动态加载提供了一个更方便的接口——一个面向对象的,已和相关服务集成的接口。关于NSBundle类的更多相关 

信息请参考Foundation框架中关于NSBundle类的文档。关于Mach-O文件的有关信息请参考Mac OS XABI Mach-O 文件格式参考库。

消息转发

通常,给一个对象发送它不能处理的消息会得到出错提示,然而,Objective-C 运行时系统在抛出错误之前,会给消息接收对象发送一条特别的消息来通知该对象。 

消息转发

如果一个对象收到一条无法处理的消息,运行时系统会在抛出错误前,给该对象发送一条

forwardInvocation:消息,该消息的唯一参数是个 NSInvocation 类型的对象——该对象封装了原始的消息和消息的参数。

您可以实现 forwardInvocation:方法来对不能处理的消息做一些默认的处理,也可以以其它的某种方式来避免错误被抛出。如 forwardInvocation:的名字所示,它通常用来将消息转发给其它的对象。

关于消息转发的作用,您可以考虑如下情景:假设,您需要设计一个能够响应 negotiate 消息的对象,并且能够包括其它类型的对象对消息的响应。通过在 negotiate 方法的实现中将 negotiate 消息转发给其它的对象来很容易的达到这一目的。

更进一步,假设您希望您的对象和另外一个类的对象对negotiate的消息的响应完全一致。一种可能的方式就是让您的类继承其它类的方法实现。然后,有时候这种方式不可行,因为您的类和其它类可能需要

在不同的继承体系中响应 negotiate 消息。虽然您的类无法继承其它类的negotiate方法,您仍然可以提供一个方法实现,这个方法实现只是简单

的将 negotiate 消息转发给其他类的对象,就好像从其它类那儿“借”来的现一样。如下所示:

- negotiate

{

if ( [someOtherObject respondsTo:@selector(negotiate)] )

return [someOtherObject negotiate];

return self;

}

这种方式显得有欠灵活,特别是有很多消息您都希望传递给其它对象时,您必须为每一种消息提供方法实

现。此外,这种方式不能处理未知的消息。当您写下代码时,所有您需要转发的消息的集合也必须确定。

然而,实际上,这个集合会随着运行时事件的发生,新方法或者新类的定义而变化。

forwardInvocation:消息给这个问题提供了一个更特别的,动态的解决方案:当一个对象由于没有相应的方法实现而无法响应某消息时,运行时系统将通过 forwardInvocation:消息通知该对象。每个对象都从 NSObject 类中继承了 forwardInvocation:方法。然而,NSObject 中的方法实现只是简单地调用了 doesNotRecognizeSelector:。通过实现您自己的 forwardInvocation:方法,您可以在该方法实现中将消息转发给其它对象。

要转发消息给其它对象,forwardInvocation:方法所必须做的有:§ 决定将消息转发给谁,并且

§ 将消息和原来的参数一块转发出去

消息可以通过 invokeWithTarget:方法来转发:

- (void)forwardInvocation:(NSInvocation *)anInvocation

{

if ([someOtherObject respondsToSelector:

[anInvocation selector]])

[anInvocation invokeWithTarget:someOtherObject];

else

[super forwardInvocation:anInvocation];

}

转发消息后的返回值将返回给原来的消息发送者。您可以将返回任何类型的返回值,包括: id,结构体,浮点数等。

forwardInvocation:方法就像一个不能识别的消息的分发中心,将这些消息转发给不同接收对象。或者它也可以象一个运输站将所有的消息都发送给同一个接收对象。它可以将一个消息翻译成另外一个消

息,或者简单的"吃掉“某些消息,因此没有响应也没有错误。forwardInvocation:方法也可以对不同的消息提供同样的响应,这一切都取决于方法的具体实现。该方法所提供是将不同的对象链接到消息链的能力。

更多消息转发的信息,参考 Foundation 框架参考库中 NSInvocation 类的文档 

消息代理对象

消息转发不仅和继承很象,它也使得以一个轻量级的对象(消息代理对象)代表更多的对象进行消息处理

成为可能。

Objective-C 2.0 程序设计语言中“远程消息”一节中的代理类就是这样一个代理对象。代理类负责将消息转发给远程消息接收对象的管理细节,保证消息参数的传输等等。但是消息类没有进一步的复制远程对象的功能,它只是将远程对象映射到一个本地地址上,从而能够接收其它应用程序的消息。

同时也存在着其它类型的消息代理对象。例如,假设您有个对象需要操作大量的数据——它可能需要创建一个复杂的图片或者需要从磁盘上读一个文件的内容。创建一个这样的对象是很费时的,您可能希望能推迟它的创建时间——直到它真正需要时,或者系统资源空闲时。同时,您又希望至少有一个预留的对象和程序中其它对象交互。

在这种情况下,你可以为该对象创建一个轻量的代理对象。该代理对象可以有一些自己的功能,例如响应

数据查询消息,但是它主要的功能是代表某个对象,当时间到来时,将消息转发给被代表的对象。当代理

对象的 forwardInvocation:方法收到需要转发给被代表的对象的消息时,代理对象会保证所代表的对象已经存在,否则就创建它。所有发到被代表的对象的消息都要经过代理对象,对程序来说,代理对象和被代表的对象是一样的。

消息转发和类继承 

尽管消息转发很“象”继承,但它不是继承。例如在 NSObject 类中,方法 respondsToSelector:和 isKindOfClass:只会出现在继承链中,而不是消息转发链中。例如,如果向一个Warrior 类的对象询问它能否响应 negotiate 消息,

返回值是NO,尽管该对象能够接收和响应negotiate。(见图 5-1。)

大部分情况下,NO 是正确的响应。但不是所有时候都是的。例如,如果您使用消息转发来创建一个代理对象以扩展某个类的能力,这儿的消息转发必须和继承一样,尽可能的对用户透明。如果您希望您的代理

对象看起来就象是继承自它代表的对象一样,您需要重新实现 respondsToSelector:和isKindOfClass:方法:

if ( [aWarrior respondsToSelector:@selector(negotiate)] )

...

- (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 = [supermethodSignatureForSelector:selector]; 

 if (!signature) {

    signature = [surrogatemethodSignatureForSelector:selector];

 } 

    return signature;

}

您也可以将消息转发的部分放在一段私有的代码里,然后从 forwardInvocation:调用它。

本节中涉及的方法在 Foundation 框架参考库中的 NSObject 类的文档中都有描述。关于invokeWithTarget:的具体信息,请参考 Foundation 框架参考库中 NSInvocation 类的文档。 

属性声明

当编译器遇到一个属性(Property)声明时(参考Objective-C 2.0 程序设计语言中的属性小节),编译器

将产生一些描述性的元数据与属性所在的类或者协议类关联。您可以通过函数访问元数据,这些函数支持

在类或者协议类中通过名字来查找,通过@encode获得属性的类型编码,将属性的特征(Attribute)作为C字符串的数组返回等。每个类或者协议类都维护了一个声明了的属性列表。 

属性类型和相关函数

属性(Property)类型定义了对描述属性的结构体 objc_property 的不透明的句柄。

您可以使用函数 class_copyPropertyList 和 protocol_copyPropertyList 来获得类(包括范畴类)或者协议类中的属性列表:

例如,有如下的类声明:

typedef struct objc_property *Property;

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

您可以象这样获得它的属性:

您还可以通过 property_getName 函数获得属性的名字:

函数 class_getProperty 和 protocol_getProperty 则在类或者协议类中返回具有给定名字的属性的引用:

id LenderClass = objc_getClass("Lender");

unsigned int outCount;

objc_property_t *properties = class_copyPropertyList(LenderClass, &outCount);

const char*property_getName(objc_property_t property)

objc_property_t class_getProperty(Class cls, const char *name)

objc_property_t protocol_getProperty(Protocol *proto, const char *name, BOOLisRequiredProperty, 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 函数将返回属性(Property)的名字,@encode 编码,以及其它特征(Attribute)。

§  property_getAttributes 返回的字符串以字母 T 开始,接着是@encode 编码和逗号。
§  如果属性有 readonly 修饰,则字符串中含有 R 和逗号。
§  如果属性有 copy 或者 retain 修饰,则字符串分别含有 C 或者&,然后是逗号。
§  如果属性定义有定制的 getter 和 setter 方法,则字符串中有 G 或者 S 跟着相应的方法名以及逗号
(例如,GcustomGetter,ScustomSetter:,,)。
如果属性是只读的,且有定制的 get 访问方法,则描述到此为止。
§  字符串以 V 然后是属性的名字结束 

 

 

0 0
原创粉丝点击