iOS 运行时
Objective-C语言进可能将许多决策从编译和链接延缓到运行时。它尽可能的动态处理事务。这意味着Objective-C不仅需要编译器还需要执行编译代码的运行时系统。运行时系统充当Objective-C的一种操作系统,使之正常工作。
Objective-C 程序与运行时系统的交互主要在三个不同的层次
通过Objective-C 源代码;通过基础框架中NSObject类中定义的方法;通过直接调用运行时函数。
- Objective-C 源代码
在大多数情况下,运行时系统自动在后台工作。只通过编写和编译Objective-C源代码才会使用到。
当编译包含Objective-C类和方法的代码,编译器会创建数据结构和函数调用,实现语言的动态特性。数据结构捕获类和分类定义以及协议中声明的信息,其中包括Objective-C编程语言中定义类和协议中讨论的类和协议,也包括方法选择器、实例变量模板和从源码中提取的其他信息。运行时函数的主要功能是发送消息。被源码消息表达式调用。 - NSObject 方法
Cocoa中大多数对象都是NSObject的之类,所以大多数对象都继承NSObject定义的方法。(NSProxy类是个例外,更多信息参见消息转发。)其方法因此为每个实例每个类对象建立行为。然而,在一些情况下,NSObject类只定义了一个模板,告知如何完成,并没有提供必要的代码。
例如,NSObject类定义了一个描述实例方法,该方法返回一个字符串描述类的内容。这主要用于调试,GDB打印对象命令打印该方法返回的字符串。NSObject方法的实现不知道类包含的内容,所以它返回的是一个字符串对象的名称和地址。NSObject的之类可以实现这个方法并返回更多详情。例如,基础类NSArray返回一个列表,包含对象的表述。
NSObject的一些方法可以简单的查询运行时系统信息。这些方法运行对着执行自省。例子中的这种方法是类放,访问一个对象来确定它的类;isKindOfClass:和isMemberOfClass:测试对象在继承层次结构中的位置;respondsToSelector:表明一个对象是否可以接收特定消息;conformsToProtocol:表明一个对象是否要求实现特定协议中定义的方法;methodForSelector:提供方法实现的地址。类似这样的方法给对象自我反省的能力。 - 运行时函数
运行时系统是一个动态共享库,包含公共接口组成的一组函数和数据结构,其头文件位于目录/usr/include/objc。这些函数允许使用纯C复制当编写Objective-C代码时编译器生成的代码。通过NSObject类方法导出其他形式的基础功能。这些函数可以开发运行时系统的其他接口和产生可以扩大开发环境的工具,Objective-C编程中不需要他们。然而,一些运行时函数有时候在编写Objective-C程序时很有用。所有的这些函数在Objective-C编程引用中有说明。
消息传递
将消息表达式转换成objc_msgSend函数调用,以及如何通过名字引用方法。然后解释如何利用objc_msgSend以及如何避免动态绑定
objc_msgSend函数
-
在Objective-C中,直到运行时,消息才会绑定到方法的实现。编译器才会转换消息表达式,
[receiver message]
-
调用消息传递函数objc_msgSend。这个函数需要接收者和消息中提到的方法名即方法选择器作为它的两个主要参数:
objc_msgSend(receiver, selector)
-
消息中传入的任何参数都可以在objc_msgSend处理:
objc_msgSend(receiver, selector, arg1, arg2, ...)
消息传递函数支持动态绑定:
- 首先,获取选择器指向的程序(方法实现)。因为相同的方法可以被不同的类分别实现,获取的具体程序取决于接收器的类。
- 然后调用程序,通过传递接收对象(数据指针)以及方法中指定的任何参数。
- 最后,它传递程序返回值作为自己的返回值。
消息传递的关键在于编译器编译每个类和对象的结构。每个类结构包括这两个基本要素
- 指向父类的指针
- 调度表。这个表的记录可以将方法选择器与指定类方法的地址关联。setOrigin:: 方法的选择器与setOrigin::地址(程序实现)有关,display 方法的选择器与的display 地址有关,等等
当创建一个新对象,会分配内存并初始化实例变量。首先,对象变量是一个指向类结构的指针。该指针,称为isa,通过类,对象可以访问该类和该对象继承的所有类。
注意:isa指针虽然不是语言严格意义上的一部分,但是是使用Objective-C运行时系统所需的一个对象。一个对象须“等效于”结构定义中的struct objc_object(定义于objc/objc.h)。然而,很少需要创建自己的根对象和继承自NSObject 或NSProxy 的对象,自动有isa变量。
当一个消息发送到一个对象,消息传递函数遵循对象的isa指针,该指针指向类结构,并在dispatch表中查找方法选择器。如果不能找到选择器,objc_msgSend则遵循指向父类的指针并试图在dispatch表找到选择器。一直找不到选择器,objc_msgSend将一直查找类的层次结构,直到NSObject类。一旦定位到选择器,函数将调用表中的方法,并将其传递到接收对象的数据结构。
运行时选择以这种方式实现方法。或者以面向对象编程术语来说,该方法是动态绑定到消息。
为了加快消息传递过程,运行时系统缓存使用的方法的选择器和地址。每个类有一个单独的缓存,可以包含继承方法和类中定义方法的选择器。在搜索dispatch表之前,消息传递程序首先检查接收对象类(理论上,是有可能再次使用的方法)的缓存。如果方法选择器在缓存中,消息传递稍微比函数调用慢。一旦一个程序运行足够长时间来“热身”缓存,几乎所有发送的消息都能找到缓存方法。在程序运行时,缓存能动态适应新消息。
使用隐式参数
当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对两个参数更加有用。实际上,接收对象的实例变量可用于方法定义。
获取方法地址
避免动态绑定的唯一方法是获取方法的地址并且直接调用它,就好像它是个函数。当一个特定的方法多次连续执行,并且你希望每次执行该方法时避免消息传递开销,在这种极少数的情况下,该方法可行。
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)的程序和方法选择器(_cmd)。这些参数在方法语法中是隐藏的,但当该方法当成函数调用时,必须是显式的。
使用methodForSelector:方法避免动态绑定节省了消息转发所需的大部分时间。然而,只有在特定消息重复多次的情况下,如上面的for循环,节省时间才会有重要意义。
注意:Cocoa运行时系统提供methodForSelector:方法,该方法并不是Objective-C 语言本身的特性。
动态方法解析
如何动态的提供一个方法的实现。
在有些情况下,需要动态的提供一个方法的实现。例如,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
BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types)
"v@:" v的意思是参数 void 后边固定@:
参见文档
动态加载
一个 Objective-C 程序可以在运行过程中加载和链接新类和分类。程序中纳入新代码,在开始加载的类和类别中都可以使用。
动态加载可以用来做很多不同的事情。例如,系统偏好设置应用程序中的模块都是动态加载的。
在Cocoa 环境中,动态加载通常用来自定义应用程序。其他人可以编写模块让你的程序在运行时加载,类似IB加载自定义调色板和OS X系统设置应用程序加载自定义偏好模块。可加载模块可扩展应用程序。他们以你允许的方式贡献代码,但是不能自己预计和定义。你提供框架,其他人提供代码。
尽管有一个运行时函数在Mach-O 文件(在objc/objc-load.h中定义的objc_loadModules)中,执行 Objective-C 模块的动态加载。Cocoa的NSBundle 类为动态加载提供了更方便的接口,这个接口是面向对象并与相关服务结合。了解NSBundle 类信息和使用,可参阅基础框架引用中的NSBundle 类规范。Mach-O 文件信息可查看OS X ABI Mach-O文件格式引用。
消息转发
发送消息到不处理该消息的对象会发生错误。然而,在声明错误之前,运行时系统给接收对象第二次机会处理该消息。
可以借此实现多重继承的功能
如果发送消息到不处理该消息的对象,在声明错误之前,运行时给该对象发送forwardInvocation: 消息,NSInvocation 对象作为唯一参数。NSInvocation 对象封装原始消息和需要传递的参数。
可以实现 forwardInvocation:方法,提供一个默认消息响应,或者以其他方式避免错误。顾名思义, forwardInvocation:通常用来将消息转发给另一个对象
为了看到转发的范围和目的,想象以下场景:首先假设,你正在设计一个叫做negotiate的对象可以响应消息,你希望它能响应另一个对象的响应。你可以通过传递negotiate消息到你实现的negotiate方法中的另一个对象。
更近一步,假设希望对象精确的响应negotiate 消息,则需要在另一个类中实现。实现这个目标的一个方法是让类继承其他类的方法。然而,它不可能以这种方式安排事情。也许存在充分的理由为什么你的类和实现negotiate 的类在不同分支的继承层次结构中。
即使类不能继承negotiate 方法,仍然可以通过实现一个版本的方法来“借用”它,该方法只是简单的将信息传递给另一个类的实例:
-(id)negotiate
{
if([someOtherObject respondsTo:@selector(negotiate)])
return [someOtherObject negotiate];
return self;
}
这种方式有点麻烦,特别是对象要传递大量消息到另一个对象。你必须实现一个方法来覆盖每个从其他类借来的方法。此外,这种方法不能处理你不知道的情况。例如,在写代码的时候,你想转发所有的消息。这取决于运行时的事件,有可能在将来作为新方法和类实现。
消息提供第二次机会,提供了一个相对不那么特殊的解决方案,该方案是动态的而非静态的。它的工作原理是:当一个对象因为没有一个方法匹配消息中的选择器而无法响应消息时,运行时系统通过发送一个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,结构,双精度浮点数。
forwardInvocation: 方法可以作为无法识别消息的分发中心,将消息打包给不同的接收者。或者可以作为中转站,发送所有消息到相同的目的地。它可以把一个消息转发给另一个,或简单的“吞咽”一些消息,所有没有响应也没有错误。forwardInvocation:方法还可以合并几个消息到一个响应上。forwardInvocation:做什么是由系统决定的。然而,它提供一个机会,使得转发链接中的链接对象为程序设计提供可能。
注意:方法只有在他们不调用名义上接收者的现有方法的情况下才处理消息。例如,如果你希望你的对象转发negotiate 消息到另一个对象,则对象不能有自己的negotiate 方法。如果是这样,消息永远不会到forwardInvocation:。
关于转发和调用的更多信息,参见基础框架引用中NSInvocation 类规范。
转发和多重继承
转发提供了大部分多重继承的功能。然而,两者之间有个重要的区别:多重继承在单一对象上结合了不同的功能。它更加强大,多层面对象。另一方面,转发分配单独的职责给不同的对象。它将问题分解为更小的对象,但是以一种方式将对象联合,该方式对消息发送者是透明的。
代理对象
转发不仅模仿多重继承,还可以开发轻量级对象代表或“覆盖”更实质的对象。代理代替其他对象并传送消息给它。
Objective-C 编程语言中“远程消息传递”中描述了该代理。代理负责管理消息转发到远程接受者,确保复制参数值和恢复链接,等等。但不尝试做其他。它不与远程对象的功能重复,只是简单的给远程对象一个本地地址,该地址可以在另一个应用程序中接收消息。
其他类型的代理对象也是可以的。假设,如果有个对象操作大量数据,它也许会创建一个复杂的图片或者读取磁盘上的文件内容。设置该对象可能非常耗时,所以为了简单,只有真正需要的时候或者系统资源暂时闲置时使用。同时,为了保证其他对象在应用程序正常运行,该对象至少需要一个占位符。
在这样的情况下,你可以首先创建,不是成熟的对象,但是时一个轻量级的代理。这个对象可以做些自己的事情,比如回答数据问题,但更多的是为大对象占个位置,当时间到了转发消息给大对象。当代理forwardInvocation: 方法首先接收一条消息传递给另一个对象,它将确保对象存在,如果不存在则创建。所有大对象的消息都是通过代理,因此,对于其余程序而言,代理和大对象是一样的。
转发个继承
尽管转发模仿继承,NSObject类不会混淆两者。像respondsToSelector: 方法和isKindOfClass: 方法只查看继承层次结构,不在转发链上。
如果使用转发来设置代理对象或扩展一个类的功能,转发机制必须如同继承一样透明。如果想让你的对象假装它们真正继承它们转发消息的对象的行为,需要重新实现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;
}
注意:这是一种先进技术,只有在没有其他解决方案可行的情况下才适用。它并不打算取代继承。如果你必须使用这种技术,确保你完全理解转发类和你要转发的类的行为。
类型编码
为了协助运行时系统,编译器用字符串为每个方法的返回值和参数类型和方法选择器编码。使用的编码方案在其他情况下也很有用,所以它是public 的,可用于@encode() 编译器指令。当给定一个类型参数,返回一个编码类型字符串。类型可以是一个基本类型如int,指针,结构或联合标记,或任何类型的类名,事实上,都可以作为C sizeof() 运算符的参数。
--
声明属性
当编译器遇到属性声明时(参见The Objective-C 编程语言中的声明属性),它生成与封闭类、分类或协议相关的描述性元数据。可以通过函数访问元数据,该函数支持通过类或协议名称查找属性,获取属性的类型作为@encode 字符串,并复制property的属性列表作为C字符串数组。声明的属性列表可用于每个类和协议。
属性类型和函数
属性结构为属性描述符定义了一个不透明句柄。
typedef struct objc_property *Property;
可以使用class_copyPropertyList 和 protocol_copyPropertyList 函数分别检索属性数组和类(包括加载的类别)和协议:
objc_property_t *class_copyPropertyList(Class cls, unsigned intint *outCount)
objc_property_t *protocol_copyPropertyList(Protocol *proto, unsigned intint *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 charchar *property_getName(objc_property_t property)
可以使用class_getProperty和protocol_getProperty函数分别获取类和协议中给定名称的属性的引用:
objc_property_t class_getProperty(Class cls, const charchar *name)
objc_property_t protocol_getProperty(Protocol *proto, const charchar *name, BOOL isRequiredProperty, BOOL isInstanceProperty)
可以使用property_getAttributes 函数获取属性的名称和@encode类型字符串。编码类型字符串详情可查看类型编码;字符串详情可查看属性类型字符串和property属性描述例子。
const charchar *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));
}