Objective-C runtime 消息传递与转发

栏目: Objective-C · 发布时间: 5年前

内容简介:Objective-C 本质上是一种基于 C 语言的领域特定语言。Objective-C 通过一个用 C 语言和汇编实现的 runtime,在 C 语言的基础上实现了面向对象的功能。在 runtime 中,对象用结构体表示,方法用函数表示。C 语言是一门静态语言,其在编译时决定调用哪个函数。而 Objective-C 则是一门动态语言,其在编译时不能决定最终执行时调用哪个函数(Objective-C 中函数调用称为消息传递)。Objective-C 的这种动态绑定机制正是通过 runtime 这样一个中间

Objective-C 本质上是一种基于 C 语言的领域特定语言。Objective-C 通过一个用 C 语言和汇编实现的 runtime,在 C 语言的基础上实现了面向对象的功能。在 runtime 中,对象用结构体表示,方法用函数表示。

C 语言是一门静态语言,其在编译时决定调用哪个函数。而 Objective-C 则是一门动态语言,其在编译时不能决定最终执行时调用哪个函数(Objective-C 中函数调用称为消息传递)。Objective-C 的这种动态绑定机制正是通过 runtime 这样一个中间层实现的。

Objective-C runtime 消息传递与转发

为了分析 runtime 是如何进行动态绑定,我们首先需要了解一下 Objective-C 中类与对象等基本结构在 C 语言层面是如何实现的。

数据结构

Objective-C 类

Objective-C 类是由 Class 类型表示的,它本质上是一个指向 objc_class 结构体的指针。如下所示为 objc/runtime.h 中关于类的定义:

typedef struct object_class *Class

struct object_class{
    Class isa OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
     Class super_class                        OBJC2_UNAVAILABLE;  // 父类
     const char *name                         OBJC2_UNAVAILABLE;  // 类名
     long version                             OBJC2_UNAVAILABLE;  // 类的版本信息,默认为0
     long info                                OBJC2_UNAVAILABLE;  // 类信息,供运行期使用的一些位标识
     long instance_size                       OBJC2_UNAVAILABLE;  // 该类的实例变量大小
     struct objc_ivar_list *ivars             OBJC2_UNAVAILABLE;  // 该类的成员变量链表
     struct objc_method_list *methodLists     OBJC2_UNAVAILABLE;  // 方法定义的链表
     struct objc_cache *cache                 OBJC2_UNAVAILABLE;  // 方法缓存
     struct objc_protocol_list *protocols     OBJC2_UNAVAILABLE;  // 协议链表
#endif
} OBJC2_UNAVAILABLE;
/* Use `Class` instead of `struct objc_class *` */

Objective-C 对象

Objective-C 对象是由 id 类型表示的,它本质上是一个指向 objc_object 结构体的指针。如下所示为 objc/objc.h 中关于对象的定义:

typedef struct objc_object *id;

struct objc_object{
     Class isa OBJC_ISA_AVAILABILITY;
};

objc_object 结构体中只有一个成员,即指向其类的 isa 指针。 当向一个 Objective-C 对象发送消息时,runtime 会根据实例对象的 isa 指针找到其所属的类。Runtime 会在类的方法列表以及父类的方法列表中去寻找与消息对应的 selector 指向的方法,找到后即运行该方法。

Objective-C 元类(meta class)

meta-class 是一个 类对象的类 。在 Objective-C 中,所有的类本身也是一个对象。事实上,在很多原型编程语言也采用这种“万物皆对象”的设计思想,如:Io。

通过向该对象发送消息,即可实现对类方法的调用。前提是类的 isa 指针必须指向一个包含这些类方法的 objc_class 结构体。 meta-class 中存储着一个类的所有类方法。所以,类对象的 isa 指针指向的就是 meta-class

  • 当向一个对象发送消息时,runtime 会在这个对象所属的类的方法列表中查找方法。
  • 当向一个类发送消息时,runtime 会在这个类的 meta-class 的方法列表中查找。

思考一下, meta-class 也是一个类,也可以向它发送一个消息,那么它的 isa 又是指向什么呢?为了不让这种结构无限延伸下去,Objective-C 的设计者让所有的 meta-classisa 指向基类的 meta-class ,以此作为它们的所属类。

下图所示,为 Objective-C 对象在内存中的引用关系图。

Objective-C runtime 消息传递与转发

Objective-C 方法

方法实际上是一个指向 objc_method 结构体的指针。如下所示为 objc/runtime.h 中关于方法的定义:

typedef struct objc_method *Method

struct objc_method{
    SEL method_name      OBJC2_UNAVAILABLE; // 方法名
    char *method_types   OBJC2_UNAVAILABLE;
    IMP method_imp       OBJC2_UNAVAILABLE; // 方法实现
}

结构体中包含成员 SELIMP ,两者将方法的名字与实现进行了绑定。通过 SEL ,可以找到对应的 IMP ,从而调用方法的具体实现。

SEL

SEL 又称选择器,是一个指向 objc_selector 结构体的指针。

typedef struct objc_selector *SEL;

方法的 selector 用于表示运行时方法的名字。 Objective-C 在编译时,会根据方法的名字(不包括参数)生成一个唯一的整型标识( Int 类型的地址),即 SEL

由于一个类的方法列表中不能存在两个相同的 SEL ,所以 Objective-C 不支持重载。但是不同类之间可以存在相同的 SEL ,因为不同类的实例对象执行相同的 selector 时,会在各自的方法列表中去根据 selector 去寻找自己对应的 IMP

通过下面三种方法可以获取 SEL

sel_registerName
@selector()
NSSeletorFromString()

IMP

IMP 本质上就是一个函数指针,指向方法实现的地址。

typedef id (*IMP)(id, SEL,...);

参数说明

  • id :指向 self 的指针(如果是实例方法,则是类实例的内存地址;如果是类方法,则是指向元类的指针)
  • SEL :方法选择器
  • ... :方法的参数列表

SELIMP 的关系类似于哈希表中 keyvalue 的关系。采用这种哈希映射的方式可以加快方法的查找速度。

消息传递(方法调用)

在 Objective-C 中, 消息直到运行时才绑定到方法实现上 。编译器会将消息表达式 [receiver message] 转化为一个消息函数的调用,即 objc_msgSend 。这个函数将消息接收者和方法名作为主要参数,如下所示:

objc_msgSend(receiver, selector)                    // 不带参数
objc_msgSend(receiver, selector, arg1, arg2,...)    // 带参数

objc_msgSend 通过以下几个步骤实现了动态绑定机制。

  • 首先,获取 selector 指向的方法实现。由于相同的方法可能在不同的类中有着不同的实现,因此根据 receiver 所属的类进行判断。
  • 其次,传递 receiver 对象、方法指定的参数来调用方法实现。
  • 最后,返回方法实现的返回值。

消息传递的关键在于前文讨论过的 objc_class 结构体,其有两个关键的字段:

  • isa :指向父类的指针
  • methodLists : 类的方法分发表( dispatch table

当创建一个新对象时,先为其分配内存,并初始化其成员变量。其中 isa 指针也会被初始化,让对象可以访问类及类的继承链。

下图所示为消息传递过程的示意图。

Objective-C runtime 消息传递与转发

  • 当消息传递给一个对象时,首先从运行时系统缓存 objc_cache 中进行查找。如果找到,则执行。否则,继续执行下面步骤。
  • objc_msgSend 通过对象的 isa 指针获取到类的结构体,然后在方法分发表 methodLists 中查找方法的 selector 。如果未找到,将沿着类的 isa 找到其父类,并在父类的分发表 methodLists 中继续查找。
  • 以此类推,一直沿着类的继承链追溯至 NSObject 类。一旦找到 selector ,传入相应的参数来执行方法的具体实现,并将该方法加入缓存 objc_cache 。如果最后仍然没有找到 selector ,则会进入 消息转发 流程(下文将进行介绍)。

消息转发

当一个对象能接收一个消息时,会走正常的消息传递流程。当一个对象无法接收某一消息时,会发生什么呢?默认情况下,如果以 [object message] 的形式调用方法,如果 object 无法响应 message 消息时,编译器会报错。如果是以 performSeletor: 的形式调用方法,则需要等到运行时才能确定 object 是否能接收 message 消息。如果不能,则程序崩溃。

对于后者,当不确定一个对象是否能接收某个消息时,可以调用 respondsToSelector: 来进行判断。

if ([self respondsToSelector:@selector(method)]) {
    [self performSelector:@selector(method)];
}

事实上,当一个对象无法接收某一消息时,就会启动所谓“消息转发(message forwarding)”机制。通过消息转发机制,我们可以告诉对象如何处理未知的消息。

消息转发机制大致可分为三个步骤:

  • 动态方法解析(Dynamic Method Resolution)
  • 备用接收者
  • 完整消息转发

下图所示为消息转发过程的示意图。

Objective-C runtime 消息传递与转发

动态消息解析

对象在接收到未知的消息时,首先会调用所属类的类方法 +resolveClassMethod: 或实例方法 +resolveInstanceMethod:

在这两个方法中,我们可以为未知消息新增一个“处理方法”,通过运行时 class_addMethod 函数动态添加到类中。比如:

void dynamicMethodIMP(id self, SEL _cmd) {
    // 方法实现
}

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

这种方案更多的是为了实现@dynamic属性。

备用接收者

如果在上一步无法处理消息,则 runtime 会继续调用 - (id)forwardingTargetForSelector:(SEL)aSelector 方法。

如果一个对象实现了这个方法,并返回一个非 nil (也不能是 self ) 的对象,则这个对象会称为消息的新接收者,消息会被分发到这个对象。比如:

- (id)forwardingTargetForSelector:(SEL)aSelector {
    NSString * selString = NSStringFromSelector(aSelector);
    if ([selString isEqualToString:@"walk"]) {
        return self.otherObject;
    }
    return [super forwardingTargetForSelector:aSelector];
}

这一步合适于我们只想将消息转发到另一个能处理该消息的对象上。但这一步无法对消息进行处理,如操作消息的参数和返回值。

完整消息转发

如果在上一步还不能处理未知消息,则唯一能做的就是启用完整的消息转发机制了。

这步调用 methodSignatureForSelector 进行方法签名,这可以将函数的参数类型和返回值封装。如果返回 nil ,则说明消息无法处理并报错 unrecognized selector sent to instance

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    if ([NSStringFromSelector(aSelector) isEqualToString:@"testInstanceMethod"]){
        return [NSMethodSignature signatureWithObjcTypes:"v@:"];
    }  
    return [super methodSignatureForSelector: aSelector];
}

如果返回 methodSignature ,则进入 forwardInvocation 。对象会创建一个表示消息的 NSInvocation 对象,把与尚未处理的消息有关的全部细节都封装在 anInvocation 中,包括 selectortarget ,参数。在这个方法中可以修改实现方法,修改响应对象等,如果方法调用成功,则结束。如果依然不能正确响应消息,则报错 unrecognized selector sent to instance

- (void)forwardInvovation:(NSInvocation)anInvocation {
    [anInvocation invokeWithTarget:_helper];
    [anInvocation setSelector:@selector(run)];
    [anInvocation invokeWithTarget:self];
}

可以利用备用接受者和完整消息转发实现对接受消息对象的转移,可以实现“多重继承”的效果。

参考

  1. Objective-C Runtime Programming Guide
  2. Objective-C Runtime · 笔试面试知识整理
  3. iOS运行时(Runtime)详解+Demo
  4. iOS内功篇:runtime

以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

运营制胜

运营制胜

张恒 / 电子工业出版社 / 2016-10-1 / 65

《运营制胜——从零系统学运营构建用户增长引擎》主要从内容运营、用户运营、推广运营三个方向来介绍产品运营方面的知识。 其中内容运营主要介绍了内容生成的机制、内容方向设定、内容输出、内容生产引擎、内容推荐机制、数据如何驱动内容运营、内容运营的KPI 设定、建立内容库、内容的赢利模式。用户运营主要介绍了产品的冷启动、获得种子用户及早期用户、建立用户增长引擎、利用心理学引爆产品用户增长、增加用户活跃......一起来看看 《运营制胜》 这本书的介绍吧!

CSS 压缩/解压工具
CSS 压缩/解压工具

在线压缩/解压 CSS 代码

Base64 编码/解码
Base64 编码/解码

Base64 编码/解码

MD5 加密
MD5 加密

MD5 加密工具