Runtime最全详解

简介

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 类为止。
  • 如果还找不到,就要开始进入动态方法解析了。
  • 如果还找不到,就要进行消息转发机制了。
时间: 2024-10-11 05:36:20

Runtime最全详解的相关文章

java的集合框架最全详解

java的集合框架最全详解(图) 前言:数据结构对程序设计有着深远的影响,在面向过程的C语言中,数据库结构用struct来描述,而在面向对象的编程中,数据结构是用类来描述的,并且包含有对该数据结构操作的方法. 在Java语言中,Java语言的设计者对常用的数据结构和算法做了一些规范(接口)和实现(具体实现接口的类).所有抽象出来的数据结构和操作(算法)统称为Java集合框架(JavaCollectionFramework). Java程序员在具体应用时,不必考虑数据结构和算法实现细节,只需要用这

Java的集合框架最全详解(图)

纯个人整理,如有错误请指正. java的集合框架最全详解(图) 前言:数据结构对程序设计有着深远的影响,在面向过程的C语言中,数据库结构用struct来描述,而在面向对象的编程中,数据结构是用类来描述的,并且包含有对该数据结构操作的方法. 在Java语言中,Java语言的设计者对常用的数据结构和算法做了一些规范(接口)和实现(具体实现接口的类).所有抽象出来的数据结构和操作(算法)统称为Java集合框架(JavaCollectionFramework). Java程序员在具体应用时,不必考虑数据

ActionBar最全详解

一.ActionBar介绍 在Android 3.0中除了我们重点讲解的Fragment外,Action Bar也是一个非常重要的交互元素,Action Bar取代了传统的tittle bar和menu,在程序运行中一直置于顶部,对于Android平板设备来说屏幕更大它的标题使用Action Bar来设计可以展示更多丰富的内容,方便操控. 二.ActionBar的功能 用图的方式来讲解它的功能 <1> ActionBar的图标,可显示软件图标,也可用其他图标代替.当软件不在最高级页面时,图标左

Android动画最新最全详解包含Material Design动画

以前写动画也是零零种种,需要的时候就查API或找现成的,不够系统.现在通过学习将Android整个动画体系勾勒出来,做到有的放矢. 安卓框架提供了2个动画系统:属性动画(Android 3.0)和View动画.这两种动画系统都是可行的,但是,在一般情况下,属性动画是首选的方法,因为它是更灵活,提供更多的功能.除了这两个系统,你可以利用Drawable动画,它允许你一帧一个的加载显示Drawable资源.所以总体来说Android API提供了三类的动画: - Tween动画或View动画(API

SpringBoot注解最全详解

一.注解(annotations)列表 @SpringBootApplication:包含了@ComponentScan.@Configuration和@EnableAutoConfiguration注解.其中@ComponentScan让spring Boot扫描到Configuration类并把它加入到程序上下文. @Configuration 等同于spring的XML配置文件:使用Java代码可以检查类型安全. @EnableAutoConfiguration 自动配置. @Compon

iOS runtime运行时详解

什么是runtime? runtime直译就是运行时间,run(跑,运行) time(时间),网上大家都叫它运行时,它是一套比较底层的纯C语言API,属于一个C语言库,包含了很多底层的C语言API,它是OC的幕后工作者,我们平时写的OC代码,在运行过程时,都会转为runtime的C语言代码 runtime有什么用? 如果你之前不知道runtime,可能觉得它没什么用,其实他的作用非常强大.下面我们就以代码的形式为大家揭开runtime的神秘面纱 作用1  获取一个类全部成员变量名(如果你的成员私

nodejs+express服务搭建最全详解

var express = require("express"); var bodyParser = require("body-parser"); var sm2 = require('sm.js').sm2; var app = express(); var path = require('path'); var exphbs = require('express-handlebars'); var hbsHelper = require('./util/hbs

史上最全的maven的pom.xml文件详解

史上最全的maven的pom.xml文件详解 http://www.cnblogs.com/hafiz/p/5360195.html <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 h

iOS开发——高级特性&amp;Runtime运行时特性详解

Runtime运行时特性详解 本文详细整理了 Cocoa 的 Runtime 系统的知识,它使得 Objective-C 如虎添翼,具备了灵活的动态特性,使这门古老的语言焕发生机.主要内容如下: 引言 简介 与Runtime交互 Runtime术语 消息 动态方法解析 消息转发 健壮的实例变量(Non Fragile ivars) Objective-C Associated Objects Method Swizzling 总结 引言 曾经觉得Objc特别方便上手,面对着 Cocoa 中大量