简介

Objective c 语言尽可能的把决定从编译推迟到链接到运行时。只要可能,它就会动态的处理事情。这就意味着它不仅仅需要一个编译器,也需要一个运行时系统来执行变异好的代码。运行时系统就好像是Objective c 的操作系统。它让Objective c能工作。

这篇文章主要讲解NSObject类和Objective c如何于运行时系统交互。特别是,解释如何在运行时动态的加载新类,以及把消息转发给其他对象。同时也提供了在运行时如何获得对象信息的相关内容。

这里可以让你理解Objective c 运行时系统以及怎么使用它。了解一下总有好处的。

运行时的版本和平台

版本分为Legacy 和Modern版本。Modern 版本是在Objective C 2.0引入的。最值得注意的是

  • Legacy版本, 如果更改了一个类的属性,需要重新编译所有继承自它的类。
  • Modern版本,如果更改了一个类的属性,不需要重新编译继承自它的类。

iPhone程序以及OS X v10.5以后的64位程序使用Modern版本

其他程序(OS X上的32位程序)使用legacy版本

和运行时交互

Objective C 和运行时交互可以在三个层次:通过Objective C源代码;通过Foundation framework中NSObject类中定义的方法;直接调用运行时方法。

Objective C 源代码

大多数时候,运行时系统在幕后自动的工作。你只是通过书写和编译Objective C源代码来使用它。

当编译包含Objective c类和方法的代码时,编译器会生成实现了这个语言动态属性的数据结构。这个数据结构包含类和category以及protocol中的信息。运行时最主要的方法是分发消息,它在源代码的消息表达式中被调用。

NSObject 方法

Cocoa中大多数对象都是NSObject的子类,所以大多数对象都继承了它定义的方法。(值得注意的例外是NSProxy类)因此NSObject中方法建立的行为被每一个实例和类继承了。然而,在有些情况,NSObject仅仅为方法应该怎么执行定义一个模版。并不提供所有的代码。

例如,NSObject类定义了一个description方法来返回类的字符串描述。这个主要用来调试(GDB的print-object命令会打印这个方法返回的字符串)。NSObject并不知道这个类包含什么,所以它返回了包含名字和地址的字符串。NSObject的子类可以实现这个方法来返回更多信息。例如,NSArray返回了它包含的对象的列表。

有一些NSObject方法仅仅向运行时获取信息。例如class方法(它让一个对象可以识别它的类),还有isKindOfClass:, isMemberOfClass:(它可以查看对象的继承层次结构),respondsToSelector:(它可以查看一个对象是否接收特定的消息),conformsToProtocol:(它可以查看是否申明实现一个特从的protocol),methodForSelector:(它提供方法的实现地址)。这类的方法让对象可以查看自己的信息。

运行时方法

运行时系统包含一个动态库,在/usr/include/objc文件夹下有一些它公开的接口和数据结构。很多方法都可以在写Objective C代码时用纯C来重写编译器做的事情。还有一些是NSObject暴露出来的方法的基础。这些方法可以用来开发运行时的其他接口以及一些优化开发环境的工具。在Objective C开发的时候并不需要,然而,有些运行时方法偶尔在写Objective C时会很有用。

消息

这里将要说消息表达式是如何转化为objec_msgSend方法调用的,以及如何通过名字引用方法。如果使用objc_msgSend的优点,(如果需要的话,以及如何避开动态绑定)。

objc_msgSend 方法。

在Objective C中,消息直到运行时才和方法绑定。编译器把一个消息表达式[receiver message]转化成一个objc_msgSend消息调用。这个方法在消息中包含接受者和名字,他们是两个主要的参数objc_msgSend(reveiver, selector)。其他参数也会被处理objc_msgSend(receiver, selector, arg1, arg2, ...)

这个消息方法做了动态绑定所有的事情:

  • 首先它找到selector指向的代码段。由于同样的方法在不同的类中可以不同的实现,精确的找到代码段依赖于类的reveiver.
  • 然后调用代码段,传递方法中的指定的参数。
  • 最后,把代码段的返回值作为返回值返回。

注意:编译器会生成调用消息方法的效用,不能直接在代码中调用。

这个结构中关于消息线的关键在于,每个类结构都有两个关键的元素:

  • 一个指向父类的指针
  • 一个dispatch table。这个表把方法和方法入口地址关联起来。

当一个新的对象创建后,会给它分配内存,实例变量也会被初始化。对象的第一个变量是一个指向类结构的指针,叫做isa,它让对象可以进入它的类,通过这个类,然后到所有它继承的类。

注意:虽然不是严格规定,但是isa指针在和Objective C运行时系统交互时是必须有的。一个对象要有struct objc_object结构。然而,很少需要创建自己的根对象,继承自NSObject或NSProxy的对象自动有isa

提到的类和对象的结构如下图

当一条消息被发送给对象时,消息方法会通过isa到类结构中来查找dispatch 表。如果找不到方法,objc_msgSend会根据指针到父类,然后到它的dispatch table中查找。查找失败会导致objc_msgSend沿着类继承机构向上爬,直到遇到NSObject类。只要方法被找到,方法就会被调用并传递对应的参数。

这就是在运行时选择方法的方式。也就是, 消息的动态绑定。

为了加快消息处理的速度,运行时系统会缓存selector和方法的地址。每个类有一个单独的缓存,它也缓存了继承自父类但是在本类中定义了的方法。在开始查找dispatch tables前,首先会查看接收类的缓存(理论上方法被用了一次很有可能就会被再次用到)。如果方法被缓存了,方法只会比直接调用慢一点点。只要程序已经被运行足够长时间来预热缓存,几乎所有的方法都会在缓存中。缓存在运行中会随着新消息来动态增长。

使用隐藏的参数

当objc_msgSend找到了实现方法的代码段,它会调用它并且把消息中所有的参数都传给它。也会传递两个隐藏的参数

  • 接收的对象
  • 方法的selector

这些参数为每个方法实现提供了消息表达式的准确信息。他们被称为隐藏是因为他们没有在定义方法的代码中定义,他们是在编译是插入到实现中的。

虽然他们没有明确的声明,代码仍然可以引用他们(和引用接收对象的变量一样)。一种引用接收对象的方法叫self,当前的selector为_cmd。下面的例子中,_cmd指向strange方法的selector,self指向接收strange方法的对象。

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

self在这两个参数中更有用一些,可以通过self访问对象的实例变量。

获取方法地址

唯一的绕开动态绑定的方法是拿到方法的地址然后直接调用它。这在很少情况下会用到,当一个方法需要被执行很多次时并且你想节省消息dispatch的时间可以用到。

用NSObject中的methodForSelector:方法,可以获得一个指向方法实现代码段的指针,然后使用这个指针调用代码。methodForSelector:返回的指针必须要很小心的转化为方法的类型。返回值和参数类型都要匹配。

下面的例子展示了如何使用setFilled:

void (* setter)(id, SEL, BOOL);
int i; setter = (void (*)(id, SEL, BOOL))[target methodForSelector:@selector(setFilled:)]; for( i = ; i < ; i ++)
setter(targetList[i], @selector(setFilled:), YES);

传递的前两个参数是接收对象(self)和方法的selector(_cmd)。这些参数在方法的愈发中是隐藏的,但是在直接作为function调用时需要明确的传递。

使用methodForSelector:来避免动态绑定节省了消息分发的大部分时间。然而,这个只有在一个方法被多次调用时才明显。就像上面的for循环里面一样。

methodForSelector:由CoCoa运行时系统提供的,并不是Objective C的一个特性。

动态方法

这里将讲述如何动态的实现方法。

动态方法实现

有些情况下,可能需要动态的提供方法的实现。例如,Objective C 声明属性的时候包含@dynamic标识。它会告诉编译器这个属性相关的方法会动态实现。

你可以实现resoliveInstanceMethod:和resolveClassMethod:来为实例和类动态的提供方法实现。

一个Objective C方法是一个包含至少两个参数(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, "v@:");
return YES;
}
return [super resolveInstanceMethod:aSEL];
} @end

一个类在转发机制命中前有机会动态的处理方法。如果respondsToSelector:或instancesRepondToSelector:被调用,动态方法就有机会给selector提供一个IMP。如果实现了resolveInstanceMethod:,但是不想处理一些特定的方法,可以对这些selector返回NO。

动态加载

Objective C 程序可以在运行时加载链接新程序。新的代码注册到程序后会和程序启动时加载的类和category一样的对待。

动态加载可以做很多事情。比如,系统设置程序中的各种模块就是动态加载的。

消息转发

把消息发送给一个不能处理的对象会导致错误,在抛出错误之前,运行时让接收对象有第二次处理消息的机会。

转发

如果把消息发送给了一个不能处理它的对象,在抛出错误之前,运行时把以NSInvocation作为唯一参数发送一个forwardInvocation:消息给这个对象。(NSInvocation对象包含原始消息以及传递的参数)。

你可以实现forwardInvocation:方法来给消息一个默认的响应,或者用其他方法避免错误。就和它的名字一样,它主要用来转发消息给其他对象。

来看一下转发消息,假如有这样一个场景:首先,你设计了一个类可以响应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:方法需要:

  • 判断消息要转发给谁
  • 用原始的参数调用它。

可以用invokeWithTarget:方法来发送

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

消息的返回值会发送给原始的调用者。所有类型的返回值都可以返回,包括id,structure,numbers等。

forwardInvocation: 方法就像是不能识别消息的分发中心,把他们发给其他接受者。也可以把所有消息发到同一个目的地。它可以把消息传递给其他对象,也可以吞掉消息,这样就不会有返回值也没有报错。forwardInvocation:方法可以让很多消息返回同一个返回值。具体forwardinvocation:做了什么取决于你怎么实现它。反正,它提供了第二次转发消息的机会。

注意:forwardInvocation:方法只有在接受者没有处理的时候才会被调用。例如,如果你想要转发negotiate消息给其他对象,你自己就不能有negotiate方法。如果有,forwardInvocation:就不会被调用。

转发和多继承

转发有点像继承,它可以借用一些多继承的特性到Objective C里面来。如下图所示,一个对象通过借用或者“继承”另一个类中的方法来响应方法。

上图中,一个Warrior类的实例把negotiate消息传递给Diplomat类的实例。这样Warrior看起来就像是Diplomat。它看起来响应了negotiate消息,以及其他消息(虽然实际上是一个Diplomat在做事情)。

转发消息的对象就像是从继承结构的两个分支上响应消息(它自己的分支和响应消息的对象的分支)。上面的例子,就好像Warrior类继承自它的父类和Diplomat类。

消息转发提供了多继承的大部分特性。但是有一个重要的不同点:多继承把所有能做的事情组合到一个对象中。它会成很大,接口很多的对象。消息转发,另一方便说,把不同的任务分配给不同的对象。它把问题分配给更小的对象,然后用它的方式把这些对象和发送这联系起来。

代理对象

转发消息并不仅仅是多继承。它可以用来开发包含很多子对象的轻量级对象。代理对象为其他对象传递消息。

有些情况也适用于代理对象。假如你有一个对象要维护很多数据(也许是一个大图片也许是从硬盘上读大量文件),设置这样一个对象可以节省时间,你可以偷点懒来做这个事情(只有真的需要或者系统空闲的时候才做)。同时,你需要一个对象来处理这些接口这样程序才能够正常工作。

这种情况下,你不需要一开始就创建所有的对象,只需要创建一个轻量的代理对象。这个对象也可以做它自己的事,比如返回一些数据相关的信息,大多数时候它只需要知道它有一个大的对象,当需要的时候,可以给这个大对象传递消息。当forwardInvocation第一次收到传给其他对象的消息时,它会保证其他对象存在,如果不存在就创建它。所有和大对象相关的消息都通过代理对象传递。这样的话,其他对象使用代理对象和大对象是一样的。

转发和继承

虽然转发模仿继承,但是NSObject从来不会把他们弄混。像respondsToSelector:和isKindOfClass:只关心继承的接口,他们从来不会查看转发的结构。比如,一个Warrior对象被问道是否对negotiate消息响应

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

答案是NO,尽管它也能通过转发消息给Diplomat来响应negotiate消息。

大部分时候NO是正确的答案。有时候可能不是。如果使用代理对象来转发消息或者扩展一个类的功能,转发机制在继承结构上是透明的。如果你想要对象像真的是继承一样,你需要重写respondsToSelector:和isKindOfClass:方法来包含你的转发逻辑:

-(BOOL)respondsToSelector:(SEL)aSelector
{
if([super respondsToSelector:aSelector])
return YES;
else{
/*在这里查看消息是否要转发给其他对象,其他对象是否能响应这个消息,如果可以,返回YES*/
}
}

除了respondsToSelector:和isKindOfClass:,instancesRespondToSelector:方法也是转发方法的一个镜像。如果用到了protocol, conformsToProtocol:方法也应该被加上。同样的,如果一个对象转发任何收到的消息,它应该有一个methodSignatureForSelector:,它可以返回最终响应消息的准确描述。比如,如果一个对象可以把消息转发给surrogate,应该像下面一样实现methodSignatureForSelector:

-(NSMethodSignature *)methodSignatureForSelector:(SEL)selector
{
   NSMethodSignature * signature = [super methodSignatureForSelector:selector];
if(!signature){
signature = [surrogate methodSignatureForSelector:selector];
}
return signature;
}

你可以考虑把转发逻辑放到私有的代码中,并且包含 forwardInvocation:中所有的方法。

注意:这是一些高级技术,只有在没有其他方法时才适用。并不是用来替换继承的。你需要完全理解类的行为以及要转发给的类的行为

这里说到的当法的NSObject文档中有讲,更多信息查看invokeWithTarget:以及NSInvocation类

属性

当编译器遇到属性声明时,它会在类,category或protocol中生成相应的数据。你可以使用对应的方法来访问这些属性。

属性类型和方法

Property定义了一个不透明的属性指针。

typedef struct objc_property *Property;

可以使用class_copyPropertyList和protocol_copyPropertyList来获得类或protocol相关的属性数组。

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方法来属性的类型。

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 = ; i < outCount; i++) {
objc_property_t property = properties[i];
fprintf(stdout, "%s %s\n", property_getName(property), property_getAttributes(property));
}

更多信息,参考官方文档

Objective C Runtime 开发介绍的更多相关文章

  1. 【转载】Ssh整合开发介绍和简单的登入案例实现

    Ssh整合开发介绍和简单的登入案例实现 Ssh整合开发介绍和简单的登入案例实现 一  介绍: Ssh是strtus2-2.3.1.2+ spring-2.5.6+hibernate-3.6.8整合的开 ...

  2. OC多文件开发介绍

    OC多文件开发介绍: 1.为什么要使用多文件? 在工作中,通常把不同的类放到不同的文件中,每个类的声明和实现分开,声明写在.h头文件中,实现写在相应的.m文件中去,类名是什么,文件名的前缀就是什么.假 ...

  3. Java Web开发介绍

    转自:http://www.cnblogs.com/pythontesting/p/4963021.html Java Web开发介绍 简介 Java很好地支持web开发,在桌面上Eclipse RC ...

  4. Lucene.Net 2.3.1开发介绍 —— 四、搜索(三)

    原文:Lucene.Net 2.3.1开发介绍 -- 四.搜索(三) Lucene有表达式就有运算符,而运算符使用起来确实很方便,但另外一个问题来了. 代码 4.3.4.1 Analyzer anal ...

  5. Lucene.Net 2.3.1开发介绍 —— 四、搜索(二)

    原文:Lucene.Net 2.3.1开发介绍 -- 四.搜索(二) 4.3 表达式用户搜索,只会输入一个或几个词,也可能是一句话.输入的语句是如何变成搜索条件的上一篇已经略有提及. 4.3.1 观察 ...

  6. Lucene.Net 2.3.1开发介绍 —— 四、搜索(一)

    原文:Lucene.Net 2.3.1开发介绍 -- 四.搜索(一) 既然是内容筛选,或者说是搜索引擎,有索引,必然要有搜索.搜索虽然与索引有关,那也只是与索引后的文件有关,和索引的程序是无关的,因此 ...

  7. Lucene.Net 2.3.1开发介绍 —— 三、索引(七)

    原文:Lucene.Net 2.3.1开发介绍 -- 三.索引(七) 5.IndexWriter 索引这部分最后讲的是IndexWriter.如果说前面提到的都是数据的结构,那么IndexWriter ...

  8. Lucene.Net 2.3.1开发介绍 —— 三、索引(六)

    原文:Lucene.Net 2.3.1开发介绍 -- 三.索引(六) 2.2 Field的Boost 如果说Document的Boost是一条线,那么Field的Boost则是一个点.怎么理解这个点呢 ...

  9. Lucene.Net 2.3.1开发介绍 —— 三、索引(五)

    原文:Lucene.Net 2.3.1开发介绍 -- 三.索引(五) 话接上篇,继续来说权重对排序的影响.从上面的4个测试,只能说是有个直观的理解了.“哦,是!调整权重是能影响排序了,但是好像没办法来 ...

随机推荐

  1. SSH WebShell: SSH在线WEB管理器安装教程 - VPS管理百科

    SSH WebShell: SSH在线WEB管理器安装教程 - VPS管理百科 SSH WebShell: SSH在线WEB管理器安装教程 本站原创 [基于 署名-非商业使用-相同方式分享 2.5 协 ...

  2. (step 8.2.13)hdu 1524(A Chess Game)

    题目大意 : 在一个 有向无环图顶点上面有几个棋子, 2个人轮流操作, 每次操作就是找一个棋子往它能够移 动的地方移动一格, 不能操作的人输. 输入第一行 为一个 N , 表示有 N 个顶点 0 -& ...

  3. AbstractQueuedSynchronizer的介绍和原理分析(转)

    简介 提供了一个基于FIFO队列,可以用于构建锁或者其他相关同步装置的基础框架.该同步器(以下简称同步器)利用了一个int来表示状态,期望它能够成为实现大部分同步需求的基础.使用的方法是继承,子类通过 ...

  4. POJ 3652 &amp; ZOJ 2934 &amp; HDU 2721 Persistent Bits(数学 元)

    主题链接: PKU:http://poj.org/problem?id=3652 ZJU:http://acm.zju.edu.cn/onlinejudge/showProblem.do? probl ...

  5. Python数据结构-序表

    序表解包: list=['aa','bb','cc'] [a1,a2,a3]=list

  6. MySQL 全角转换为半角

    ​序言:       用户注冊时候,录入了全角手机号码,所以导致短信系统依据手机字段发送短信失败.如今问题来了,怎样把全角手机号码变成半角手机号码? 1.手机号码全角转换成半角先查询出来全角半角都存在 ...

  7. c#程序内存分配

    c#程序内存分配 进程可使用内存数就是操作系统给进程分配的最大地址,一般的32位操作系统提供给用户地址最大都是3g(操作系统自己保留1g),windows由于商业目的,对于个人用户只提供了2g地址,要 ...

  8. Extjs4.10Model模型具体解释

    一.创建Model模型 Extjs4.10提供了两种方法来创建Model模型,也就是创建类 方法一: Ext.define('person',{              extend:'Ext.da ...

  9. c++ 按行读取txt文本

    CStdioFile 类的声明保存在 afx.h 头文件中. CStdioFile 类继承自 CFile 类, CStdioFile 对象表示一个用运行时的函数 fopen 打开的 c 运行时的流式文 ...

  10. ecshop首页调用指定分类的所有产品(指定一级调二级)

    第一种方法 第一 在/includes/lib_goods.php下增加如下代码,用过网上的直接换掉就可以 function index_get_cat_id_goods_best_list($cat ...