探求系列已发布文章列表,有兴趣的同学能够翻阅一下:

第一篇 | iOS 特点 @property 具体探求

第二篇 | iOS 深入理解 Block 运用及原理

第三篇 | iOS 类别 Category 和扩展 Extension 及相关目标详解

第四篇 | iOS 常用锁 NSLock ,@synchronized 等的底层完结详解

第五篇 | iOS 全面理解 Nullability

第六篇 | iOS Equality 具体探求

第七篇| iOS 异常 (NSException) 和错误 (NSError) 处理详解

——- 正文开端 ——-

导言

Objective-C 言语将尽或许多的决策从编译和链接时推迟到运转时(Runtime)。也就是说只要有或许,它就会动态地做事。这意味着 Objective-C 不仅需求编译器进行编译,还需求 Runtime 来履行编译后的代码。能够说 Runtime 是使 Objective-C 言语作业的基础,充任了 Objective-C 言语的一种操作体系,。


运转时版别和平台

Legacy 和 Modern

Objective-C runtime 有两个版别,即 “modern” 和 “legacy”。现代版别是在 Objective-C 2.0 中引入的,包括许多新特性。现代版 runtime 的编程接口在 Objective-C 文档中有具体的描绘,具体可检查 Objective-C Runtime。

最值得留意的新特性是现代运转时中的实例变量是“非软弱的”:

  • 在旧版 runtime 中,假如更改类中实例变量的布局,则有必要从头编译它及其子类。
  • 在现代 runtime 中,假如更改类中实例变量的布局,则不必从头编译它及其子类。

此外,现代运转时支撑声明特点的实例变量合成。

运转时交互

  • Objective-C 程序在三个不同层次上与运转时体系交互:

1. 经过 Objective-C 源代码:

在大多数状况下,运转时体系在幕后主动运转。咱们只需编写和编译 Objective-C 源代码即可运用它。

当咱们编译包括 Objective-C 类和办法的代码时,编译器会创立数据结构和函数调用来完结言语的动态特性。数据结构捕获类和类别界说以及协议声明中的信息。它们包括在 Objective-C 中界说的类和协议中评论的类和协议目标,以及办法选择器、实例变量模板和其他从源代码中提取的信息。主要的运转时函数是发送音讯的函数,下面音讯传递中会具体介绍,它由源代码音讯表达式调用。

2. 经过 Foundation 结构的 NSObject 类中界说的办法:

Cocoa 中的大多数类都是 NSObject 类的子类,因而大多数目标都承继了它界说的办法。(留意:NSProxy 类是个破例,下面音讯转发部分会详述)因而,它的办法建立了对每个实例和每个类目标固有的行为支撑。在有些状况下,NSObject 类仅仅界说了一个模板,用于描绘应该怎么操作,它自身并不供给相应代码完结。

例如,NSObject 类界说了一个描绘实例办法,该办法回来一个描绘类内容的字符串。这主要用于调试 GDB print-object 指令打印从此办法回来的字符串。NSObject 的这个办法的完结并不知道类包括什么,所以它回来一个带有目标称号和地址的字符串。NSObject 的子类能够完结这个办法来回来更多的细节。例如,FoundationNSArray 回来它包括的目标的描绘列表。

一些 NSObject 办法仅仅查询运转时体系以获取想要的信息,这些办法答应目标履行自我检查。此类办法的示例是类办法,它要求目标辨认其类:

  • isKindOfClass: 和 isMemberOfClass: 检查目标在承继层次结构中的方位;
  • respondsToSelector: 表明一个目标是否能够接受特定的音讯;
  • conformsToProtocol: 表明目标是否宣称完结了特定协议中界说的办法;
  • methodForSelector: 它供给了办法完结的地址;

以上的这些办法使目标能够进行自我检查。

3. 经过直接调用运转时函数:

运转时体系是一个动态同享库,其公共接口由坐落目录 /usr/include/objc 中的头文件中的一组函数和数据结构组成。其中许多函数答应咱们运用标准的 C 言语来复写编译器在编写 Objective-C 代码时所做的工作。其他则构成了经过 NSObject 类的办法完结功用输出的基础。这些功用使开发运转时体系的其他接口和生成增强开发环境的东西成为或许。

在 Objective-C 编程时它们不是必需的。可是,一些运转时函数有时很有用。一切这些函数都能够在Objective-C Runtime 中查到。

音讯传递

这里重点介绍下音讯表达式怎么转换为 objc_msgSend 函数调用,以及怎么经过称号引证办法。然后解说怎么利用 objc_msgSend,以及怎么躲避动态绑定(正常来说不会这么做)。

1. 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 中界说)“等效”。可是,咱们很少需求创立自己的根目标,而且从 NSObjectNSProxy 承继的目标都会主动具有 isa 变量。

类和目标的结构如下:

iOS 探究 | 第八篇 Runtime 详细探究

当音讯被发送到一个目标时,音讯传递函数跟从目标的 isa 指针指向类结构,它在调度表中查找办法选择器。假如在那里找不到选择器,objc_msgSend 会依据指向超类的指针找到超类,并尝试在其调度表中找到选择器。就这样 objc_msgSend 依据类的承继结构顺次查找,直到找到 NSObject 类。一旦找到选择器,该函数就会调用在表中查找到的办法,并将接纳目标的数据结构传递给它。

这是在运转时选择办法完结的办法,或许用面向目标编程的术语来说,办法是动态绑定到音讯的。

为了加速音讯传递进程,运转时体系会在运用办法时缓存办法的选择器和地址。每个类都有一个独自的缓存,它能够包括承继办法以及类中界说的办法的选择器。在查找调度表之前,音讯传递例程首要检查接纳目标类的缓存(理论上来说,运用过的办法或许会再次运用)。假如办法选择器在缓存中,音讯传递只比函数调用慢一点。一旦一个程序运转了满足长的时刻来“预热”它的缓存,它发送的一切音讯基本上都会找到对应缓存办法。别的当程序运转时,缓存会动态增长以容纳新音讯。

2. 运用函数躲藏参数

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 是这两个参数中更有用的一个,也是咱们运用频率比较高的一个,一般咱们也是以这种办法在办法界说中运用接纳目标的实例变量。

3. 获取办法地址

绕过动态绑定的唯一办法是获取办法的地址,并直接调用它,就好像它是一个函数相同。这或许仅适用于某些特定办法需求接连履行屡次,而且咱们期望避免因每次履行办法的音讯传递而带来的开支时,正常来说这种场景极其少见。

运用 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 循环。

留意: methodForSelector:Cocoa 运转时体系供给的,它不是 Objective-C 言语自身的特性。

动态办法解析

1. 动态办法解析

在有些状况下,咱们或许期望动态的供给办法的完结。 例如,Objective-C 声明的特点包括 @dynamic 指令:

@dynamic propertyName;

它告诉编译器与特点相关的办法将动态完结即由咱们自己来完结,这时假如咱们代码中没有完结的话,当函数调用对应的 getter/setter 办法时,会因找不到对应的办法而 Crash ,具体介绍,能够看一下之前的文章:iOS 特点 @property 具体探求。

咱们能够完结办法 resolveInstanceMethod:resolveClassMethod: 来分别为实例和类办法指定的选择器(Selector)动态供给完结。

Objective-C 办法仅仅一个 C 函数,它至少有两个参数即 self_cmd。 咱们能够经过运用函数 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:,动态办法解析器有时机为选择器供给 IMP ,假如有就履行对应的函数调用并回来 YES,假如没有则继续往下履行。假如完结了 resolveInstanceMethod: 但期望经过转发机制实际转发特定的选择器,则为这些选择器回来 NO

2. 动态加载

Objective-C 程序能够在运转时加载和链接新的类和类别。这些加载和链接的新代码被合并到程序中,它们与发动 App 时加载的类和类别同等对待。

动态加载能够用来做许多的工作。例如,体系偏好应用程序中的各种模块都是动态加载的。

在 Cocoa 环境中,一般运用动态加载来定制应用程序。咱们的程序能够在运转时加载其他人编写的程序模块,就像 Interface Builder 加载自界说调色板和 OS X System Preferences 应用程序加载自界说首选项模块相同。可加载模块扩展了应用程序的功用,咱们供给结构,其他人供给代码进行功用扩展。

虽然在 Mach-O 文件中有一个运转时函数履行 Objective-C 模块的动态加载(objc_loadModules,在 objc/objc-load.h 中界说),Cocoa 的 NSBundle 类为动态加载供给了一个更方便的接口,即一个面向目标并与相关服务集成的接口。

音讯转发

向一个不处理某个特定音讯的目标发送这个特定音讯是错误的。可是,在确定错误之前,运转时体系会给接纳目标一次时机来处理音讯。

1. 转发

假如咱们向不处理该音讯的目标发送音讯,则在承认 error 之前,运转时会向目标发送 forwardInvocation: 音讯,其中 NSInvocation 目标作为其唯一参数,而 NSInvocation 目标封装了原始音讯和传递的参数。

咱们能够完结一个 forwardInvocation: 办法对音讯进行默许处理,或许以其他办法避免错误。望文生义,forwardInvocation: 一般用于将音讯转发到另一个目标。

要想知道转发的规模和意图,能够幻想以下场景:首要,假定咱们正在规划一个函数名为 negotiate 的音讯的目标,而且咱们期望它的呼应包括另一种目标的呼应。咱们能够经过将 negotiate 音讯传递给咱们完结的 negotiate 办法主体中的某个目标,这样就能够完结这个操作了。

更进一步,假定咱们期望目标对 negotiate 音讯的呼应与在另一个类中完结的呼应完全相同。完结这个功用的一种办法是让咱们的类办法承继自另一个类的办法。可是,或许无法以这种办法完结功用,咱们的类和完结 negotiate 的类自身或许不具有承继结构。

即便咱们的类不能承继 negotiate 办法,咱们依然能够经过将音讯传递给另一个类的实例的办法来完结这个功用:

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

这种办法或许会有点费事,尤其是当咱们的目标会传递许多音讯给另一个目标时。有必要完结一个办法来掩盖咱们想从其他类借用的一切办法。此外,当咱们在编写代码,不知道或许想要转发的悉数音讯集时,就没办法处理这种状况。该集合或许依赖于运转时的工作,而且随着未来新办法和类的完结,它或许会发生变化。

forwardInvocation 供给的第2次时机:音讯为这个问题供给了一个动态的解决方案。它的作业原理是这样的:当一个目标由于没有匹配到与音讯中的选择器对应的办法而无法呼应音讯时,运转时体系会向目标发送 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:

2. 转发和多重承继

转发相似承继,能够给 Objective-C 程序供给相似多重承继的一些功用。 如下图,经过转发音讯来呼应音讯的目标借用或“承继”了另一个类中界说的办法完结。

iOS 探究 | 第八篇 Runtime 详细探究

在此图中,Warrior 类的实例将 negotiate 音讯转发到 Diplomat 类的实例。Warrior 会像 Diplomat 相同进行谈判。它也会呼应 negotiate 音讯,而且出于真实的目的,它确实会呼应(虽然实际上是外交官在做这项作业)。

因而,转发音讯的目标从承继层次结构的两个分支“承继”办法——它自己的分支和呼应音讯的目标的分支。在上面的示例中,Warrior 相似乎承继自 Diplomat 以及它自己的超类。

转发能够完结多承继能完结的绝大部分功用。可是,两者之间有一个重要的区别:多重承继在一个目标中完结了不同的功用。它更适用于大型、多面的目标,而转发将不同的功用分配给不同的目标。它将问题分解为更小的目标,但以一种显现的办法将音讯发送者相关到这些目标。

3. 署理目标

转发不仅能够模仿多承继,它还能够开发轻量级目标来代替或“掩盖”更本质的目标,署理能够代表一个目标并将它的一切音讯汇集到一同。

The Objective-C Programming Language 中的“长途音讯传递”中评论的署理是这样的,署理负责将音讯转发到长途接纳者,保证在衔接中仿制和检索参数等等。它不会仿制长途目标,而仅仅给长途目标一个本地地址,以便在另一个应用程序中能够经过这个地址接纳音讯。

其他种类的署理目标也是能够的。例如,假定咱们有一个操作很多数据的目标,它或许会创立一个杂乱的图像或读取磁盘上文件的内容。设置这个目标或许很耗时,所以咱们更或许会运用懒加载,在真的需求或许体系资源暂时闲暇的时分才去处理它。别的至少需求该目标的占位符,这样不至于干扰应用程序中的其他目标的正常运转。

在这种状况下,咱们一开端不必创立完好的目标,而是它的轻量级署理。这个目标能够自己做一些工作,例如回答有关数据的问题,但大多数状况下,它会为更大的目标保存一个方位,并在时机成熟时将音讯转发给它。当署理的 forwardInvocation: 办法第一次收到发往另一个目标的音讯时,它会保证该目标存在,假如不存在则创立它。较大目标的一切音讯都经过署理,因而,就程序的其余部分而言,署理和较大目标是相同的。

4. 转发和承继

如上所说转发相似承继,但 NSObject 类从不混淆两者。像 respondsToSelector:isKindOfClass: 这样的办法只检查承继结构,而不检查转发链。例如,假如问询 Warrior 目标是否呼应 negotiate 音讯,

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

即便它能够毫无错误地接纳 negotiate 音讯并将它们转发给 Diplomat 来呼应它们。(见上面图)答案依然是: 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: 在内的这些办法,以便在需求的时分运用他们。

留意: 这是个比较高阶的技能方案,仅适用于没有其他解决方案的状况,别的这种办法不能作为承继的替代品。假如咱们有必要运用此技能,就需求具体的了解需求进行转发的类及类的功用完结。

假如对 invokeWithTarget: 感兴趣,能够检查一下官方文档的具体介绍 NSInvocation。


总结

本文着眼于 NSObject 类以及 Objective-C 程序怎么与 Runtime 体系交互,特别是 Runtime 动态加载新类以及将音讯转发给其他目标的介绍。还介绍了怎么在程序运转时查找相关目标的信息等,看完本篇基本上对整个流程及流程中的细节完结会有一个比较全面体系的了解,如有疑问能够进群线上线下沟通沟通。以上就是本文对 Runtime 及动态音讯转发和解析作业原理及相关知识点的介绍,期望这篇文章对你有所帮助,感谢阅览。

参考资料:

Objective-C Runtime Programming Guide

Objective-C Runtime

NSInvocation

Objective-C Boot Camp

Message and Message Forwarding in Objective-C


关于技能组

iOS 技能组主要用来学习、分享日常开发中运用到的技能,一同坚持学习,坚持前进。文章仓库在这里:github.com/minhechen/i… 微信公众号:iOS技能组,欢迎联系进群学习沟通,感谢阅览。

iOS 探究 | 第八篇 Runtime 详细探究