好久没写了,一是因为懒,二也是因为懒吧...
本文主要翻译了 Objective-C Runtime Programming Guide 这篇苹果的文档,不是逐字逐句翻译,其中会有自己的补充,如果翻译理解有偏差或错误,烦请指正。
本来想研究下YYModel的源码,发现里面用到的Runtime的知识挺多,所以就把原来记录的runtime的知识看了看,又翻译了一下这篇。
参考阅读
Objective-C Runtime Programming Guide 苹果官方文档
Objective-C Runtime Reference 苹果官方文档
The Objective-C Programming Language 苹果官方文档
1 Introduction——介绍
Objective-C 会尽可能地将许多决策从编译时(compile time)、链接时(link time)推迟到运行时(runtime)进行。这意味着OC不仅需要一个编译器,还需要一个运行时系统去执行编译的代码。换句话来说,对于OC来说,runtime系统相当于一个操作系统,runtime系统使OC可以工作运行。
2 Interacting with the Runtime——与Runtime的交互
OC程序可以在3个不同的层面上与runtime系统进行交互:
- 通过OC源码;
- 通过Foundation framework中的NSObject类(内省方法);
- 直接调用runtime函数方法
2.1 Objective-C Source Code——OC源码
大多数情况下,runtime系统会默默地在后台运行。
当编译包含OC的类、方法的代码时,编译器会创建实现了动态特性的数据结构和函数。数据结构会捕获类(class)、分类(category)、协议(protocol)中的各种信息:包括类和协议对象[参考The Objective-C Programming Language中的Defining a Class 和 Protocols]、方法选择子(method selectors)、实例变量等。
runtime最主要的功能就是发送消息,这点在下面会说明。
理解:编译时生成运行时的所使用的数据结构。
2.2 NSObject Methods——NSObject中的内省方法
Cocoa框架中的大部分对象都继承自NSObject类(但是NSProxy类是个例外),因此这些对象都继承了NSObject类中定义的方法。这些继承下来的方法建立了每个实例和每个类的固有行为。
在一些继承下来的方法中,NSObject只定义了一个模板,但是没有给出实现的代码,例如description这个实例方法,它需要子类自己去覆盖实现。
还有一些NSObject中的方法,叫做内省(introspection)方法,可以实现从runtime系统中查询信息:
- isKindOfClass: 是否是某一个类或其子类
- isMemberOfClass: 是否是某一个类
- respondsToSelector: 是否能够响应某个方法
- conformsToProtocol: 是否遵守了某个协议
- methodForSelector: 提供方法实现的地址
2.3 Runtime Functions——Runtime函数方法
运行时系统是一个动态共享库,具有一个公共接口,该公共接口由位于目录/ usr / include / objc中的头文件中的一组函数和数据结构组成。这里面的很多函数可以提供运行时系统的功能。具体见Objective-C Runtime Reference。
3 Messaging——消息传递(注意,不是消息转发)
这一章描述了消息表达式是怎样转换为 objc_msgSend 函数调用的(其实OC中的调用方法本质是给对象发送一条消息),以及如何按名称调用方法,以及如何利用objc_msgSend方法,以及如何在你需要的情况下规避掉动态绑定。
3.1 The objc_msgSend Function——objc_msgSend函数
OC中,消息没有绑定到方法实现,直到runtime时。
理解:也就是说,在运行时,才能够在内存中找到这个方法实现的地址。
编译器会将下面的消息表达式转化为objc_msgSend函数:
[receiver message]
转化为:
objc_msgSend(receiver, selector)
若携带有参数的话,转化为:
objc_msgSend(receiver, selector, arg1, arg2, ...)
消息传递完成了动态绑定所需要的一切:
- 首先,它会找到selector选择子的方法实现。由于相同的方法可以由不同的类实现,因此找到selector的方法实现需要精确到接收这个消息的类。
- 然后,它会调用这个方法实现,并将参数传递给这个方法实现。
- 最后,它会将方法实现的返回值作为自己的返回值返回。
注意:编译器会生成这个消息函数objc_msgSend的调用,不要自己调用objc_msgSend函数
消息传递的关键在于编译器为每个类和对象创建的结构。每个类结构包含两个基本元素:
- superclass父类的指针
- 一个类调度表(class dispatch table)(应该就是方法列表)。这个表上有方法selector与这个方法实现的地址的对应,比如,setOrigin这个selector和setOrigin这个方法实现的地址的关联。
当一个新的对象被创建时,会给它分配内存,并且会初始化它的实例变量。在这个对象的变量里,第一个是指向类结构的指针,称为isa指针。这个isa指针可以让对象去访问它所属的类,并可以通过这个类访问其继承的所有类。
当一条消息被发送给一个对象时,消息传递函数会跟随对象的isa指针找到其类结构,并在类结构中的方法调度表(dispatch table)中寻找方法selector。若没有找到,objc_msgSend 则会跟随类结构的superclass指针去找父类,并在父类的方法调度表中寻找。若找到了selector,会调用方法实现。
这个就被称作消息的动态绑定——在运行时去寻找方法实现。
为了加快消息传递的过程,会有一个对方法列表的缓存,消息传递时首先会去找这个方法列表的缓存(根据理论,曾经使用过的方法可能会再次使用)。如果方法选择器在缓存中,则消息传递仅比函数调用慢一点。
理解:实例的isa指针指向类对象,类对象的isa指针指向元类;动态绑定的一个坏处:比直接调用函数实现要慢,即使有了缓存。
3.2 Using Hidden Arguments——隐藏参数
当objc_msgSend找到方法实现时,它就会调用这个方法实现并将方法参数传进去。同时,它还会给方法实现传递两个隐藏参数:
- 接收消息的对象
- 方法的selector选择子
这两个参数为方法实现指明了有关调用它的消息表达式的参数信息,这两个参数之所以是隐藏的,是因为没有在方法源码中进行声明,而是在代码编译的时候被插入进去的。
虽然没有声明这两个参数,但是在方法的源码中仍然可以引用它们,
在方法里,使用self指的是对象本身,_cmd指的是selector选择子本身。
- strange
{
id target = getTheReceiver();
SEL method = getTheMethod();
if ( target == self || method == _cmd )
return nil;
return [target performSelector:method];
}
3.3 Getting a Method Address——获取方法地址
规避动态绑定的唯一方式:获取到方法的地址,然后直接调用它。在极少数的情况下,当你希望避免掉连续传递消息时的开销时,这么做可能是合适的。
NSObject的methodForSelector:方法可以获取到方法实现的地址,你可通过这个方法获取到一个指针,然后使用这个指针直接调用方法实现。
methodForSelector: 是一个内省方法,不是OC语言的功能,是Cocoa的运行时系统提供的方法。
4 Dynamic Method Resolution——动态方法解析
这一章说明了,你可以如何动态地提供一个方法的实现。
4.1 Dynamic Method Resolution——动态方法解析
在声明属性的时候,可以使用@dynamic指令去描述这个属性。
@dynamic propertyName;
@dynamic 就会告诉编译器不要去生成这个属性的getter和setter方法,这些方法会动态的提供。
使用 resolveInstanceMethod: 为实例方法动态地提供selector的实现;
使用 resolveClassMethod: 为类方法动态地提供selector的实现。
一个OC方法只是一个带有最少两个参数(self、_cmd)的C函数。可以使用 class_addMethod 函数添加一个函数到一个类中作为一个方法。
void dynamicMethodIMP(id self, SEL _cmd) {
// implementation ....
}
@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。当想启动消息转发机制时,可以在 respondsToSelector: 或 instancesRespondToSelector: 为这些selector中返回NO。
理解:当对象接收到无法解析的消息时,也就是说消息传递没有找到这个方法时,才会进入消息转发。
动态方法解析是消息转发的第一步,看消息的接受者能否动态的添加方法,以处理当前的未知选择子。若动态方法解析无法动态的添加方法,下面就会进入完整的消息转发机制。
4.2 Dynamic Loading——动态加载
一个OC工程可以在运行时加载和链接新的类和分类,新代码被合并到程序中,并且与刚开始时加载的类和分类没有区别。
动态加载可以被用来做许多事情,例如,系统的偏好设置中的各个模块就是动态加载的。
在Cocoa环境中,通常使用动态加载来定制应用程序。
有一个runtime函数(objc_loadModules)可以在Mach-O文件中执行OC模块的动态加载,但是Cocoa的NSBundle类为动态加载提供了更方便的接口,即面向对象得与相关服务集成。
NSBundle类参考
推测:这个动态加载应该是与组件化有关。
5 Message Forwarding——消息转发
当给一个对象发送消息,但这个对象无法处理这条消息时,会发生错误。但是,在报出这个错误之前,Runtime运行时系统给了这个对象第二次机会去处理这条消息。
5.1 Forwarding——转发
如果将消息发送给一个无法处理该条消息的对象时,在报错之前,runtime系统会给这个对象发送 forwardInvocation:(NSInvocation *)invocation 消息,invocation里面封装了原始的消息和参数。
你可以实现 forwardInvocation: 方法以对消息提供默认响应或以其它方式避免错误。forwardInvocation: 通常用来将消息转发给另一个对象。
给个例子:现在你想设计一个对象,它能够响应 negotiate 这条消息(方法),并且你想让它的响应里包含另一种对象的响应(简单理解,就是这个方法里面你想调用其它对象的方法)。你可以通过继承来实现这个方法,当不想或无法继承的时候,你也可以这么实现这个例子:
// 这个代码很奇怪,[someOtherObject negotiate]和self不是同一个级别的返回吧
- (id)negotiate
{
if ( [someOtherObject respondsTo:@selector(negotiate)] )
return [someOtherObject negotiate];
return self;
}
也可以这么做,使用运行时方法 forwardInvocation: ,它 的工作方式如下:
- 当对象中没有与消息里面的选择子selector相匹配的方法,这时候runtime运行时系统会给这个对象发送forwardInvocation:消息来通知对象。
- 每个对象都是从NSObject对象里继承forwardInvocation: 方法的,NSObject里这个方法默认只是调用了dosNotRecognizeSelector:(也就是平常selector does not recognized那个报错的来源)。
- 通过覆盖NSObject的forwardInvocation:方法来实现将消息转发给其它对象。
要转发消息,forwardInvocation: 方法需要做这两点:
- 确定这个消息要去哪里
- 将原始参数发送到那里去
invokeWithTarget: 方法可以实现消息的发送
- (void)forwardInvocation:(NSInvocation *)anInvocation
{
if ([someOtherObject respondsToSelector:
[anInvocation selector]])
[anInvocation invokeWithTarget:someOtherObject];
else
// 注意:当发现这个消息不能被当前对象转发时,应该调用父类的同名方法forwardInvocation:,
// 这样才能最终走到NSObject的默认方法实现,调用dosNotRecognizeSelector:报出错误
[super forwardInvocation:anInvocation];
}
这个被转发的消息的返回值会返回给消息的发送者,所有类型的返回值都可以被传递给发送方。
forwardInvocation: 可以做很多很多事情,它提供了在消息转发链中链接对象的机会,为程序设计开辟了可能性:
- 它可以当做一个未识别消息的分发中心,将其打包发送给其它不同的接收者;
- 或者它可以当做一个中转站,将所有消息发送到同一个目的地;
- 它还可以将一种类型的消息转换为另一种;
- 它可以将一些消息“吞”掉,这样这些消息既没有响应也不会有报错;
- 它可以将多个消息合并为一个(这个不太懂,多个方法其实本质上调用的是一个?)
注意,forwardInvocation: 方法是在消息转发过程中被调用的,也就是说,如果消息接受者默认实现了某个(消息)方法,它就不会再走消息转发了,也就不会调用forwardInvocation:了。
更多的信息可以看NSInvocation。
补充:在动态方法解析和forwardInvocation:之间应该还有一步,-(id)forwardingTargetForSelector:(SEL)selector,这个方法可以将消息转发给其它对象,但是这个方法无法对消息进行处理。如果这个方法返回nil,则会走到forwardInvocation:方法。(参考自Effective Objective-C 2.0)
5.2 Forwarding and Multiple Inheritance——转发和多重继承
消息的转发机制模拟了继承机制,可以将多重继承的某些效果赋予OC程序。如下图所示,一个对象通过转发消息让另一个类中的方法去响应这条消息,看上去就像继承了这个方法。
转发提供了通常需要多重继承的大多数功能。但是,两者之间有一个重要的区别:多重继承在单个对象中结合了不同的功能。它趋向于大型、多面的对象。另一方面,转发将不同的职责分配给不同的对象。它将问题分解为较小的对象,但以对消息发送者透明的方式关联这些对象(怎么感觉是不透明的呢,消息发送者哪知道你往哪里发的)。
5.3 Surrogate Objects——替代对象
转发不仅可以模拟多重继承,还可以开发成一种轻量级对象用来代替或表示更重量级的对象。
在 The Objective-C Programming Language 的Remote Messaging中讨论的proxy也是一种Surrogate(粗略翻了一下,没看到,应该是在协议那一章?)。
来个例子:当你有一个处理大量数据的对象,然后这个对象是一个比较重量级的对象,初始化设置这个对象很耗时,所以可以使用懒加载在需要它时再去加载这个对象。这时,可以使用轻量级的替代对象来代替这个重量级对象,这个替代对象可以做一些简单的事情,但是大多数情况下,它只是通过forwardInvocation:将消息转发给重量级对象。在替代对象里,当需要重量级对象但其不存在时,才会创建这个重量级对象。这样就能延迟重量级对象的加载。
5.4 Forwarding and Inheritance——转发与继承
虽然消息转发模拟了继承机制,但是NSObject类不会混淆这两个玩意。也就是说,自省方法像respondsToSelector: 和 isKindOfClass: 只会看继承关系,而不会去看消息转发。
在大多数情况下,这些方法返回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;
}
另外,instancesRespondToSelector:也应该被重写。如果使用协议(应该是看起来是使用了协议,实际还是转发),conformsToProtocol: 应该被重写。
相似地,如果一个对象转发远程消息,它应该重写这个方法methodSignatureForSelector:,该方法可以返回对最终响应所转发消息的方法的准确描述。例如,若果一个对象来转发消息到它的替代对象,应该这么实现methodSignatureForSelector:方法:
- (NSMethodSignature*)methodSignatureForSelector:(SEL)selector
{
NSMethodSignature* signature = [super methodSignatureForSelector:selector];
if (!signature) {
signature = [surrogate methodSignatureForSelector:selector];
}
return signature;
}
methodSignatureForSelector: 方法在NSObject类中有提及。
6 Type Encodings——类型编码
为了协助运行时Runtime系统,编译器会每个方法的返回值类型、参数类型编码成字符串,并且和方法选择子selector相关联。由于这个编码机制在其它一些地方也很有用,所以可以使用 @encode() 编译器指令公开使用。当给定一个类型时,@encode() 方法可以返回这个类型的编码字符串。这个类型可以是基本类型,如int、指针,也可以使其它类型,一切可以使用sizeof()运算符的类型。
char *buf1 = @encode(int **);
char *buf2 = @encode(struct key);
char *buf3 = @encode(Rectangle);
具体还是见原文档吧。
7 Declared Properties——属性声明
终于到最后一章了...
当编译器碰到属性声明(见The Objective-C Programming Language中的Declared Properties)时,编译器会生成与类、分类、协议相关联的描述性元数据(metadata)。
你可以使用函数访问这个元数据:可以通过名称查找一个类或协议里的属性;获取属性的类型编码;将属性列表以C字符串数组的形式拷贝出来。
7.1 Property Type and Functions——属性类型与相关函数
The Property structure defines an opaque handle to a property descriptor.
Property structure定义了属性描述符的不透明句柄。
typedef struct objc_property *Property;
(1)使用 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);
(2)使用property_getName来获取属性名
const char *property_getName(objc_property_t property)
(3)使用 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)
(4)使用 property_getAttributes 函数获取一个属性的名称和类型编码
const char *property_getAttributes(objc_property_t property)
(5)总结,综合使用,利用运行时系统打印一个类的关联属性:
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));
}
7.2 Property Type String——属性类型字符串
你可以使用property_getAttributes函数获取属性的名称、类型编码以及一些其它特性。
这个字符串以T开头,后面跟着类型编码,然后是逗号,然后以V开头,跟着实例变量的名称。在这两个之间,有以逗号分隔的属性修饰符的类型编码。
表格具体见文档,这里写的是属性修饰符(如copy)的类型编码。
7.2 Property Attribute Description Examples——属性特性描述例子
这里有很多例子,可以参考源文档。