iOS runtime 官方文档解析

在 iOS 开发中,runtime时常作为Objective-C这门语言底层的代名词,代表着复杂和难懂,但同时充满着黑魔法的吸引力。事实上,runtime的确有这些特性,runtime内容很多,作用也很多,可能很难说清楚。但是作为一名合格的程序员,应该要去试着弄清楚runtime这一底层机制。今天,我也试着谈谈自己对runtime的理解。

本文并不会谈runtime的实际使用场景,而是以苹果的官方文档为基准对其原理进行分析,毕竟,人家是官方嘛~ 下面引用了苹果官方文档对runtime的总体介绍,我们便从此处开始谈起:

The Objective-C language defers as many decisions as it can from compile time and link time to runtime. Whenever possible, it does things dynamically. This means that the language requires not just a compiler, but also a runtime system to execute the compiled code. The runtime system acts as a kind of operating system for the Objective-C language; it’s what makes the language work.

Objective-C这门语言会尽可能地将许多决定从编译期推迟到运行时。它会在任何可能的时候动态地处理事情。这意味着这门语言需要的不仅仅是编译器,同时还需要运行时系统来执行编译后的代码。运行时系统好像就是Objective-C的操作系统一样,让这门语言能够运作。

This document looks at the NSObject class and how Objective-C programs interact with the runtime system. In particular, it examines the paradigms for dynamically loading new classes at runtime, and forwarding messages to other objects. It also provides information about how you can find information about objects while your program is running.

这篇文档探究了NSObject这个类以及Objective-C项目是如何和运行时系统交互的;讨论了在运行时动态加载新类,以及给其他对象转发消息的范例;它还会教我们如何在程序运行的时候获取对象的信息。

You should read this document to gain an understanding of how the Objective-C runtime system works and how you can take advantage of it. Typically, though, there should be little reason for you to need to know and understand this material to write a Cocoa application.

你应该阅读这篇文档来理解Objective-C运行时系统是如何工作的以及如何利用它。虽然,在写Cocoa应用的时候你通常并没有太多的理由需要去理解这一份素材...


知道了苹果要我们在这篇文档中理解和掌握的内容后,让我们来一个个的进行解析。

一、项目中与 runtime 交互的三种不同形式:

在我们平常写代码的时候,其实经常直接或者间接的和runtime打着交道,只不过有些时候我们并没有发觉到罢了。Objective-C项目共有三种方式和runtime进行交互,分别是:通过源代码NSObject中定义的方法直接调用runtime函数

通过源代码

我们在写oc方法的时候,通常会通过[object foo]这样一种形式进行调用,我们称之为消息发送。编译器看到此消息后,会将其转换为一条标准的 C 语言函数调用,所调用的函数乃是消息传递机制中的核心函数,叫做objc_msgSend,关于这个函数,我们会在下文中进行详细的探讨。所以,编译器其实一直作为一个桥梁帮我们和runtime系统进行着交互。

通过 NSObject 类中定义的方法

NSObject中定义了很多的方法,有些方法的实现很简单,如description方法只返回了类名和对象地址,因此子类可以重写这些方法而得到运行时它们更多的信息。而有些方法提供给对象自省的功能,它们能够直接的返回对象在运行时的信息,比如isMemberOf:方法会判断该对象是否是某类的对象。

通过直接调用 runtime 函数

最直接的方式当然是调用runtime函数。某些函数其实就是编译器转换消息后生成的函数,某些则形成了NSObject方法奏效的基础。这些方法一般不需要用到,但在有些特定的情况下非常有用。所有函数都定义在 Objective-C Runtime Reference.


二、消息机制

Objective-C中,直到运行时消息才会绑定方法实现。 编译器会将Objective-C的消息转化为runtime的一个函数:objc_msgSend(),这个方法会将消息接收者以及方法选择子,也就是方法名作为它的两个默认参数,消息中其他参数也会一并交给这个函数。

这个函数会做三件事情:

1.根据类名寻找方法选择子所对应的方法实现
2.调用方法实现,将对象和参数传递给它
3.将方法实现的返回值作为自己的返回值进行传递

我们主要来看第一件事:根据类名寻找方法选择子所对应的方法实现。在解释这件事之前,我们需要先了解一下类和对象的结构,每个类结构中包含两个重要的元素:

1.指向父类的指针。
2.一个类的分发表。这个表拥有将方法选择器和方法地址联系在一起的条目。

每个对象都有一个isa指针指向对应的类对象,在运行时,runtime系统会根据这个指针去找到相对应的类对象,然后根据方法选择子去方法链表中查找对应的实现地址,如果找到,直接将地址传递给对象进行操作,如果没有找到,那么根据superclass指针去父类当中寻找,以此类推直到NSObject类。这就是动态绑定,也就是函数objc_msgSend做的第一件事。可参见下图:

messaging Framework

接下来的两件事就不过多解释了,按照字面意思理解便可。

隐式参数:

objc_msgSend()找到方法实现并将参数传递给它的时候,还有两个隐藏的参数一并传给了它,分别是:

1.接收消息的对象self
2.方法选择子_cmd

之所以叫它们隐式参数,是因为它们并没有出现在源代码消息的参数中,但是它们的确会作为参数传递给每个方法实现。不知道大家有没有想过一个问题:为什么我们可以在每个方法实现中使用self?其实就是因为self作为隐式参数传递进来了。

如何避免动态绑定:

避免进行动态方法绑定的唯一方式就是找到方法的地址然后像函数一样调用它,它的使用场景是多次循环调用同一个方法。通过使用NSObject中的methodForSelector:方法能够得到地址,代码如下:

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);

}

三、动态方法解析:

这一个章节将会告诉你如何动态的添加方法实现。为什么要动态的添加呢?因为,不是所有情况下都能够动态绑定到方法实现,此时运行时系统会给我们一次动态添加方法实现的机会,如果还是没能处理这条消息,那么接着会进行消息转发,关于消息转发,我们会在下章进行讨论。

如果你想动态的给一个方法添加实现,可以使用动态方法解析,比如属性使用@dynamic关键词,他告诉编译器setter getter方法会动态的提供。

resolveInstanceMethod:resolveClassMethod:分别为对象和类动态添加方法。这两个方法可以根据SEL调用class_addMethod()函数来添加方法,具体代码可以参照下面:

+ (BOOL)resolveInstanceMethod:(SEL)aSEL {
     
     if (aSEL == @selector(resolveThisMethodDynamically)) {
     
          class_addMethod([self class], aSEL, (IMP) dynamicMethodIMP, "v@:");
     
          return YES;
     }
     
     return [super resolveInstanceMethod:aSEL];
}

其中 dynamicMethodIMP是自定义的函数名,这个函数会和aSEL这个选择子绑定,那么当调用resolveThisMethodDynamically这个方法的时候就会去调用自定义的函数了。

Objective-C项目运行的时候,不仅能动态的添加方法,还能够动态的添加类和类目,这些新代码和普通的类和类目同等对待。

动态加载可以用于很多地方,比方说系统的偏好设置。在Cocoa开发中,动态加载一般用于项目的定制化。其他开发者可以开发运行期的模块,就像Interface Builder添加调色板和MacOS系统加载自定义的设置模块。可加载模块使项目得以扩展,你只需要提供框架而其他开发者提供代码。


四、消息转发

将消息发送给一个无法处理消息的对象会报错,但是运行时系统会在报错前额外再给我们一次机会来处理这条消息。这就涉及到一个方法forwardInvocation:,系统会在报错前向对象调用这个方法,这个方法有一个参数NSInvocation,它会封装原始的方法以及附带的参数。

为了探究消息转发的原理,让我们来设想一个场景。如果你想设计一个对象能够响应negotiate这个消息,但是你希望转交给另外一个对象来响应,那么你很容易想到这样做:

- (id)negotiate
{
    if ( [someOtherObject respondsTo:@selector(negotiate)] )

        return [someOtherObject negotiate];

    return self;
}

但是,让我们更进一步的设想,如果你不想让自己的类实现这个方法,也就是在实现文件中没有这个方法的实现,那又该如何处理这条消息呢?你可能会想到用继承的方式,让父类去实现这个方法,但是,你现在这个类不一定和实现这个方法的类在同一个继承树上。

即使无法继承,你仍然可以通过上述代码的方式将消息转交给其他对象。但是,如果想要转发的消息很多,你就需要为每一个消息提供实现,这样工作量很大。而且,当你在编写代码的时候并不知道所有要转发的消息,这个情况是无法处理的。这些方法可能依赖运行时的事件,并且可能会随着新的方法和类的实现而改变。

这时候就要用到forwardInvocation:这个方法了,通过实现这个方法,你可以集中的进行处理,也就是在运行时将需要转发的方法提供给其他对象。

这个方法需要做两件事:

1.决定消息的去向
2.将消息和其参数一去发送过去

具体实现可参照下述代码:

- (void)forwardInvocation:(NSInvocation *)anInvocation
{
    if ([someOtherObject respondsToSelector:[anInvocation selector]]) {

        [anInvocation invokeWithTarget:someOtherObject];
    } 
    else {

        [super forwardInvocation:anInvocation];
   }
}

值得注意的是,被转发消息的返回值会返回给最初的发送者,所有类型的返回值都可以被发送给这个调用者,包括id对象、结构体、浮点值。

forwardInvocation:这个方法其实扮演了未识别方法unrecognized messages的分发中心的角色,它会将消息包裹起来发送给不同的接受者。或者是一个中转站,将所有消息发送到同一个目的地。它可以将一个消息转换成另一个消息,或者吞噬掉一部分消息,这样既没有响应者也不会报错。它还可以将多条消息融合成一个响应。总结的来说,让转发响应链中的对象建立联系,给程序设计提供了各种可能性。

要注意的是,如果要让某对象不响应某条消息而走forwardInvocation:这个方法,那么你不能在该类中提供这个消息的实现。

消息转发和多继承

转发模仿了继承,并且可以将一些多继承的元素借鉴到Objective-C项目中。如下图所示,通过转发来响应消息的对象看起来像是借了或者是继承了另外一个类的方法实现:

Forwarding

在这个图例中,Warriors勇士类的的对象将negotiate谈判这条消息转发给Diplomat外交官这个类的对象。看起来勇士像一名外交官一样在谈判,这似乎是勇士这个对象响应了谈判的消息,实际效果来看确实响应了(虽然是外交官真正在做这件事)

转发这条消息的的对象因此“继承了”了两个分支的继承树上的方法——它自己的继承树分支和响应此消息的对象的继承树分支。

然而转发和多继承是有不同点的:

1.多继承是将不同能力组合到单一的对象上,它用于获得大的、复杂的对象。
2.转发是将不同的职责赋予不同的对象,它将问题分解给不同的对象,并通过一种对消息发送者透明的方式将这些对象联系在一起。

代理对象

消息转发不仅是模仿了多继承,而且使开发轻量级对象代替重量级对象成为可能。代理对象代表其他对象并且将消息发送给它。

proxy就是这样一个代理对象,它负责将消息转发给远程接收者的管理细节,确保参数值被拷贝并能在连接中取回,但它并不会做更多的事,它并不会复制远程对象的功能但是会给远程对象一个地址,从这个地址中可以获得其他应用的地址。

也可能会有其他类型的代理。比如说,现在有一个处理大量数据的对象,创建这个对象很耗时,所以需要懒加载它,也就是当需要它时或者系统空闲时创建它。此时,你至少需要一个占位的对象使程序中其他对象能够正确的调用它。

在这种情况下,你并不会创建整个对象,而是用一个代理来代替它,这个代理对象自己可以做一些事情,比如回答有关数据的问题,但是它整体上而言还是作为一个代理,并在适当的时候将消息转发给代替的对象。当代理的forwardInvocation:方法第一次接受到去往其他对象的消息的时候,它就会确保它代替的对象存在——在对象未被创建的时候创建它。所有的消息都会经过代理,所以对程序中其他对象而言,代理和它替代的对象是一样的。

消息转发和继承

虽然转发模仿了继承,但是NSObject并不会将两者混淆,比如像respondsToSelector:isKindOfClass:这样的方法只会查看继承树而不会查看转发链,例如要查看一个Warrior对象能否响应negotiate方法,答案则为NO,即使它能够无报错的处理它。

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

    // 并不会进入这个条件

}

如果某个对象 A 将消息转发给另外一个对象 B,又想让 A 真的看起来像是继承了 B 的行为,那么需要重写respondsToSelectorisKindOfClass 来添加转发机制,

- (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;

}

通过这个方法你能够得到代理响应这条消息的所用方法的详细信息。

推荐阅读更多精彩内容