简介
OC这门语言把很多事情从编译和链接阶段推迟到运行时处理。只要有可能,它就会采取动态运行时机制。这意味着这门语言不仅需要一个编译器还需要一个运行时系统来执行这些编译后的代码。这个运行时系统相当于OC语言的操作系统,它使得这门语言运转良好。
Runtime版本和平台
Objective-C runtime 在不同的平台上使用不同的版本。
Objective-C runtime,有2个版本:“modern” and “legacy”. modern 版本是在 Objective-C 2.0 介绍的,并且包含了很多新特性。 legacy 版本的使用接口是在 Objective-C 1 Runtime Reference介绍的。modern版本的使用接口 是在 Objective-C Runtime Reference介绍的。
Modern版本的Runtime System有一个显著的特征:实例变量是“non-fragile”,
legacy runtime中,父类的成员变量的布局发生改变时,子类需要重新编译
modern runtime中, 父类的成员变量的布局发生改变时,子类不需要重新编译
此外,还支持为声明的属性进行合成操作(即@property和@synthesis)。
iPhone 应用和 OS X v10.5 之后的 64-bit 使用 modern runtime.其他使用 legacy runtime。
与Runtime的交互
OC程序可以通过三种不同的方式和runtime系统交互:
1.通过OC源代码
2.通过Foundation框架中NSObject的定义方法
3.通过直接调用runtime的方法
OC源代码
大部分情况下,runtime系统在后台自动工作,你编写并且编译后的代码就会转换成runtime语言。当你编译的代码包含OC类和对象的时候,这个编译器会创建数据结构和方法来实现这门语言的动态特性。这个数据结构捕捉类的信息、申明在协议中的分类、方法选择器和一些源代码中的其他信息.runtime系统的关键就是消息传递,一般调用类或对象的方法就会促发它。
NSObject方法
大部分Cocoa对象是NSObject子类(NSProxy例外,它是个虚拟的超类),因此继承了NSObject的方法。然而在一些情况下,NSObject只提供了一些方法的定义,并没有实现它,比如description实例方法,需要返回一个描述对内容的字符串描述,主要用于debug调试。它的实现方法不知道这个类包含的信息内容,所以它返回的是类的名称和地址,子类可以重写这个方法获得更多信息,比如NSArray类返回一系列它包含的对象的描述。一些NSObject方法向runtime系统查询信息,例如:
isKindOfClass: 和 isMemberOfClass:方法是判断一个对象代表的类是否位于某个类的继承体系中。
respondsToSelector:判断一个对象是否响应了某个方法
conformsToProtocol:判断一个对象是否遵循了特定协议的方法
methodForSelector:提供了一个方法实现的地址
像这些方法有助于一个对象更好的了解自身的信息。
Runtime方法
runtime是一个动态的开源库,在它的/usr/include/objc头文件里定义了很多公开的方法和数据结构,许多这些方法可以用C语言来编写这些OC的运行时代码,其他一些方法就是使用NSObject定义的方法了。你不一定要在编写OC时候使用runtime方法,但是使用少量的runtime方法可以使得代码更有用。
消息传递
在OC中消息直到运行时才会实现,编译器会把消息语句[receiver message]转换成objc_msgSend(receiver, selector)这个方法将一个接收者和方法选择器作为参数,如果方法带参数就跟在后面传递objc_msgSend(receiver, selector, arg1, arg2, …)。这个方法将完成动态绑定的所有工作。
1.首先它会找到方法选择器对应的方法实现,介于相同的方法名可以对应不同类的不同实现。找到这个正确的方法实现就取决于这个接收者的类了。
2.找到方法的实现后,就需要将这个方法需要的参数传递给这个接受对象了,
3.最后将这个函数实现的返参作为自己的返参。
这个消息发送的链条关键信息有两点
(1)指向父类的指针
(2)一个类的方法列表,这个列表建立了方法定义和实现的映射关系。
当一个对象创建以后,系统就会给它分配内存和初始化。这个对象生成的第一个变量就是一个指向该类结构体的指针,称为isa指针。可以通过它访问这个对象的类以及它的所有继承类。
当给一个对象发送一条消息时,这个objc_msgSend就会沿着这个isa指针找到它的类结构体,这样就能找到这个类的方法列表。如果方法列表里面找不到该方法,就会沿着它的父类指针指向的类来寻找它的父类方法列表,一直到NSObject基类。一旦找到这个方法,就会传递参数调用方法实现。这是在运行时选择方法实现的方法,或者在面向对象编程的术语中,方法动态地绑定到消息。为了加快消息传递过程,当一个方法被调用时运行时系统会缓存方法的选择器和地址。每个类都有一个单独的缓存,它可以包含继承方法的选择器以及类中定义的方法。在搜索调度表之前,消息传递例程首先检查接收对象类的缓存(关于一次使用的方法可能再次被使用的理论)。如果方法选择器在缓存中,消息传递只比函数调用慢一点。一旦程序运行足够长,以“预热”它的缓存,它发送的几乎所有消息都会找到一个缓存方法。当程序运行时,缓存动态增长以容纳新消息。
当找到方法的实现时,它会调用这个方法的实现以及进行传参,这里有两个隐藏的传参,self代表消息接收者,_cmd代表方法选择器。
然而,在有些情况下需要规避这种耗时的消息传递,可以通过调用NSObject的methodForSelector:方法来返回方法实现的地址,然后直接调用它的实现。这种情况一般用于在一段时间连续调用同一个方法。注意这个方法是cocoa的方法不是runtime的方法。
动态方法解析
在一些情况下需要动态的提供一个方法的实现,比如你使用了dynamic来申明一个属性@dynamic propertyName;,告诉编译器这个属性相关的方法是动态生成的。你可以实现 resolveInstanceMethod: 和 resolveClassMethod:方法用于动态的提供类方法和实例方法的实现。
一个OC方法相当于一个C方法带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, "[email protected]:"); |
return YES; |
} |
return [super resolveInstanceMethod:aSEL]; |
} |
@end |
转发方法和动态方法解决方案在很大程度上是正交的。在转发机制启动之前,类有机会动态地解析方法。如果respondstoselector:或instancesrespondtoselector:被调用时,动态方法解析有机会为第一重要的选择。如果你实现resolveinstancemethod:但要特别的选择消息转发机制,你可以在这些方法里返回NO。
动态加载
一个Objective-C程序可以在运行时加载和链接新的类和分类。新的代码被合并到程序中,并与开始加载在开始时的类和分类是一样的。动态加载可以用来做很多不同的事情。例如,各个模块的系统偏好设置就是动态加载的。
在cocoa环境中,动态加载通常用于允许定制应用程序。别人可以写模块用于你的程序运行时加载,比如在IB中加载自定义模版和在OS X系统偏好设置自定义模块。这个加载模块扩展您的应用程序。它们以你所允许的方式做出贡献,但是不允许定义你的程序。您提供框架,但其他提供代码。
虽然是一个运行时函数的执行,在Objective-C Mach-O文件动态加载模块(在objc / objc-load.h定义objc_loadmodules,),cocoa的NSBundle类提供了一个面向对象的动态加载和相关服务集成更方便的接口。想了解在NSBundle类及其使用信息基础框架参可考NSBundle类规范。想了解OS X Mach-O文件格式可参考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提供的第二次机会是:消息提供了一些临时解决方法来处理这种问题,其中一个方法就是动态处理而不是静态的,它的工作流程是这样的:当一个对象不能响应消息因为它没有一个方法在消息选择器匹配,运行时系统通知对象发送一个消息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:所做的就是提升类的方法实现能力,然而,它提供了在转发链中连接对象的机会,为程序设计提供了可能性。
转发和多重继承
消息转发可以用于模拟多重继承,如图所示,一个Warrior实例转发negotiate消息到一个Diplomat实例。这就好像Warrior继承了Diplomat来响应negotiate消息
然而转发和多重继承有一个最主要的区别就是:多重继承合并不同的功能到单个对象中。它趋向于大的、多方面的对象。另一方面,转发就是将不同的职责分配给不同的对象,它将问题分解为较小的对象,但将这些对象透明的关联到一个消息发送者。
代理对象
转发不仅模拟了多重继承,还可以开发表示或覆盖更多实体对象的轻量级对象。这个代理可以代表其他对象并且作为一个漏斗转发消息。
在Objective-C编程语言关于远程通信提到的proxy就是这样一个代理,一个proxy将消息转发给远程接收器的管理细节,确保参数值在连接上复制和检索,等等.
但它不会做其他事情;它不复制远程对象的功能,而是简单地给远程对象一个本地地址,一个它可以在另一个应用程序中接收消息的地方。
还有其他类型的一些代理对象。例如,假设您有一个操作大量数据的对象,也许它会创建一个复杂的图片,或者读取磁盘上文件的内容。设置此对象可能耗时,因此您希望在需要时或系统资源暂时闲置时进行此操作。同时,为了应用程序中的其他对象正常运行,您至少需要一个占位符
在这种情况下,您可以首先为它创建一个轻量级代理而非真实对象。这个代理可以自己做一些事情,比如回答有关数据的问题,但大多数情况下,它只为较大的对象保留一个位置,当时间到来时,向它转发消息。当代理的forwardinvocation:方法首先接收一个信息用于其他对象,它将确保对象的存在,没有就创建它,这个较大对象的所有的消息都通过这个代理,就程序的其余部分而言,这个代理和较大的对象是等价的。
转发和继承
虽然转发模仿了继承,这个NSObject类从来不会混淆了这两者.像respondstoselector 和isKindOfClass:方法只看继承层次结构,而不看转发链。例如,如果询问战士对象是否对协商消息作出响应,
if ( [aWarrior respondsToSelector:@selector(negotiate)] ) |
... |
答案是NO,尽管它可以在不出错的情况下收到谈判信息,并在一定程度上对他们作出回应,将他们转交给外交官。
在许多情况下,会正确的返回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:调用它。这是个前沿技术,用于你没有其他方式处理未知消息的时候,它不是替代继承,如果你必须使用该技术,确保你充分理解了消息转发机制。
类型编码
为了支持运行时系统,编译器将传参类型和返参类型进行了编码,相应的编码器指示符是@encode,比如void 编码为v,char编码为c,对象编码为@,类编码为#,SEL编码为:,而结构体类型则由基本类型组成。事实上,可以用来作为一个参数C sizeof()算法。
char *buf1 = @encode(int **); |
char *buf2 = @encode(struct key); |
char *buf3 = @encode(Rectangle); |
结构体类型则由基本类型组成,如下:
typedef struct example { |
id anObject; |
char *aString; |
int anInt; |
} Example; |
属性申明
当编译器遇到属性申明时,它会生成一些可描述的元数据将其与相应的类、分类和协议关联起来。存在一些函数可以在类活着协议中通过名称来查找这些元数据,通过这些函数我们可以获得编码后的属性类型,复制属性的attribute列表,因此类和协议的属性列表我们都可以获得。
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) |
你可以使用protocol_getproperty和class_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 = 0; i < outCount; i++) { |
objc_property_t property = properties[i]; |
fprintf(stdout, "%s %s\n", property_getName(property), property_getAttributes(property)); |
} |
属性类型编码
与类型编码类似,属性类型也有相应的编码方案,比如readonly编码为R,copy编码为C,retain编码为&等。通过函数可以获取编码后的字符串,该字符串以T开头,紧接@encode type和逗号,接着以V和变量名结尾。比如:
@property char charDefault 编码为 Tc,VcharDefault。
Runtime常用的数据结构
id
指向某个类的实例的指针,指向objc_object结构体的指针指针,objc_object包含isa指针,根据 isa 指针就可以找到对象所属的类。
typedef struct objc_object *id;
struct objc_object {
Class isa;
};
SEL
OC中@selector表示类型,SEL是指向objc_selector的结构体指针。
typedef struct objc_selector *SEL;
Class
Class是指向objc_class的结构体指针
typedef struct objc_class *Class;
struct objc_class {
Class isa;
Class super_class;
const char *name;
long version;
long info;
long instance_size;
struct objc_ivar_list *ivars;
struct objc_method_list **methodLists;
struct objc_cache *cache;
struct objc_protocol_list *protocols;
}
从objc_class 可以看到,一个运行时类中包含了了它的父类指针、类名、成员变量、方法列表、缓存以及附属的协议。
Ivar
一个不透明类型表示一个实例变量
typedef struct objc_ivar *Ivar;
char *ivar_name;
char *ivar_type;
int ivar_offset;
int space;
}
Method
一个不透明类型表示类定义中的一个方法
typedef struct objc_method *Method;
struct objc_method {
SEL method_name; //方法名
char *method_types; //方法类型
IMP method_imp; //方法实现
}
其中IMP是一个block类型typedef id (*IMP)(id, SEL, …);
Cache
typedef struct objc_cache *Cache;
struct objc_cache {
unsigned int mask /* total = mask + 1 */;
unsigned int occupied;
Method buckets[1];
};
Cache 会缓存最近调用的方法,如果一个方法被调用,那么它有可能今后还会被调用,每当实例对象接收到一个消息时,优先在 Cache 中查找。
完整的消息转发机制
- 首先检测这条消息是否需要处理,比如selector 的 target 为 nil的话就不处理。
- 如果消息有效,就进行消息传递机制,先从 cache 里查找selector对应的实现IMP,如果找到了就运行对应的函数去执行相应的代码。
- 如果 cache 找不到就找类的方法列表中是否有对应的方法。
- 如果类的方法列表中找不到就到父类的方法列表中查找,一直找到 NSObject 类为止。
- 如果还找不到,就要开始进入动态方法解析了。
- 如果还找不到,就要进行消息转发机制了。