Runtime编程指南及笔记

笔记

Runtime机制提供了Objective-C的方法调用过程中的动态绑定,消息转发能力和一些属性,类和方法的操作接口.通过Runtime就可以实现很多种复杂的需求,比如动态添加方法,类和替换重写方法实现,错误处理和虚拟多继承等等.

Objective-C Runtime Programming Guide

runtime的版本和平台

Objective-C的runtime有两个版本: “现代版” 和 “历史版”. 现代版本是从Objective-C 2 开始引入使用的, 相对于历史版本, 增加了一些新功能. 历史版本是从Objective-C 1 开始使用的. 两个版本最显著的区别在于新版本, 我们更改了一个类的实例变量的布局, 我们不用重新编译继承这个的类的所有子类, 而在历史版本则需要重新编译.

新版本运行在 iPhone应用和OS X v10.5及以上的64位应用, 其他的应用则运行老版本.

runtime交互

Objective-C程序和runtime交互有三种方式: 通过Objective-C的源码, 通过NSObject的方法, 通过直接调用runtime方法.

与Objective-C源码交互

大多数情况下, runtime系统都是自动运行的,我们只需要编写代码,编译即可.当编译的代码中包含了Objective-C的类和方法时,编译器创建了实现语言动态特性的数据结构和函数调用. 数据结构捕获在类,类别和协议声明中的信息,包括类和协议的对象,方法选择器, 实例变量的模板和其他一些信息. 主要运行时函数是发送消息的函数,就像消息中描述的那样, 它由源代码消息表达式调用.

NSObject方法

大多数Cocoa对象都是继承自NSObject,也继承了方法的定义.(有个例外, NSProxy类,它没有继承任何类). NSObject与每一个实例和类对象建立了关联,然而在一些情况下,NSObject类很少为做某些事情而定义一些模板,它不完全提供必要的代码.

例如,NSObject类定义了一个description的实例方法,用来返回一个类的内容字符串.这个主要用在调试的时候– GDB的print-object命令会从这个方法打印返回的字符串.NSObject在实现这个方法的时候不知道具体的子类内容是什么,所以默认返回了实例对象的名字和内存地址.子类可以实现NSObject的description 方法来返回自己的内容.比如NSArray类可以返回一个内部对象描述的列表.

一些NSObject方法只是简单的从runtime系统中获取一些信息.这些信息允许对象内省.例如class方法,目的是可以通过isKindOfClass:方法判断一个对象的class,和isMemberOfClass:方法来判断对象在层级的位置.通过respondsToSelector:判断对象是否可以响应特定的消息,通过conformsToProtocol:判断对象是否遵循了特定的协议,通过methodForSelector:来返回方法实现的地址,

runtime方法

runtime系统是由定义在/usr/include/objc文件中的一些方法和数据结构组成的动态共享库.很多的方法允许我们使用C语言来复制编译器在编写Objective-C代码是所做的工作.另一些则是通过NSObject类的方法导出的功能.也可以通过这些方法为runtime系统开发出其他的接口,产生工具,增强开发环境.这些方法在使用Objective-C的开发时候不是必须的,但是使用一些runtime系统方法会对Objective-C开发有好处.

消息传递

objc_msgSend方法

在Objective-C中, 消息直到runtime的时候才进行绑定,编译器将消息解释成:

[receiver message]

当调用objc_msgSend方法的时候:

objc_msgSend(receiver, selector)

有多个参数的时候:

objc_msgSend(receiver, selector, arg1, arg2, …)

这个消息传递方法做了一切需要动态绑定的操作:

编译器会调用消息转发过程,永远不要直接通过代码调用

消息传递的关键在于编译器为每个类和对象构建的结构。每个类结构都包含这两个基本元素:

当一个新的对象被创建,内存中会创建空间,初始化它的实例变量.在对象变量最开始是一个指向类结构的一个指针.这个指针叫做 isa指针,通过这个指针可以访问本身类和父类.

当向一个对象发送一个消息的时候, 消息的方法会根据isa指针找到类的结构,并在类结构中的分发表里寻找方法选择器.如果没有找到, objc_msgSend方法会沿着isa指针找到父类,继续查找,一直到NSObject类, 在查找过程中, 一旦找到了, 就会停止查找并调用方法选择器.

这是在运行时选择方法实现的方式,或者用面向对象编程的行话来说,方法是动态绑定到消息的。

为了加速消息处理, runtime系统缓存了使用过的方法选择器和方法地址.因为是针对不同的类做缓存,所以也保存了从父类继承的方法选择器.在寻找类的分发表之前, 消息程序会先检测缓存. 如果方法选择器位于缓存中,消息传递只比函数调用稍微慢一点. 当程序运行了一段时间后, 几乎所有的消息都会在缓存中找到. 当程序运行时,缓存会动态地增长以适应新的消息.

使用隐藏参数

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 :可以获取一个方法实现的指针,然后用这个指针可以调用方法,这个方法返回的指针必须小心地将其转换为适当的函数类型, 返回值和参数的类型必须包含在转换列表中.

下面的例子展示了如何调用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);

前两个参数是接收者和方法选择器,这些参数在方法里是隐藏的, 但是当一个方法被当做函数的调用的时候需要显示指定.

使用methodForSelector :方法可以避免动态绑定, 节约很多时间.但是,只有在特定的消息重复多次的情况下,节省才会有意义,就像上面所示的for循环一样.

注意方法methodForSelector :是通过Cocoa的runtime系统提供的, 它不是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:方法调用了, 如果respondsToSelector:方法或instancesRespondToSelector:方法被调用,那么动态方法解析器就有机会为选择器提供一个IMP.如果我们实现了resolveInstanceMethod:方法,但是希望通过转发机制实际地转发特定的选择器,返回NO就可以了.

动态加载

一个Objective-C程序可以在运行的时候加载并链接新的类和类别.新代码被合并到程序中,并对开始时加载的类和类别进行相同的处理.

动态加载可以做很多事情.例如,系统首选项应用程序中的各种模块都是动态加载的.

在Cocoa环境中,动态加载技术通常允许应用自定义.其他人可以编写程序在运行时加载的模块,就像接口构建器加载自定义调色板和OS X系统首选项应用程序加载自定义的首选模块一样.可加载模块扩展了应用程序可以做的事情,他们以你所允许的方式为之做出贡献,但却无法预料或定义自己.您提供了框架,但是其他人提供了代码.

尽管有一个运行时函数在Mach-O文件中执行Objective-C模块的动态加载(objc_loadModules, 定义在objc/objc-load.h),Cocoa的NSBundle类也提供了更方便的接口来动态加载–一种面向对象的,与服务集成.可以查看NSBundle类中如何使用.

消息转发

给一个对象发送它处理不了的消息属于一种错误.但是在报告这个错误之前,runtime系统会给接收者第二次机会处理这个消息.

转发

如果我们给一个对象发送了它不能处理的消息,runtime会在报错之前给这个对象发送forwardInvocation:消息, 并携带了一个参数NSInvocation,里面包含原始的信息和参数.

我们可以实现forwardInvocation:方法,或者给一个默认的处理方法,或者避免报错等.正如这个方法名字一样,这个方法通常用于将消息转发给另一个对象.

为了印证转发的范围和目的,我们想象以下场景:首先, 我们设计了一个对象能响应negotiate方法,方法里面包含了另一个对象的响应.我们可以在negotiate方法里面调用另一个对象来实现.

更进一步,假设我们想让一个对象的响应恰好是另一个类实现的响应.一个方法实现是让我们的类中的方法从另一个类集成来.但是这不太容易实现.因为可能有更好的原因在两个不是集成结构分支上的类分别实现negotiate方法.

即使我们不能继承negotiate方法, 我们也可以简单的通过下面的方式实现:

- (id)negotiate
{
    if ( [someOtherObject respondsTo:@selector(negotiate)] )
        return [someOtherObject negotiate];
    return self;
}

这种方式有点繁琐,尤其是当有很多消息我们想从一个对象传递给另一个对象的时候.我们必须实现一个方法来覆盖到所有的这种需要转发的消息.而且这个方法不能处理我们在写代码的时候还不知道的消息.我们希望转发所有消息,但是消息集合依赖于runtime的事件,它将来可能会变成一个新方法或者类.

通过forwardInvocation:方法进行二次转发提供了一个临时的解决方案, 而且是一个动态方案.它的工作流程: 当一个对象由于没有在方法列表里找到方法而不能响应这个消息的时候, runtime系统通过forwardInvocation:方法通知这个对象.每个对象都可以从NSObject对象继承这个方法.NSObject里面的只是简单的调用了doesNotRecognizeSelector:方法,通过复写父类的方法,我们可以有机会将消息转发给另一个对象.

针对一个转发的消息,forwardInvocation:方法需要做:

消息转发可以通过invokeWithTarget:方法实现:

- (void)forwardInvocation:(NSInvocation *)anInvocation
{
    if ([someOtherObject respondsToSelector:
            [anInvocation selector]])
        [anInvocation invokeWithTarget:someOtherObject];
    else
        [super forwardInvocation:anInvocation];
}

转发的消息的返回值会直接返回给最原始的发送者.所有类型的返回值都会返回给发送者,包括 ids,结构体,浮点数等.

forwardInvocation:方法扮演者无法识别的消息发布中心,打包发送给不同的接收者.或者扮演一个换乘站的角色,将所有的消息发送给同一个接收者.它可以将一个消息发送到另一个地方,或者简单的”吞噬”掉消息,不会有报错或相应.forwardInvocation:方法也可以合并几个消息发给同一个响应者.这个方法如何实现还是要看实现者的实现.然而,它提供的在转发链中链接对象的机会,为程序设计提供了可能.

forwardInvocation:方法只有在接收者没有找到方法的实现的时候才会被调用.比如:我们想让一个对象转发negotiate这个消息给另一个对象,那么这个对象就不能有negotiate方法.如果有这个方法,就永远不会触发forwardInvocation:方法.

转发和多重继承

转发模拟继承, 可以用于向Objective-C提供一些多继承的影响.下图所示,一个对象通过在另一个类中借用或者继承的方法,响应了一个消息.

在图中,一个Warrior类的对象转发了消息negotiateDiplomat类的对象.在negotiate消息面前,WarriorDiplomat一样.Warrior似乎会响应消息,而且出于所有实际的目的,它确实会做出回应(即便是Diplomat真正的做了工作).

因此,转发消息的对象继承了继承层次结构的两个分支的方法,即它自己的分支和响应消息的对象.在上面的例子,似乎Warrior类继承自Diplomat和自己的父类.

转发机制提供了很多我们想要在多重继承下的功能.但是转发机制和多重继承有很大的区别: 多重继承把不同的能力合并在一个对象里.它倾向于大的、多面的对象.转发机制主要面向单一的功能.它将问题分解为较小的对象,但以一种对消息发送者透明的方式将这些对象关联起来.

代理对象

转发机制不仅模拟了多重继承, 它还能是够使开发一个轻量级的对象来代表或者覆盖更大的对象成为可能. 代理代表这个更大的对象来接收消息.

The Objective-C Programming Language文档中的Remote Messaging代理也是一种代理.它管理了将消息转发给远端接收者的一些细节,可以保证参数都能通过连接正确的传输.但是它又不能做太多工作,它不会复制远端对象的功能,只是通过一个本地地址做沟通的桥梁,这个地址中存储这个消息.

还有其他类型的代理对象. 比如, 有一个对象操作着很多数据:创建了复杂的图片对象或者从磁盘读取很多文件.设置这个对象的时候就比较耗时,所以我们尝试懒加载,就是只有在真正用到这个对象或者在系统短暂空闲的时候才去设置这个对象.但是同时,我们至少需要一个这个对象的占位符来给其他对象或者应用去使用.

在这种情况下,我们可以简单初始化创建,而不是完全的对象,而是一个轻量级代理.这个代理可以对自己做一些事情,比如回答一些数据的问题,但是大多数情况,它只会为大的对象保留一个位置,当需要的时候,将消息转发给这个大的对象.当代理者的forwardInvocation:方法首次收到为被代理对象指定的消息的时候,代理者要保证被代理对象要存在,否在需要创建它.所有针对大的对象的消息都通过代理,所以对于其他的程序来说,代理和大对象是一样的.

转发和继承

尽管转发机制模拟了继承,NSObject类从来没有混淆两者,比如respondsToSelector:isKindOfClass:方法只会在继承链路里面去查找,从来不会通过在转发链里面查找.比如一个Warrior对象去询问是否已响应negotiate方法的时候:

if ( [aWarrior respondsToSelector:@selector(negotiate)] )
    ...

结果会返回NO. 即使它可以通过消息转发机制转发给Diplomat类来接收negotiate消息并响应.

在很多情况下, 返回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:方法调用.

这个高级技术,只适用于没有其他的解决办法的时候使用,它不能用来代替继承.如我我们确认使用这个技术,请请确保完全理解转发机制和类.

类型编码

为了runtime系统服务, 编译器将每一个方法的返回值和参数进行编码成字符串, 并于方法选择器关联.它所使用的编码方案在其他上下文中也很有用,因此可以通过@encode()编译器指令直接使用.当给定类型规范时,@encode会返回一个字符串编码类型.可以使用的类型可以是基本数据类型,比如int,指针,结构体,联合体或者一个类名,总之可以被sizeof()方法当做参数的类型都可以.

char *buf1 = @encode(int **);
char *buf2 = @encode(struct key);
char *buf3 = @encode(Rectangle);

下面列表列出了类型列表.其中,它们中的许多与我们在为一个对象编码时所使用的代码相重叠,以便进行归档或分发.但是这些编码是在编写代码时候不能使用的,而且很多我们想使用的编码也不是@encode生成的.

编码 含义
c char
i int
s short
l long
q long long
C unsigned char
I unsigned int
S unsigned short
L unsigned long
Q unsigned long long
f float
d double
B C++ bool or a C99 _Bool
V void
* character string (char *)
@ An object (whether statically typed or typed id)
# class object (Class)
: method selector (SEL)
[array type] array
{name=type…} struct
(name=type…) union
bnum bit field of num bits
^type A pointer to type
? An unknown type (among other things, this code is used for function pointers)

注意,Objective-C不支持long double类型. @encode(long double)返回 d,和double返回的一样.

数组类型的编码使用方括号包围的,开始的数字代表数组的大小,然后紧跟类型.比如有一个数组包含12个float类型的指针:

[12^f]

结构体用花括号包围,联合体用小括号包围.结构体用等号连接tag和所有内容的类型:

typedef struct example {
    id   anObject;
    char *aString;
    int  anInt;
} Example;

会编码成:

{example=@*i}

这时候使用结构体名或者结构体tag都可以得到一样的结果.如果是一个结构体指针,编码结果和其他指针一样:

^{example=@*i}

然而,另一种间接方式消除了内部类型规范:

^^{example}

对象类型会被当做结构体类型.比如,NSObject类名会编码成:

{NSObject=#}

NSObject类只声明一个变量实例isa的类类型.

尽管@encode没有直接返回下面的类型,runtime系统会用到这个编码类型当方法定义在协议中的时候.

编码 含义
r const
n in
N inout
o out
O bycopy
R byref
V oneway

声明属性

当编译器遇到属性的定义的时候,它生成与封闭类、类别或协议相关联的描述性元数据.我们可以调用方法,通过一个类或协议的属性名来访问这个元数据.得到属性类型的编码字符串,或者将属性列表拷贝成C数组.每个类和协议都有一个声明属性的列表.

属性类型和方法

属性结构为属性描述符定义了一个不透明的句柄.

typedef struct objc_property *Property;

我们可以使用class_copyPropertyListprotocol_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_getPropertyprotocol_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));
}

属性类型字符串

我们可以使用property_getAttributes方法来获取一个属性的编码字符串和其他信息.这个字符串以T开始,紧跟类型编码和逗号,最后一段以V开始紧跟属性名字,中间的所有段以逗号分隔.下面列表是属性修饰符编码:

属性描述例子

首先定义一些:

enum FooManChu { FOO, MAN, CHU };
struct YorkshireTeaStruct { int pot; char lady; };
typedef struct YorkshireTeaStruct YorkshireTeaStructType;
union MoneyUnion { float alone; double down; };

下面列出来了通过property_getAttributes方法返回编码字符串的例子:

属性定义 属性描述  
@property char charDefault; Tc,VcharDefault  
@property double doubleDefault; Td,VdoubleDefault  
@property enum FooManChu enumDefault; Ti,VenumDefault  
@property float floatDefault; Tf,VfloatDefault  
@property int intDefault; Ti,VintDefault  
@property long longDefault; Tl,VlongDefault  
@property short shortDefault; Ts,VshortDefault  
@property signed signedDefault; Ti,VsignedDefault  
@property struct YorkshireTeaStruct structDefault; T{YorkshireTeaStruct=”pot”i”lady”c},VstructDefault  
@property YorkshireTeaStructType typedefDefault; T{YorkshireTeaStruct=”pot”i”lady”c},VtypedefDefault  
@property union MoneyUnion unionDefault; T(MoneyUnion=”alone”f”down”d),VunionDefault  
@property unsigned unsignedDefault; TI,VunsignedDefault  
@property int (*functionPointerDefault)(char *); T^?,VfunctionPointerDefault  
@property id idDefault; Note: the compiler warns: “no ‘assign’, ‘retain’, or ‘copy’ attribute is specified - ‘assign’ is assumed” T@,VidDefault
@property int *intPointer; T^i,VintPointer  
@property void *voidPointerDefault; T^v,VvoidPointerDefault  
@property int intSynthEquals; In the implementation block: @synthesize intSynthEquals=_intSynthEquals; Ti,V_intSynthEquals  
@property(getter=intGetFoo, setter=intSetFoo:) int intSetterGetter; Ti,GintGetFoo,SintSetFoo:,VintSetterGetter  
@property(readonly) int intReadonly; Ti,R,VintReadonly  
@property(getter=isIntReadOnlyGetter, readonly) int intReadonlyGetter; Ti,R,GisIntReadOnlyGetter  
@property(readwrite) int intReadwrite; Ti,VintReadwrite  
@property(assign) int intAssign; Ti,VintAssign  
@property(retain) id idRetain; T@,&,VidRetain  
@property(copy) id idCopy; T@,C,VidCopy  
@property(nonatomic) int intNonatomic; Ti,VintNonatomic  
@property(nonatomic, readonly, copy) id idReadonlyCopyNonatomic; T@,R,C,VidReadonlyCopyNonatomic  
@property(nonatomic, readonly, retain) id idReadonlyRetainNonatomic; T@,R,&,VidReadonlyRetainNonatomic  

参考文档: Objective-C Runtime编程指南