简介
Objective-C将很多静态语言在编译和链接时做的事放到了运行时来处理。只要有可能,它做了一些动态。这意味着该语言需要不只是一个编译器,但也是一个运行时系统来执行的编译代码。运行时系统作为一种Objective-C操作系统的; 是什么使该语言工作。
本文着眼于NSObject类以及Objective-C程序的运行时系统交互。尤其是,在运行时动态加载新类,和转发消息到其它的对象。它还提供了有关如何可以找到有关对象的信息,当你的程序运行的信息。
你应该阅读这个文件来获得的Objective-C运行系统是如何工作的理解以及如何利用它。通常情况下,你写一个Cocoa 应用程序很少需要知道和理解这些。
1. 运行时版本和平台
Objective-C runtime 在不同平台上有不同的版本
1.1 Legacy and Modern Versions
Objective-C runtime 有两个版本—"modern"和"legacy"。modern版本在Objective-C 2.0时引入并且包含一些新特性。legacy版本接口描述在Objective-C 1.0文档中。modern版本的接口描述在Objective-C Runtime Reference。
最显著的新特点是,modern运行时的实例变量是"non-fragile"。
在legacy runtime模式下,如果你在类中的布局改变实例变量,你必须从它继承的类重新编译
在modern runtime模式下,如果你在类中的布局改变实例变量, 你不必从它继承的类重新编译
另外,modern runtime 支持实例变量合成为声明的属性。
1.2 Platforms
iPhone 应用程序和OX v10.5的64位程序以及后来的都是用了 modern runtime
其他的程序(OS X 桌面32位程序)使用的是legacy runtime.
3. Interacting with the Runtime
Objective-C 程序的运行时交互在三个不同层次:通过Objective-C源码;通过Foundation框架NSObject类中定义的方法;通过直接调用运行时的函数
3.1 Objective-C Source Code
在大多数情况下,运行时系统自动的工作。你可以仅仅通过编写和编译Objective-C源码
当你编译的代码中包含Objective-C类和方法,编译器会创建实现动态语言特性的数据结构和函数调用。数据结构获取类和类别的定义以及协议声明中找到的信息,它们包括在Objective-C语言中定义一个类和协议的对象,以及方法selectors, 实例变量模板,并且从源代码中提取其他信息。主要的运行时方法是发送消息,描述在Messaging。由源代码消息表达式调用。
3.2 NSObject Methods
在Cocoa中的大多数对象是NSObject的子类,所以大多数对象继承了它所定义的方法。(值得注意的是NSProxy类, 从Message Forwarding查看更多信息)。因此它的方法建立在每个固有的实例和对象。 然而,在少数情况下,NSObject类仅仅定了如何去做的模板,它不提供是有必要的代码本身。
例如,NSObject类定义了一个返回类内容描述字符串的实例方法description。这主要用于调试——GDB打印方法命令打印方法返回的字符串。NSObject对方法的实现并不知道类包含,因此他返回一个描述对象的名称和地址的字符串。NSObject的子类可以此实现方法的更多细节。例如,Foundation的类NSArray返回它所包含对象列表的描述。
一些NSObject方法简单地查询runtime system 的信息。这些方法允许对象进行自我检查。这类方法的实例是类方法,他要求一个对象以确定它的类,isKindOfClass: 和 isMemberOfClass:,它测试了类在继承层次对象的位置;respondsToSelector:,其指示对象是否接受一个特定的消息;conformsToProtocol:,其指示对象是否声明实现一个特定协议中实现的方法;methodForSelector:其提供了一种方法的实现的地址。这样的方法提供了对象內省得能力。
3.3 Runtime Functions
runtime system 是一个公共接口包含一组函数和数据结构的动态分享库,头文件目录位置:/usr/include/objc。许多函数允许用纯C去复制Objective-C 代码的编译器实现。其他基础功能可以通过Objective-C 类的方法来导出。这些功能使得能够在runtime系统中开发其他接口,并且增加开发环境的工具;在Objective-C编程时他们不需要。然而,当编写一个Objective-C 程序时的runtime 函数可能是有用的。所有这些功能文档在Objective-C Runtime Reference.
4. Messaging
这章描述消息表达式转换为objc_msgSend的函数调用,以及如何使用方法名来调用方法。它然后解释了如何使用objc_msgSend优势,以及如何绕过动态绑定---绕过需要的话。
4.1 The objc_msgSend Function
在Objective-C, 消息知道运行时才绑定方法的实现,编译器转换消息表达式:
[receiver message]
调用一个发送消息函数,objc_msgSend。该函数需要接收器和在消息中提到的函数名, 这个方法选择器——两个主要参数:
objc_msgSend(receiver, selector)
在消息中你也可以传递任何参数给objc_msgSend:
objc_msgSend(receiver, selector, arg1, arg2, ...)
消息传递函数做必要的动态绑定的一切:
它首先找到selector指向的程序(方法实现)。由于同样地方法能够通过单独类的不同方式实现,这个精确的过程依赖于类的接收器。
然后,它调用程序,通过它的接受对象(它的数据指针),以及用于该方法指定的任何参数。
最后,它通过程序的返回值作为自身的返回值
发送消息的关键在于编译器生成每个类和对象的结构体,每个类结构体包含 两个基本元素:
一个指向父类的指针
一个调度表。这个表具有特定类对该方法标识的地址及相关联的方法选择器条目,setOrigin::方法选择器关联setOrigin::方法地址(实现地址),选择器的显示方法与显示的地址相关联,等等。
当创建一个对象时,内存被分配,并且它的实例变量被初始化。首先在类结构中对象的变量实际上是一个指针。这个指针叫做isa,给了类的对象访问权限,并通过类,来找到所有他继承的类。
当一个消息被发送给你一个对象,这个消息传递函数遵循对象的isa指针的类结构,其查找调度表中的方法选择器。如果它不能在哪里找到方法选择器,objc_msgSend找到父类的指针,并试图找到其在调度表中的方法选择器。连续的失败引起objc_msgSend追溯类结构至到NSObject类。一旦定位到选择器,函数调用调度表中的方法,并传递结束对象的数据结构。
这种方法实现在运行时被选中,或者在面向对象编程里,即方法动态绑定到消息。为了加速消息处理过程,runtime system缓存了选择器与方法地址当被调用过。对于每一个类有一个单独的缓存,包含选择器和继承方法以及在类中定义的方法。在搜索调度表前,消息例程首先检查接受对象类的缓存(理论上一个方法一旦被调用后很大可能被再次调用)。如果在缓存中存在这个方法选择器,消息发送只比函数调用稍微慢一点。一旦一个程序以及运行足够长的时候去"唤醒"它的缓存,几乎所有发送的消息能够找到缓存的方法。缓存的动态增长以适应程勋运行的新信息。
4.2 Using Hidden Arguments
当objc_msgSend找到方法实现的过程中,他调用程序并且传递在消息中所有的参数。它还传递两个隐藏的参数
接受对象(The receiving object)
方法选择器(The selector for the method)
这些参数给每个方法实现关于方法表达式调用的准确信息。他们说的"hidden",因为他们没有定义在方法的源码中,而是代码编译时被插入进来的。
尽管这些参数没有被明确声明,源码任然可以指向他们(正如它可以指向接受对象的实例变量)。一个方法可以指向接收对象的本身,并且它自己的选择器_cmd。在下面的例子中_cmd指向strange方法的选择器,并且self接收strange消息。
- strange{
id target = getTheReceiver();
SEL method = getTheMethod();
if ( target == self || method == _cmd )
return nil;
return [target performSelector:method];
}
self是两个参数中比较有用的信息,实际上,这样接收对象实例变量使变量在方法中更精确。
4.3 Getting a Method Address
避免动态绑定的唯一方式是获取函数地址并且直接调用它,就像他是一个函数。在一个特定方法罕见的被多次执行的情况下是释放的,这样就避免了消息调用的每一次开销。
在NSObject类中定义的methodForSelector:方法,你可以获取一个指向实现方法的指针,然后用这个指针来调用程序。methodForSelector: 方法返回的指针必须强制转换为正确类型,无论在返回值还是在参数类型中都应包括。
下面的例子展示了thesetFilled:方法实现是怎么被调用的:
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 system 提供。它不是Objective-C语言本身提供的。
5. Dynamic Method Resolution
本章描述了如何提供一个方法的动态实现。
5.1 动态方法解析(Dynamic Method Resolution)
某些情况下可能需要提供一个方法的动态实现。例如,Objective-C的属性声明功能(参见Objective-C语言的属性声明)包括 @dynamic 指令:
@dynamic propertyName;
它告诉编译器,与属性关联的方法是动态的提供的。
你可以实现方法resolveInstanceMethod:和resolveClassMethod: 去为实例和类方法动态提供一个给定的程序实现。
一个Objective-C方法根本是至少有两个参数(self和_cmd)的C函数。你可以用class_addMethod方法给类方法添加一个功能,因此,给出以下功能:
void dynamicMethodIMP(id self, SEL _cmd) {
// implementation ....
}
你可以使用方法resolveInstanceMethod:向类中添加一个方法(方法 resolveThisMethodDynamically),如下:
@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对于这些。
5.2 Dynamic Loading
一个Objective-C在运行时可以载人并链接一个新类。新的代码并入程序和相同的处理,已在开始时加载雷和类别。
动态loading可以用来做很多不同的事情。例如在系统预置应用程序中的各种模块是动态加载。
在Cocoa环境下,动态载入通常运行应用程序进行定制。其他人的写的模块可以在运行时载入你的程序——就像Interface Builder载入自定义调色板和OS X系统加载自定义的喜好模块。可扩展模块可以扩展你的应用程序功能。它们有助于在你允许胆没有预料或自定义的方式。 你提供框架,其他人提供代码。
尽管在Mach-O(objc_loadModules,在objc/objc-load.h 定义)文件有运行时函数来执行Objective-C模块的动态加载,Cocoa的NSBundle类提供了一个更方便的接口用于动态加载——一个面向对象并集成了相关服务。在Foundation framework 参考中NSBundle类规范中关于一个NSBundle类及其使用参考。
6 消息转发(Message Forwarding)
发送一个消息给你个对象却不处理消息是错误的。然而,在公布错误前,runtime system系统给接受对象第二次机会来处理消息。
6.1 转发(Forwarding)
如果你发送一个消息到一个不处理消息的对象,在公布错误前,runtime发送这个对象一个NSInvocation对象做文艺参数的forwardInvocation:消息。 �该NSInvocation对象封装原始消息和它已通过的参数。
你可以实现这个forwardInvocation:方法,得到一个消息的默认反馈,去避免某些其他方式的错误。正如名字所暗示的,forwardInvocation:通常用于转发消息到另一个对象。
要查看转发的范围和目的,想想一下以下场景:假设,首先你正设计一个对象,能对一个所谓谈判的消息做出反应,并且希望包含对另一个对象的反馈。你可以在negotiate方法的实现很容易的发送anegotiate消息给其他对象。
更进一步,假设你希望你对anegotiate消息的响应在另一个类中实现响应。你的类继承自其他类会很容易坐到这一点。然而,它可能无法以这种方式安排事情。可能有好的理由,为什么你的类和实现negotiate的类在继承层次的不同分支。
即使你的类不能继承negotiate方法,你仍然可以"借用"它,通过实现一个简单地将消息传递到其他类的一个实例方法来实现:
- (id)negotiate{
if ( [someOtherObject respondsTo:@selector(negotiate)] )
return [someOtherObject negotiate];
return self;
}
这种做事方式可能变得有点麻烦,尤其你有一些消息需要从你的对象传递给其他对象。你必须实现一个方法来覆盖每一个你想从其他类借用的方法。此外,你在写代码时处理你你想要转发的全套消息是不可能的。这组依赖于runtime时的事件,当一个新的类或者方法被实现时可能会改变。
由forwardInvocation:提供第二次机会:消息提供了对于这个问题的解决,�而另一个是动态的而不是静态的。它的工作原理是这样的:当一个对象不能对消息做出响应因为在消息中选择器没有匹配到方法, runtime system通过发送一个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];
}
被转发消息的返回值被返回给原始的发送者。所有类型的返回值可以被传递给发送者,包括ids,structures和双精度浮点数。
一个forwardInvocation:方法可以作为未识别消息的集散地,分发他们到不同的接收器。或者它可以作为一个传输站,发送所有消息到同一个目的地。它可以转发一个消息到另一个,或者干脆"吞"一些消息,所以没有返回值也没有错误。一个forwardInvocation:可以合并多个消息到一个当以响应。forwardInvocation:做的是实现。但是,它提供了一种为转发链接对象开辟程序设计的可能性。
关于 forwarding 和 invocations更多信息, 查看Foundation framework索引的NSInvocation类
6.2 转发和多继承(Forwarding and Multiple Inheritance)
转发模仿了多继承,可以向在Objective-C程序中的对多继承的一些效果。如图5-1所示,一个对象响应一个消息听过借用或继承在其他类中实现的方法。
在这个例子中,一个Warrior类的实例变量转发一个anegotiate消息给一个Diplomat类的实例。Warrior会出现像Diplomat类的negotiate。它将返回一个反馈给negotiate消息,并且为所有实际目的做出回应(尽管做出反馈的是Diplomat类)。
转发消息的对象从两个继承体系"继承"——它自己的分支和响应消息的对象。在上面的例子中,它看起来像Warrior继承自Diplomat类和他的父类。
转发提供了大多数希望从多重继承中想要的功能。然而,两者之间有一个重要的区别:多重继承在单一对象中集成不同能力。它趋向于大型,多层面的对象。而另一方面,转发分配不同的职责给不同的对象。它分解问题为更小的对象,但在某种程度上是透明的的消息转发到相关联对象。
6.3 替代对象(Surrogate Objects)
转发不仅模仿多重继承,这也使你开发出轻量级的对象,代表或"掩盖"较大幅度的对象。其他对象的替代对象和漏斗消息。
在Objective-C编程语言中"Remote Messaging"代理是这样一个替代品。代理转发消息到一个远程接收器,确保参数值被复制并通过连接检索,等等。但它不会尝试做出更多地行为,它不会复制远程对象的功能而只是简单地给出远程对象的地址,可以在另一个应用程序接受对象的地方。
其他种类的替代对象也是可能的。举个例子,你有一个操作大量数据对象——或许创建一个复杂的图片或读取磁盘文件的内容。设置这个对象可能非常耗时,所以你选择懒加载——在它需要或者系统资源闲置时。同时,你需要这个对象需要至少一个占位符,以便应用中的其他对象正常工作。
在这种情况下,你可以在最初创建时不全部生成,而是一个轻量级的替代。这个对象做一些事情,如有关数据答题,但大多数只是一个多对象的地址,当时间触发转发消息给它。当代理的forwardInvocation:方法首先收到发往另一个对象的消息,确保对象存在如果不存在创建它。大对象的所有消息都通过替代,这样,对于程序的其他部分而言,替代和大对象是一样的。
6.4 转发和继承(Forwarding and Inheritance)
虽然转发模仿继承,但NSObject类从不混淆两者。像respondsToSelector: �和 isKindOfClass:方法只在继承层次的,从来没有在转发链上。例如一个Warrior对象被询问是否响应一个negotiate消息。
if ( [aWarrior respondsToSelector:@selector(negotiate)] )
...
结果是NO,虽然能够无误的收到negotiate消息并回应它,从某种意义上说,消息被转发给Diplomat(见图5-1)。
在很多情况下,NO是正确地答案。但它也可能不是。如果你用转发去设置一个代理对象或者扩展一个类的功能,转发机制夜巡应该是透明的继承。如果你转发消息的对象继承了这些行为,你需要重新实现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:
7. 类型编码(Type Encodings)
为了帮助runtime system,编译器编码了返回值和参数类型为字符串的方法,并且把字符串与方法选择器关联。它使用的编码方案可以在上下文中使用编译指令@encode()是公开可用的。当给定一个类型,@encode()返回一个类型的字符串编码。类型可以是基础类型,如int,pointer,structure或union,或类名——实际上,任何类型可以被C的sizeof()操作符使用。
char *buf1 = @encode(int **);
char *buf2 = @encode(struct key);
char *buf3 = @encode(Rectangle);
下表列出了类型编码。需要注意的是其实很多时候为了存档和分发的目的编码对象,不过,这里有你编写编码器时不能使用的编码列表,并有你希望使用但不能被@encode()编码器生成。
数组的类型编码是一个封闭的方括号;数组中元素的数目在数据类型前可以马上指定。 例如一个12个指针的浮点数组可以编码为:
[12^f]
结构体是一个特殊的括号和括号里的unions。结构体标签被列在第一位,接着是等号和在队列中的结构体字段编码。例如,这个结构体:
typedef struct example {
id anObject;
char *aString;
int anInt;
} Example;
将被像这样编码:
{example=@*i}
同样地编码结果是否已定义的类型名(Example)或者结果体标识(example)被传递给@encode()。结构体指针编码对于结构体字段携带了相同的信息:
^{example=@*i}
不过,另一方面它间接消除了内部规则:
^^{example}
对象被当做结构对待。例如NSObject类用@encode()编码得到:
{NSObject=#}
NSObject类声明是一个变量实例,isa。请注意尽管@encode()指令不直接返回他们,当他们在协议中声明方法用runtime system用额外的编码如表6-2。
8. 声明属性(Declared Properties)
当编译器遇到属性声明时(参见Objective-C语言属性声明),它通常会生成一个与封闭类,类别或协议相关联的描述性元数据。你可以通过类或协议支持查找属性的函数来访问元数据,获得属性类型作为@encode字符串,并复制属性列表的属性作为一个C字符串数组。为每一个类和协议声明可使用的属性列表。
8.1 属性类型和功能(Property Type and Functions)
Property结构体定义了一个不透明的句柄属性描述符。
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类型的字符串。对于编码字符串的细节,查看Type Encodings,该字符串的详细吸吸,查看Property Type String 和 Property Attribute Description Examples。
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));
}
8.2 属性类型字符串(Property Type String)
你可以使用property_getAttributes 函数去发现名字,属性的@encode字符串, 和属性的其他属性。字符串在使用@encode类型是在开始使用T和一个逗号,在结束时跟实例变量的名字和一个V这些中间,属性被指定使用如下描述,以逗号分隔:
8.3 属性类型描述示例(Property Attribute Description Examples)
给出如下定义:
enum FooManChu { FOO, MAN, CHU };
struct YorkshireTeaStruct { int pot; char lady; };
typedef struct YorkshireTeaStruct YorkshireTeaStructType;
union MoneyUnion { float alone; double down; };
下表显示了实力型声明和property_getAttributes:返回的响应字符串: