Objective-C 2.0的运行时编程

Objective-C 2.0 的运行时环境叫做Morden Runtime,iOS 和Mac OS X 64-bit 的程序都运行在

这个环境,也就是说Mac OS X 32-bit 的程序运行在旧的Objective-C 1.0 的运行时环境Legacy

Runtime,这里我们只讲解Morden Runtime。

同运行时交互主要在三个不同的地方,分别是A.Objective-C 源码(譬如:你定义的Category

中的新方法会在运行时自动添加到原始类)、B.NSObject 的方法(isMemberClassOf 等动态判

定的方法)、C.运行时函数。由于前两者在第一篇文档中讲解过,这里我们讲一下运行时函

数的相关内容。



(1.)isa指针:

NSObject 中有一个Class isa 的指针类型的成员变量,因为我们的对象大都直接或者间接的从

NSObject 继承而来,因此都会继承这个isa 成员变量,isa 在运行时会指向对象的Class 对象,

一个类的所有对象的Class 对象都是同一个(JAVA 也是如此),这保证了在内存中每一个类

型都有唯一的类型描述。这个Class 对象中也有个isa 指针,它指向了上一级的父类的Class

对象。



在明白了这个isa 之后,你就可以明白在继承的时候,A extends B,你调用A 的方法a(),首

先A 的isa 到A 的Class 对象中去查找a()方法,找到了就调用,如果没找到,就驱使A 的Class

对象中的isa 到父类B 的Class 对象中去查找。



(2.)SEL 与IMP:

第一篇文档中,我们提到了方法选择器SEL,它可以通过如下两种方式获得:

(SEL) @selector(方法的名字)

(SEL) NSSelectorFromString(方法的名字的字符串)

另外,你还可以通过(NSString*) NSStringFromSelector(SEL)函数来获取SEL 所指定的方法名称

字符串。



其实Objective-C 在编译的时候,会依据每一个定义的方法的名字、参数序列,生成一个唯

一的整数标识,这个标识就是SEL。因此,在运行时查找方法都是通过这个唯一的标识,而

不是通过方法的名字。



Objective-C 又提供了IMP 类型,IMP 表示指向实现方法的指针(函数指针),通过它,你可

以直接访问一个实现方法,从而避免了[xxx message]的静态调用方式,需要首先通过SEL 确

定方法,然后再通过IMP 找到具体的实现方法,最后再发送消息所带来的执行效率问题。

一般,如果你在多次循环中反复调用一个方法,用IMP 的方式,会比直接向对象发送消息

高效一些。



例:

Person.m:

#import "Person.h"

@implementation Person

@synthesize name;

@synthesize weight;



-(Person*) initWithWeight: (int) w

{

self=[super init];

if (self)

{

weight=w;

}



return self;

}



-(void) print: (NSString*) str

{

NSLog(@"%@ %@",str,name);

}



-(void) dealloc

{

[self setName:nil];

[super dealloc];

}



@end







main.m:

int main (int argc, const char * argv[])

{

NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];

Person *person=[[Person alloc] initWithWeight:68];

person.name=@"Jetta";



SEL print_sel=NSSelectorFromString(@"print:");

IMP imp=[person methodForSelector: print_sel];

imp(person,print_sel,@"*********");



[pool drain];

return 0;

}



这里我们看到要获得IMP 的指针,可以通过NSObject 中的methodForSelector: (SEL)方法,访

问这个指针函数,我们使用imp(id,SEL,argument1,… …),第一个参数是调用方法的对象,第

二个方法是方法的选择器对象,第三个参数是可变参数,表示传递方法需要的参数。



(3.)objc_msgSend函数:

通过isa 指针的讲解,我们知道Objective-C 中的方法调用是在运行时才去绑定的,再进一步

看,编译器会把对象消息发送[xxx method]转换为objc_msgSend(id receiver,SEL selector,参数…)

的函数调用。因此上面例子中的print 方法你也可以像下面这样调用:



objc_msgSend(person,print_sel,@"++++++++");



当然,这是编译器要做的事情,你在写代码的时候,是不需要直接使用这种写法的。

综合isa、SEL、IMP 的讲解,实际上objc_msgSend 的调用过程就应该是这样的:



A.首先通过第一个参数的receiver,找到它的isa 指针,然后在isa 指向的Class 对象中使用

第二个参数selector 查找方法;



B.如果没有找到,就使用当前Class 对象中的新的isa 指针到上一级的父类的Class 对象中查

找;



C.当找到方法后,再依据receiver 的中的self 指针找到当前的对象,调用当前对象的具体实

现的方法(IMP 指针函数),然后传递参数,调用实现方法。



D.假如一直找到NSObject 的Class 对象,也没有找到你调用的方法,就会报告不能识别发送

消息的错误。









(4.)动态方法解析:



我们在Objective-C 2.0 的新特性中的属性访问器一节中,实际忽略了一个内容,那就是动态

属性。Objective-C 2.0 中增加了@dynamic 指令,表示变量对应的属性访问器方法,是动态实

现的,你需要在NSObject 中继承而来的+(BOOL) resolveInstanceMethod:(SEL) sel 方法中指定

动态实现的方法或者函数。



例:

Person.h:

@interface Person : NSObject

{

NSString *name;

float weight;

}



@property (retain,readwrite) NSString* name;

@property (readonly)float weight;

@property float height;



-(Person*) initWithWeight: (int) weight;

-(void) print: (NSString*) str;

@end





Person.m:

void dynamicMethod(id self,SEL _cmd,float w)

{

printf("dynamicMethod-%s\n",[NSStringFromSelector(_cmd) cStringUsingEncoding:NSUTF8StringEncoding]);

printf("%f\n",w);

}



@implementation Person

@synthesize name;

@synthesize weight;



@dynamic height; // 注意这里

// 在实现类中使用了@dynamic指令





-(Person*) initWithWeight: (int) w

{

self=[super init];

if (self)

{

weight=w;

}



return self;

}



-(void) print: (NSString*) str

{

NSLog(@"%@%@",str,name);

}



+(BOOL) resolveInstanceMethod: (SEL) sel

{

NSString *methodName=NSStringFromSelector(sel);

BOOL result=NO;



//看看是不是我们要动态实现的方法名称

if ([methodName isEqualToString:@"setHeight:"])

{

class_addMethod([self class], sel, (IMP) dynamicMethod,"v@:f");

result=YES;

}



return result;

}



-(void) dealloc

{

[self setName:nil];

[super dealloc];

}



@end



这里我们对于接口中的height在实现类中使用了@dynamic指令,紧接着,你需要指定一个函

数或者其他类的方法作为height的setter、getter方法的运行时实现。为了简单,我们指定

了Person.m中定义的函数(注意这是C语言的函数,不是Objective-C的方法)dynamicMethod

作为height的setter方法的运行时实现。被指定为动态实现的方法的dynamicMethod的参数

有如下的要求:



A.第一个、第二个参数必须是id、SEL;

B.第三个参数开始,你可以按照原方法(例如:setHeight:(float))的参数定义。



再接下来,你需要覆盖NSObject 的类方法resolveInstanceMethod,这个方法会把需要动态

实现的方法(setHeight:)的选择器传递进来,我们判断一下是否是需要动态实现的选择器,

如果是就把处理权转交给dynamicMethod。如何转交呢?这里我们就要用到运行时函数



class_addMethod(Class,SEL,IMP,char[])。



运行时函数位于objc/runtime.h,正如名字一样,这里面都是C 语言的函数。按照这些函数

的功能的不同,主要分为如下几类:操作类型、操作对象、操作协议等。大多数的函数都可

以通过名字看出是什么意思,例如:class_addProtocol 动态的为一个类型在运行时增加协议、

objc_getProtocol 把一个字符串转换为协议等。具体这些运行时函数都是做什么用的,你可

以参看Apple 官方页面:

http://developer.apple.com/library/ios/documentation/Cocoa/Reference/ObjCRuntimeRef/Reference/reference.html#//apple_ref/doc/uid/TP40001418



言归正传,我们来解释一下这里需要用到的class_addmethod 方法,这个方法有四个参数,

Class 表示你要为哪个类型增加方法,SEL 参数表示你要增加的方法的选择器,IMP 表示你要

添加的方法的运行时的具体实现的函数指针。其实在这里你能够看出SEL 并不能在运行时找

到真正要调用的方法,IMP 才可以真正的找到实现方法的。



在讲解第四个参数char[]之前,我们先看一下第一篇文档中提到的@encode 指令,在把任意

非Objective-C 对象类型封装为NSValue 类型的时候使用到了@encode 指令,但当时我们没

有详细说明这个指令的含义。实际上@encode()可以接受任何类型,Objective-C 中用这个指

令做类型编码,它可以把任何一个类型转换为字符串,譬如:void 类型被编码之后为v,对

象类型为@,SEL 类型为:等,具体的你可以参看Apple 官方页面关于Type Encoding 的描述:

http://developer.apple.com/library/ios/documentation/Cocoa/Conceptual/ObjCRuntimeGuide/Articles/ocrtTypeEncodings.html#//apple_ref/doc/uid/TP40008048-CH100-SW



现在我们来正式的看以下第四个参数v@:f 的含义,它描述了IMP 指向的函数的描述信息,

按照@encode 指令编译之后的字符说明,第一个字符v 表示返回值为void,剩余的字符为

dynamicMethod 函数的参数描述,@表示第一个参数id,:自然就是第二个参数SEL,f 就是

第三个参数float。由于前面说过动态方法的实现的前两个参数必须是id、SEL,所以第四个

参数中的字符串的第二、三个字符一定是@:。



我们看到resolveInstanceMethod 方法的返回值为BOOL,也就是这个方法返回YES 表示找到

了动态方法的具体实现,否则就表示没有在运行时找到真实的实现,程序就汇报错。

经过了上面的处理,Objective-C 的运行时只要发现你调用了@dynamic 标注的属性的setter、

getter 方法,就会自动到resolveInstanceMethod 里去寻找真实的实现。这也就是说你在

main.m 中调用peson.height 的时候,实际上dynamicMethod 函数被调用了。



实际上除了@dynamic 标注的属性之外,如果你调用了类型中不存在的方法,也会被

resolveInstanceMethod 或者resolveClassMethod 截获,但由于你没有处理,所以会报告不能

识别的消息的错误。



你可能在感叹一个@dynamic 指令用起来真是麻烦,我也是研究了半天Apple 官方的晦涩的

鸟语才搞明白的。不过好在一般Objective-C 的运行时编程用到的并不多,除非你想设计一

个动态化的功能,譬如:从网络下载一个升级包,不需要退出原有的程序,就可以动态的替

换掉旧的功能等类似的需求。



(5.)消息转发:

在前面的objc_msgSend()函数的最后,我们总结了Objective-C 的方法调用过程,在最后一步

我们说如果一路找下来还是没有找到调用的方法,就会报告错误,实际上这里有个细节,那

就是最终找不到调用的方法的时候,系统会调用-(void) forwardInvocation: (NSInvocation*)

invocation 方法,如果你的对象没有实现这个方法,就调用NSObject 的forwardInvocation 方

法,那句不能识别消息的错误,实际就是NSObject 的forwardInvocation 抛出来的异常。



我们这里告诉你这个系统内部的实现过程,实际是要告诉你,你可以覆盖forwardInvocation

方法,来改变NSObject 的抛异常的处理方式。譬如:你可以把A 不能处理的消息转发给B

去处理。



NSInvocation 是一个包含了receiver、selector 的对象,也就是它包含了向一个对象发送消息

的所有元素:对象、方法名、参数序列,你可以调用NSInvocation 的invoke 方法将这个消息

激活。

例:

main.m:

int main (int argc, const char * argv[])

{

NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];

Person *person=[[Person alloc] init];

person.name=@"Jetta";

[person fly];

[person release];

[pool drain];

return 0;

}



这里我们调用了一个Person 中不存在的方法fly。

Bird.m:

#import "Bird.h"

@implementation Bird

-(void) fly

{

printf("Bird Can fly!");

}

@end



Person.m

@implementation Person

@synthesize name;

@synthesize weight;



-(NSMethodSignature*) methodSignatureForSelector:(SEL)selector

{

//首先调用父类的方法

NSMethodSignature *signature=

[super methodSignatureForSelector: selector];



//如果当前对象无法回应此selector,那么selector构造的方法签名必然为nil

if (!signature)

{

//首先判断Bird的实例是否有能力回应此selector

if ([Bird instancesRespondToSelector:selector])

{

//获取Bird的selector的方法签名对象

signature=[Bird instanceMethodSignatureForSelector:selector];

}

}



return signature;

}



-(void) forwardInvocation: (NSInvocation*) invocation

{

//首先验证Bird是否有能力回应invocation中包含的selector

if ([Bird instancesRespondToSelector:[invocation selector]])

{

//创建要移交消息响应权的实例bird

Bird *bird=[Bird new];



//激活invocation中的消息,但是消息的响应者是bird,而不是默认的self。

[invocation invokeWithTarget:bird];

}

}



-(void) dealloc

{

[self setName:nil];

[super dealloc];

}

@end

下面我们来详细分析一下如果你想把不能处理的消息转发给其他的对象,需要经过哪个几个

步骤:

A.首先,你要覆盖NSObject中的methodSignatureForSelector方法。这是因为你如果想把消

息fly从Person转发给Bird处理,那么你必须将NSInvocation中包含的Person的fly的方法签

名转换为Bird的fly的方法签名,也就是把方法签名纠正一下。

由此,你也看出来NSInvocation的创建,内部使用了两个对象,一个是receiver,一个是

NSMethodSignature,而NSMethodSignature是由SEL创建的。NSInvocation确实存在一个类方

法invocationWithMethodSignature返回自身的实例。



B.然后我们覆盖forwardInvocation方法,使用的不是invoke方法,而是invokeWithTarget方法,

也就是把调用权由self转交给bird。



实际上消息转发机制不仅可以用来处理找不到方法的错误,你还可以变相的实现多继承。假

如我们的Person 想要拥有Bird、Fish 的所有功能,其实你可以尽情的用Person 的实例调用

Bird、Fish 的方法,只要在Person 的forwardInvocation 里,把消息的响应权转交给Bird 或者

Fish 的实例就可以了。不过这种做法实在有点儿BT,除非万不得已,否则千万不要这么做,

但是你也从这里能够看出来Objective-C 这种语言有多么的灵活、强大,这是JAVA 所完全不

能相比的。