Objective-C runtime 详解

2018-07-29  本文已影响0人  Acton393

Objective-C runtime 介绍

使用 runtime

Objective c 使用系统的 runtime 有三种形式:通过 objective-c 源码,通过 Foundation framework 里面的 NSObject class 和 直接调用 runtime 的函数。

objective-c 源码

大多数场景,我们仅写和编译Objectiv-C 代码,系统 runtime 底层被我们使用。编译包含 Objective-C 的 class 和 methods 的时候,编译器创建对应的数据结构和实现编程语言动态特性的函数调用。 创建的数据结构中包括 class 和 category 的定义,还有 protocol 的声明,还有对应的 methods,变量模板和其他从源码中提取的信息。最重要的 runtime 函数是我们后面要提到的发消息(message)。

NSObject methods

很多的 Cocoa 应用中的类都是继承于 NSObject, 所以也继承了NSObject 中定义的一些方法(NSProxy 不是NSObject 的子类)。对于这些类来讲,他们的实例就固有 NSObject 的这些方法和实现。在一些场景下 NSObject 只是定义了一个模板,它自己没有一些特定的实现逻辑。

比如 对于 description 方法,NSObject 的实现逻辑是返回一个类的描述字符串, 这个方法主要在使用GDB debug 过程中打印对象内容的时候用到。NSObject 的实现并不知道当前 class 有哪些成员,所以它只返回了当前对象的名字和地址,NSObject 的子类可以覆盖实现该方法返回更多信息,例如 Foundation 中的NSArray 类返回一个包含数组所有元素的列表。

​ 有些 NSObject中的方法只是从系统 runtime 查信息,例如 isKindOfClass:isMemberOfClass 这两个是测试类在继承层级中的位置; respondsToSelector: 可以让我们知道某个对象是否接受特定的消息;conformsToProtocol确认某个对象是否实现了特定protocol中定义的方法;methodForSelector可以让我们拿到函数的实现地址。

Runtime 函数

系统 runtime 是一个包含一些列方法和数据结构的一个公共接口,它的头文件在系统目录/usr/include/objc. 这里面很多函数可以让我们用纯 C 做编译器在编译 Objective-C时候为我们做的事情。其他基础的一些能力都暴露在 NSObject类中。我们可以用这些函数在系统 runtime 上开发其他的 interface 和开发环境的工具,在使用 Objective-c 时候编程时候一般使用不到

发消息

这部分主要介绍调用 oc 方法的表达式如何被转换到 objc_msgSend的函数调用,如何利用 objec_msgSend 避免动态绑定。

objc_msgSend 函数

Objective-C 只有在运行时,消息才被关联到方法实现。编译器转换如下发消息的表达式:

[receiver message]

objc_msgSend 的消息函数调用。 objc_msgSend 函数接受两个参数,一个是消息接受者,另外一个是发给消息接受者的消息名字,也就是方法名字,或者叫 method selector, 这是两个很重要的参数。

objc_msgSend(receiver, selector)

在发消息过程中传递的其他参数也会被转objc_msgSend 函数处理:

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

在动态绑定过程中 objc_msgSend 会完成所有需要做的事情:

上面发消息的关键在编译器为每个类和对象构建的数据结构。每个类的结构包含下面两个关键的部分:

当一个新的对象创建的时候,操作系统分配给它需要的内存,初始化成员变量。在对象的所有成员变量中有一个指向该对象的类结构体的指针。这个指针叫 isa, 通过这个指针,该对象可以访问到它的类, 通过它的类可有空访问到它的继承书中所有的类。

isa 指针是一个对象和 Objective-C 运行时交互的关键。一个对象的结构需要和 objc/objc.h 中定义的结构体 struct objc_object 一致。然后,很少我们需要自己创建root 对象, 一般都会直接继承 NSObject或者NSProxy,他们已经自动包含了 isa 变量

class 和 对象 的结构如下表:

image

向一个对象发消息时候,消息函数(objc_msgSend) 沿着这个对象的 isa 指针找到该对象的类的结构体,在当前类结构体的 函数表(dispatch table)中查找发送的消息的selector。如果找不到,消息函数(objc_msgSend)就继续沿着当前类中指向父类的指针找到父类的结构,在父类的函数表(dispatch_table) 继续查找。查找过程会一直沿着继承图一直找到 NSObject 类。一旦找到对应的方法,消息函数会直接调用对应的表中方法的函数实现,同时传给它参数和对象的数据结构。

这就是运行时选择方法实现的过程,或者用面向对象语言的术语来讲:方法动态绑定到消息的过程。

runtime 系统一般会为使用过的消息的 selector 和 函数指针做一个缓存,加速消息处理的过程,每个类都有一个这样独立的缓存,它同时包含了当前类定义的selector 和从父类中继承的方法。在搜索类的方法表之前,会先检查接受消息者对象的类的缓存(理论上使用过的方法很有可能再次被使用)。如果 selector 在缓存中,发消息仅仅比直接进行函数调用慢一点。只要一个程序运行的时间足够长,能够把所有的方法都缓存起来,几乎所有消息都能在缓存中找到对应的方法。随着程序的运行,缓存会动态的增长来容纳新消息。

使用影藏参数

当消息函数(objc_msgSend)找到一个selector 对应的方法实现时候,调用该方法时候时候回把所有的参数都传递过去,同时他也会传递一下两个隐藏参数:

消息表达式给每个方法实现的参数都是显式的参数信息。上面两个参数之所以被叫做隐藏参数,是因为上面两个参数并不在方法定义的源码中声明,上面两个参数在编译期间被动态的插入。

虽然这两个参数不是显式声明,源码仍然可以方法到他们。消息的接受者对象就是 self, 它的 selector 就是 _cmd。下面的例子中, _cmd 指向的selector 就是strangeself 就是接受 strange消息的对象。

- strange
{
    id  target = getTheReceiver();
    SEL method = getTheMethod();
 
    if ( target == self || method == _cmd )
        return nil;
    return [target performSelector:method];
}

self 是两个参数中最常用的一个,接受消息的对象的成员变量也可以在定义的方法中访问到。

获得方法地址

避免动态绑定的唯一方法是获取到方法的地址,直接调用(如果是一个函数)。当一个特定的方法被非常多次数调用的时候,而且你想避免前期的发消息查找的过程时候,这种场景可能比较适合。

通过 NSObject 类中定义的方法methodForSelector:, 你可以获得selector对应的函数指针,然后直接使用指针调用函数。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) 和 方法selector(_cmd)。这两个参数是方法语法中的隐藏参数,但是在直接调用函数的时候必须显示的被传递。

使用 methodForSelector: 避免动态绑定可以节约发消息过程中需要的大多时间。然而,这个节约只有特定消息被重复发送多次的时候,比如上面的 for循环中调用, 类似的场景才会有意义一些。

注意 methodForSelector: 方法是 Cocoa runtime 系统提供的, 不是 Objective-C 自己的一个特性。

动态方法解析

这个部分将会介绍如何动态提供一个方法实现。

动态方法解析

有一些场景我们需要动态提供方法实现,比如,Objective-C 声明属性的时候有一个特性包含 @dynamic 指令

@dynamic propertyName;

这个声明告诉编译器,跟这个属性相关的方法将会被动态提供。

你可以实现 resolvInstanceMethod: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: 或者instancesRespondsToSelector: 调用时候, 动态方法解析有机会给一个selector 提供一个函数实现(IMP)。如果你实现 resolveInstanceMethod: ,可以通过在方法中return NO来实现只有特定的selector通过转发机制被转发。

动态加载

Objective-C 程序可以在程序运行时动态加载和链接新类和扩展(category)。新代码被纳入程序,跟程序启动时候加载的类和扩展(category)一样对待。

利用动态加载可以做很多事情。比如,系统偏好应用中不同的模块 就是动态加载的。

在 Cocoa 环境中,动态加载通常被用来允许应用自定义。其他人可以写一些你的程序在运行期加载的模块。加载的模块扩展程序功能。其他人用你允许的方式贡献你不期望自己定义的一些功能。你提供框架,其他人提供具体的实现。

虽然运行时函数也有通过 Mach-O 文件执行动态加载 Objective-C 模块的能力(objc_loadModules,在objc/objc-load.h), NSBundle 提供了更方便的动态加载的接口。

消息转发

发消息给一个不能处理该消息的对象是会产生错误。但是在产生错误结果之前,系统 runtime 会再次给当前对象机会处理该消息。

转发

如果发消息给一个不能处理该消息的对象,在通知错误结果之前,runtime发送 forwardInvocation:给该对象,同时会带上一个 NSInvocation 对象作为参数。这个NSInvocation对象包含了原来消息和参数。

你可以实现 forwardInvocation:方法,给消息提供一个默认的响应,或者用其他的方式来避免这个错误。就像这个方法名字暗含的意义,forwardInvocation: 通常用来转发消息给另外一个对象。

为了看到转发的范围和意图,假设如下场景:首先你正在设计一个可以相应 negotiate详细的对象,而且你想让它的相应包含其他对象的相应。你可以通过传递 negotiate消息给其他对象很容易完成这个。

进一步,假设你想让你的对象对 negotiate的相应是在其他类中实现的相应。一个方法是通过让你的类继承其他类。然而,你的类和实现negotiate方法的类在不同继承树中(OC 不支持多继承)。

及时你的类不能继承 negotiate 方法,你仍然可以通过实现一个传递消息给其他类对象的方法:

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

这样的实现方式可能有些笨重,尤其是你想转发很多消息给其他对象的时候。必须实现一个覆盖每一个你想转发到其他对象的方法。

forwardInvocation: 消息提供的第二次处理消息的机制。它的机制是当一个对象无法处理某一个消息时候,runtime 会通过发送forwardInvocation:消息通知这个对象。每一个类都从 NSObject继承了forwardInvocation:这个方法。然而 NSObject实现这个方法仅仅是调用 doesNotRecognizeSelector:. 通过覆盖NSObject的实现,你可以利用 forwardInvocation:这个消息的机会转发消息到其他对象。

要转发一个消息,所有的 forwardMethodInvocation:方法需要:

发送消息可以通过 invokeWithTarget:方法:

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

被转发的消息的返回值会被返回到原来的发送者。所有类型的返回值都可以被返回给发送者,包括 id,结构体,单双进度的浮点数。

forwardInvocation:方法看起来像不能处理的消息的分发中心,打包不能处理的消息到其他不同的接受者。或者说是一个转送站,发送所有的消息到同一个地方。它可以把一个消息转换到另外一个消息, 也可以直接吞掉某些消息,因此被吞掉的消息既没有错误页没有响应,forwardInvocation: 方法也可以对多个消息发送一个响应,forwardInvocation:能做什么还得看具体的实现细节。

forwardInvocation:方法只处理那些对对象发送不存在的消息或者方法。例如,你想让你的类可以转发 negotiate消息到其他类,那就是说你的类不能有negotiate方法,否则的话,消息不会到达forwardInvocation:

转发和多继承

消息转发机制也模仿了继承机制,它也可以让Objective-C程序具有多继承 特性,如下图所示,一个对象对一个消息的响应过程似乎是通过在“继承”树中其他的类中去查找方法实现。

image

Warriorclass 的对象实例转发negotiate消息给Diplomat类的实例。 Warrior类的实例看起来像跟Diplomat类的实例一样可以响应 negotiate 消息(虽然实际是Diplomat实例响应的消息)。

从继承树的两个分支(它自己的继承分支和转到到的对象的继承分支)转发消息。上面的例子,看起来是 Warrior类继承了Diplomat类。

转发机制提供了多继承的可能性。然而这两个之间有一个很重要的区别:多继承会结合不同的能力到一个类,而转发只是分配不同的相应到其他对象,把一个大问题拆分为几个小的问题,用一种方式关联起来,对消息发送者透明。

转发和继承

虽然转发看起来有点像继承,但是 NSObject类对他们区分的却很明显。 respondsToSelector:isKindOfClass:这个两个方法只查找继承树,并不会在转发链上查找。例如 Warrior对象如下调用:

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

结果是 NO, 即使它可以接受 negotiate消息,返回正确的结果,中间没出任何错误。

上一篇 下一篇

猜你喜欢

热点阅读