本文源自https://developer.apple.com/library/ios/documentation/Cocoa/Conceptual/ObjCRuntimeGuide/ObjCRuntimeGuide.pdf
Objective-C 程序与runtime系统的交互有三个唯一的层次:通过Objective-C源代码的方式;通过NSObject类里面的方法;通过直接调用runtime函数。
Objective-C源代码
大部分情况,runtime系统自动工作并且隐藏在屏幕背后,编译器创建数据结构和函数调用(实现动态特性得语言)。数据结构捕获类或者category定义得信息和协议里面声明的东西;包括类和协议对象,同样包括selector,实例变量模版和源码中的其他信息。主要的runtime函数是发送消息。它会在源码发送消息是调用。
NSObject Methods
大部分Cocoa对象都是NSObject类的子类,所以大部分对象都继承了它定义的方法(NSProxy是一个例外)。每个继承了NSObject的对象和类都建立了这些继承来的行为。然而,在一些情况下,NSObject类很少定义一个模版一个事情该怎么做,它没有提供所有必须的代码。
比如,NSObject类定义了一个description的实例方法来返回一个描述类的内容的字符串。这主要用在调试过程中,在GDB print-object命令就是打印的description返回的字符串。NSObject’s 实现这个方法的是好并不知道类包括什么,所有它返回一个类的名字和对象的地址。NSObject子类能够实现这个方法并且返回更多的细节。比如,基础类NSArray返回了它所包含的对象的列表。
一些NSObject方法简单的向runtime系统查询一些信息。这些方法允许对象进行自省。典型的方法就是class方法,它要求对象定义它的类;isKindOfClass和isMenberOfClass测试对象在继承层次中的位置,responseToSelector指出一个对象是否接收一个特殊的消息。conformsToProtocol和methodForSelector提供了方式实现的地址。像这些方法提供给一个对象自己查看自己的能力。
Runtime 函数
runtime系统是一个动态共享库,有一个公共的接口包含一系列的函数和数据结构在/usr/include/objc的头文件中。他们中的许多函数允许你使用C来实现你写Objective-c代码时编译器做的事情。其他的形成了基本的功能输出通过NSObject类的函数。这些函数让开发runtime系统其他的接口和
扩大开发环境成为了可能。在用Objective-c编程时这些东西是不需要的。然而,一些runtime函数可能在一些场合比较有用。所有的函数都可以在Objective-c Runtime Reference中查到。
消息(Messaging)
本部分描述消息表达式是如何转化程objc_msgSend函数调用的,你是如何通过名字来调用函数的。然后解释你能通过objc_msgSend获取哪些优势,怎么做?当你需要时,你能够避免使用动态绑定。
objc_msgSend函数
在objective-c中,消息直到runtime时才绑定方法实现。编译器转化一个消息表达式[receiver message]成为一个消息函数的调用,objc_msgSend。这个函数把receiver和消息中提及的方法名(@selector(message))当作主要的参数:
objc_msgSend(receiver, selector);
传给消息的参数放在后面,如下:
objc_msgSend(receiver, selector, arg1, arg2,…);
消息函数完成所有的动态绑定必要的事情:
1. 它首先找到这个selector指向的处理过程。同样的方法名可以通过不同的类有不同的实现,准确的处理过程是取决于receiver的类。
2. 接着它调用这个处理过程,传给这个过程接收对象,还有指定的参数。
3. 最后,它把处理过程的返回值当作自己的返回值返回。
注意:编译器产生这个消息函数的调用。你不能指定在你的代码中调用。
消息依赖于编译器为每个对象和类构建的结构。每个类的结构包含下面两种本质的元素:
1. 一个指向父类的指针
2. 一个类的dispatch table。这个表的内容是方法的selectors关联到类定义的函数的地址的映射。@selector(setOrigin)关联setOrigin函数的地址,@selector(display)关联display的地址等等。
当一个对象被创建了,它会被分配内存,然后它的实例变量被初始化了。 对象的第一个变量是一个指向它类结构的指针。这个指针叫做isa,提供了一个对象访问它的类和它继承的所有的类。
注意:这严格来说不是语言的一部分,这个isa指针在对象需要与Objective-c runtime系统交互时使用。一个对象需要等价成一个objc_object的结构体这个结构定义的任何字段。然而,你很少,需要创建自己的根对象,所有继承NSObject或者NSProxy自动拥有isa变量。
类和对象的元素的结构如下图所示:
当一个消息被发送到一个对象时,消息函数跟着对象的isa指针到它的类结构的dispatch表中去查找方法selector。如果它在那里找不到selector,
objc_msgSend会跟着指向父类的指针在父类的dispatch表中查找这个方法的selector。失败的的话会导致
objc_msgSend一直查找到NSObject类。一旦它找到这个selector,函数就调用表中的这个方法并且传递结构的对象的数据结构。
这就是一种方式来达到方法实现的选择在runtime,或者说在面向对象编程的行话中说的,方法是动态绑定到消息的。
为了加速消息处理过程,runtime系统缓存它曾经使用过的selectors和方法的地址。这里为每个方法都有一个分离的缓存,它可以包含继承的方法的selectors就像这些方法在类里面定义的一样。在查找dispatch表之前,消息路由首先检查消息接收对象的缓存。如果方法selector在缓存中,消息只会比函数调用慢很少的一点点。一旦一个程序已经允许足够长时间来加热它的缓存,基本上它发送的所有的消息都能找到一个缓存的方法。程序运行时缓存动态的增长来适应新的消息。
使用隐藏对象
当
objc_msgSend找到了方法的处理过程,它调用这个处理过程并且传给它消息中所有的参数。它同时提供了两个隐藏的参数:
1. receive object
2. the selector for the method
这些参数提供给每一个方法实现明确的信息关于两个半它调用的消息表达式。他们是隐藏的是因为定义方法时他们不在源代码中声明。他们在代码编译时被插入到实现中去了。
尽管这些参数没有显示的声明,源代码仍然能够使用他们。方法用self来使用这个receiving对象,用_cmd来使用它自己的selector。在下面的例子中,_cmd指的是strange方法的selector,self指的是接收strange消息的对象。
- 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)和方法selector(_cmd)。这些参数隐藏在方法语法中但是在方法当作一个函数调用时必须显示给出。
使用methodForSelector:来避免动态绑定节约了大部分消息过程中的时间。然后,这些节约的时间只有在特定的消息被重复很多遍时才有意义,就像上面的for循环那样。
注意methodForSelector:是Cocoa runtime系统提供的,这不是Objective-C语言本身的。
动态方法解决方式
本部分描述了你如何动态的提供一个方法的实现。
你可以实现方法resolveInstanceMethod:和resolveClassMethod来动态的为一个给定的selector提供一个实现分别给实例和类方法。
一个Objective-C方法是一个C至少提供2个参数(self,_cmd)的函数。你可以使用函数class_addMethod添加一个函数给一个类方法。给定下面这个函数:
void dynamicMethodIMP(id self, SEL, _cmd) {
//implementation ...
}
你可以动态的将它当作一个方法添加到类中(类中的方法叫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];
}
传递方法和动态方法方案很大程度上是正交的。一个类有机会动态的解决一个方法在传递机制进入前。如果respondsToSelector:或者instancesRespondToSelector:被调用了,动态方法解决者有机会首先提供一个IMP给这个selector。如果你实现了resolveInstanceMethod但是希望特定的selectors实际上通过传递机制来传递时,你可以给这些selectors返回NO.
动态加载
一个Objective-C程序在运行时可以加载和连接新的classed和categories。新的代码加入到程序中并且被开始装载的类和categories同等的对待。
动态装载可以用来做许多不同的事情。比如系统偏好设置程序的不同的模块可以动态的加载。
在Cocoa环境中,动态加载通常用来允许程序被自定义。其他的人可以写一些模块你的程序可以在运行时加载。这些加载的模块可以扩展你的程序可以做的事情。它们用这样你允许的方式来做出贡献,你不用自己参与(anticipate)和定义。你提供框架,其他人提供代码。
尽管有一个runtime函数为Objective-C模块执行动态加载,Cocoa’s NSBundle 类提供了一个重要的更加方便的接口来进行动态加载,面向对象和相关服务的集成。
消息传递(Message Forwarding)
发送一个消息给一个不处理该消息的对象是一个错误。然后在宣布这个错误之前,runtime系统给这个接收消息的对象有第二次处理这个消息的机会。
传递(传递)
如果你发生一个消息给一个不处理这个消息的对象,在宣布这个错误之前runtime发送给这个对象一个forwardInvocation:消息,唯一的参数是NSInvocation对象,这个对象封装了原始的消息和需要传递给它的参数。
你可以实现forwardInvocation:方法来提供一个默认的响应给一个消息,或者用其他的方式来避免错误。就像它的名字表示的,forwardInvocation:通常用来将消息传递给另外一个对象。
查看这个范围和打算传递时,想像以下情景:假设,首先你设计一个对象可以响应叫做negotiate的消息,然后你希望这个响应包括另外一个类型对象的响应。你可以简单的完成这个任务,你在你实现的negotiate实现方法中传递一个negotiate消息给其他的对象来达到目的。
在次基础上更近一步,假设你希望你的对象能够响应一个negotiate消息恰好在另外一个类中实现了这个响应。一个方法来完成就是让你的类从其他的类中继承这个方法。然而,不太可能像这样安排事情。有很多原因为什么你的类和实现negotiate方法的类在不同的继承层次的分支上。
即使你的类不能继承这个negotiate方法,你仍然能借用它通过实现下面这个方法,简单的把消息传递给那个类的一个实例:
- (id) negotiate
{
if ([someOtherObject respondsTo:@selector(negotiate)])
{
return [someOtherObject negotiate];
}
return self;
}
这种方法来处理这个事情可能有一点不灵活,特别是当有很多消息你希望你的对象传递给其他对象时。你可能必须实现一个方法覆盖所有的你希望从其他对象借用的消息。而且,不太可能处理那些你还不知道的情况在你写代码时候,一个完整的你希望传递的消息集合。这个集合可能依赖runtime的事件,并且它可能会因为新方法和新类的实现而改变。
由forwardInvocation:提供的第二次机会来处理这样的问题,并且这是通过动态实现的。它工作的方式如下:当一个对象不能响应一个消息时因为它没有方法匹配消息中的这个selector,runtime系统发送forwardInvocation:消息通知对象。每一个对象都从NSObject类继承了forwardInvocation:方法。然而NSObject’s 的这个方法仅仅是简单的调用了doesNotRecognizeSelector:,通过你自己的代码重写这个方法,你可以获取这个机会,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:方法获得这个消息只有在不能调用receiver中已经存在的方法。比如,你希望你的对象传递negotiate消息给另外一个对象,你的对象不能由一个negotiate方法。如果它有了这个方法,消息永远不会到达receiver。
更多的forwarding和invocations信息,查看NSInvocation class清单。
转发和多继承
转发模仿继承,在Objective-C程序,可以用来借用一些多继承的效果。如下所示,一个对象通过借用或者继承另外一个类中定义的方法的实现通过转发来响应这个消息。
在这个描述中,一个Warrior类的实例转发一个negotiate消息给一个Diplomat类的实例。Warrior会像Diplomat一样响应negotiate。看起来就像响应了negotiate消息,实际目的它确实响应了。
对象转发了一个消息就像继承了方法重不同的继承层次的分支,它自己的分支和响应消息对象的分支。上面这个例子中,就像Warrior类继承自它的父类Diplomat。
转发提供了大部分的典型的需要多继承的特性。然而,两者中有一个重要的不同点:多继承合并不同的能力到一个的对象中。它趋向于导致大的,多面的对象。转发,相反的,赋予分开的能力给不同的对象。它分解问题成更小的对象,但是为消息发送者透明的关联这些对象。
代理对象(Surrogate Objects)
转发不仅仅是模仿多继承,它同时使开发轻量级的对象来表示或者覆盖更多重要的对象成为可能。代理代表了其他的对象并且为它们过滤消息。
在
The Objective-C Programming Language中讨论的Proxy就是这样的一个代理。一个proxy为远程接受者精心的管理转发消息的细节,确保参数值得到复制,通过链接进行检索等等。但是它没有打算做其他事情的意图;它不复制远程对象的功能,仅仅只是简单了给远程对象一个局部的地址,这个地址它可以接收其他对象来的消息。
还有其他类型的代理对象。假设,你有一个对象可以操作很多数据,可能它创建复杂的图片或者读取磁盘文件的内容。设置这个对象可能是耗时的,所以你可能懒惰的处理它,当它真的需要或者当系统资源临时空闲时才处理它。在这个时候,为了让应用中其他的对象功能正常你需要至少一个这个对象的占位符。
在这样的情形中,你可以初始化创建,但是不需要完整的充实这个对象,但是一个轻量级的代理。这个对象可以做一些事情,比如回答一些关于数据的问题,通常只是大对象的一个占位符,随着时间的推移,转发消息给它。当代理的forwardInvocation:方法首先接收到发往另一个对象的消息,它可能确保对象存在并且在不存在的时候创建它。大对象的所有的消息都通过代理,因此,对于程序的其他部分需要关注的,代理和这个大对象是一样的。
转发和继承
尽管转发模仿继承,NSObject类从来不会把它们搞混淆。像respondsToSelector:和isKindOfClass只有在继承层次中,不会在在转发链中。比如,一个
Warrior对象问它是否能响应一个negotiate消息,if [aWarrior respondsToSelector:@selector(negotiate)] ,它会返回NO,尽管它能接收negotiate消息并且能够响应它,通过转发它们给Diplomat。
在很多情况,NO是正确的回答,但是有时候不是。如果你使用转发来设置一个代理对象或者扩展类的能力,转发机制必须正确的像继承一样透明。如果你希望你的对象通过转发消息表现得就像继承对象得到的行为一样,你可能需要重新实现respondsToSelector:和isKindOfClass:方法来配合你的转发算法。
- (BOOL)respondsToSelector:(SEL)aSelector
{
if ([super respondsToSelector])
return YES;
else
// Here, test whether the aSelector message can be forward to another object .
return NO;
}
除了上面的两个方法,
instancesRespondToSelector:方法必须反映转发算法。如果协议被使用了,conformsToProtocol:方法必须同样的需要添加到列表中。类似的,如果一个对象转发任何它接收的远程消息,它必须有一个methodSignatureForSelector:来返回这个方法的正确的描述最后用来响应被转发的消息。比如,如果一个对象可以转发一个消息给它的代理,你必须实现
methodSignatureForSelector:如下:
- (NSMethodSignature*)methodSignatureForSelector:(SEL)selector
{
NSMethodSignature* signature = [super methodSignatureForSelector:selector];
if (!signature) {
signature = [surrogate methodSignatureForSelector:selector];
}
return signature;
}
你可能考虑把转发算法放在私有代码中,并拥有所有这些方法,包括forwardInvocation:。
注意:这是一个高级的技术,只有在没有其他解决方案时候才使用。它不打算取代继承。如果你必须使用这个技术,确保你已经完全理解了这些转发的行为和那些对象你用来转发的。
上面提到的方法在NSObject类中列出了。