Runtime

iOS 熟知必会系列

Posted by 水水 on October 5, 2018

iOS Runtime 学习笔记

除了面试中我们会经常被问到 iOS Runtime 相关知识,作为一个优秀的 iOSer Runtime 这种底层的基础知识应该是熟知必会的,只有对使用的语言充分深入了解,我们才能更加灵活的使用它。

揭秘 Runtime 原理

Runtime 介绍

通过学习编译原理我们知道一般来说高级编程语言想要转换成可执行文件需要先编译成为汇编语言再转换成机器语言 (也就是 0 1 的这种机器码) 但是由于 Objective-C 是一门动态语言所以并不能一开始就被编译为汇编语言而是需要先转写成纯 C (这个部分便是 Runtime 做的) 然后再转换成汇编最后转换成机器码,我们也知道 Objective-C 是一门面向对象的语言而 C 语言是一门面向过程的编程语言因此 Runtime 也可以理解成是将面向对象的类转变城面向过程的结构体。目前我们开发使用的 Objective-C 2.0 采用的是 modern 版本的 Runtime,其只能运行在 iOSMacOS 10.5 之后的 64 位程序中。

Runtime 消息传递 (messaging)

什么是消息传递

对象调用方法的过程就是一个消息传递,执行 [obj meth] 时编译器会将这个转换成消息发送 objc_msgSend(obj,meth) 函数。

实现消息传递

首先先复盘一下消息传递的流程

  • 首先通过obj (对象) 的isa指针找到它所对应的类对象 ;
  • 在类对象的 method_list 找 foo 即 selector 方法;
  • 如果 class 中没到 foo,继续往它的 superclass 的 method_list 中查找;
  • 一旦找到 foo 这个函数,就去执行它的 IMP;
  • 转发 IMP 的 return 值.

看完这个基本流程会发现其中涉及了这么几个东西,对象 (object),对象对应的类 (class),类中方法 (method),那么接下来通过解读这几个东西源码我们似乎应该就可以将消息传递搞懂一大部分了。

//对象
struct objc_object {
    Class isa  OBJC_ISA_AVAILABILITY;
};
//类
struct objc_class {
    Class isa  OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
    Class super_class                                        OBJC2_UNAVAILABLE;//OC 2.0 中已经没有了
    const char *name                                         OBJC2_UNAVAILABLE;
    long version                                             OBJC2_UNAVAILABLE;
    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;
//方法列表
struct objc_method_list {
    struct objc_method_list *obsolete                        OBJC2_UNAVAILABLE;
    int method_count                                         OBJC2_UNAVAILABLE;
#ifdef __LP64__
    int space                                                OBJC2_UNAVAILABLE;
#endif
    /* variable length structure */
    struct objc_method method_list[1]                        OBJC2_UNAVAILABLE;
}                                                            OBJC2_UNAVAILABLE;
//方法
struct objc_method {
    SEL method_name                                          OBJC2_UNAVAILABLE;
    char *method_types                                       OBJC2_UNAVAILABLE;
    IMP method_imp                                           OBJC2_UNAVAILABLE;
}

接下来补充讲解一下消息传递中用到的一些概念:

  • 类对象 (objc_class)
  • 实例 (objc_object)
  • 元类 (Meta Class)
  • Method (objc_method)
  • SEL (objc_selector)
  • IMP (Implemetation)
  • 类缓存 (objc_cache)
  • Category (objc_category)

实例 (objc_object)

// Represents an instance of a class.
struct objc_object {
    Class isa  OBJC_ISA_AVAILABILITY;
};

// A pointer to an instance of a class.
typedef struct objc_object *id;

类对象 (objc_class)

Objective-C 中的类跟我们通常了解的类不一样,它也是一个对象,代码中用 Class 表示,看 Reference 中可以知道它其实就是一个指向 objc_class 的结构体指针,在上面的代码中有 objc_class 中的具体内容,通过命名不难发现, 结构体里保存了指向父类的指针、类的名字、版本、实例大小、实例变量列表、方法列表、缓存、遵守的协议列表等,一个类中所需要的信息也就是这些啦,这个结构体当中存储的数据被称作为 元数据(metadata) 当然你仔细看得话你也会发现这个结构体中也有 isa 这也就印证了我们之前说明的 Objective-C 当中的类也是对象,类对象在编译期产生,用来创建实例对象,是单例。

元类 (MetaClass)

通过之前的讲解我们知道类实例是通过类对象创建的,那类对象又是通过什么产生的呢?因为我们引入了 元类 ,类对象的 isa 指向的就是 元类 元类 (Meta Class) 是创建类对象的一个类。由于类本身也是对象,我们可以向这个对象发送消息(即调用类方法)。为了调用类方法,这个类的 isa 指针必须指向一个包含这些类方法的一个 objc_class 结构体。这就引出了 meta-class 的概念,元类中保存了创建类对象以及类方法所需的所有信息。任何 NSObject 继承体系下的 meta-class 都使用 NSObjectmeta-class 作为自己的所属类,而基类的 meta-classisa 指针是指向自己。

经典的消息传递结构图 (但是我个人还是觉得掘金上的这位大佬的绘制更合适一些,横轴和纵轴的结合会更加清晰)

Method (objc_method)

Method 翻译成中文就是方法其实和我们平时编写的函数是一个东西,看一下 objc_method 相关的源码

typedef struct objc_method *Method;
struct objc_method {
    SEL method_name                                          OBJC2_UNAVAILABLE;
    char *method_types                                       OBJC2_UNAVAILABLE;
    IMP method_imp                                           OBJC2_UNAVAILABLE;

Method 的定义方式与之前的 Class 颇有相似都是结构体指针只是定义不同,源码中我只选取了部分我会分析的地方,如果你对它的整体有兴趣的话可以去官网看一下完整的代码。

通过分析命名可以知道:

  • SEL method_name 方法名
  • char *method_types 方法类型
  • IMP method_imp 方法实现

接下来逐个分析

SEL (objc_selector)

//objc.h
typedef struct objc_selector *SEL;

SEL 是 selector 在 Objective-C 中的表示类型,selector 是方法选择器可以理解成是区分方法的 ID 而这个 ID 的类型是 SEL

@property SEL selector;

其实 selector 就是映射到对应方法的 C 语言字符串,你可以用 Objective-C 编译器命令 @selector() 或者 Runtimesel_registerName 函数来获得一个 SEL 类型的方法选择器。

selector 既然是一个string,感觉应该是类似className+method 的组合,命名规则有两条:

  • 同一个类,selector 不能重复
  • 不同的类,selector 可以重复

这也带来了一个弊端,我们在写 C 语言 代码的时候,经常会用到函数重载,就是函数名相同,参数不同,但是这在 Objective-C 中是行不通的,因为 selector 只记了 method 的 name,没有参数,所以没法区分不同的 method。

IMP (Implementation)

下面的代码是对 IMP 的定义:

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

是的一开始你可能看不懂,因为你对 C 中的 typedef 可能还不太了解可以去了解一下(方便后面读源码哈)这个地方 IMP 就是一个返回值类型为 id 参数为 (id, SEL, …) 的一个函数指针类型的别名 ( C 语言当中起别名主要是为了方便编码) 因此 IMP 其实就是只想函数实现体的指针类型,在 iOS 的 Runtime 中 method 通过 selector 和 IMP 能够对相应的方法快速查找与实现。

类缓存 (objc_cache)

通过梳理前面已经学到的我们可以知道对于方法的查找是通过 isa 指针去查找,但是如果每次找一个方法都需要 method_list 中整个的遍历一遍的话那就太慢了,并且很多了时候我们会经常使用一个类中的较少的方法很多次,因此类实现了一个缓存,当你寻找一个选择器并且寻找到了就将它加入到缓存当中,并且每次在寻找方法的时候优先去缓存当中找,没有了再去 method_list 中寻找。这是基于这样的理论:如果你在类上调用一个消息,你可能以后再次调用该消息。

Category (objc_category)

和之前一样先看一下对 Category 定义的代码:

//https://opensource.apple.com/source/objc4/objc4-371.1/runtime/runtime.h.auto.html
//declaration
typedef struct objc_category *Category;
//details
struct objc_category {
    char *category_name                                      OBJC2_UNAVAILABLE;
    char *class_name                                         OBJC2_UNAVAILABLE;
    struct objc_method_list *instance_methods                OBJC2_UNAVAILABLE;
    struct objc_method_list *class_methods                   OBJC2_UNAVAILABLE;
    struct objc_protocol_list *protocols                     OBJC2_UNAVAILABLE;
}

Category 是一个指向 struct objc_category 类型的指针,这个结构体的定义当中有类别的名字还有类的名字,实例方法列表,类方法列表,协议列表,我们都知道类别 (Category) 是对类的拓展,由上面的结构可以看出类别中可以添加的有类方法,实例方法以及协议。

Runtime 消息转发

Runtime 中消息转发分为 3 种方式

  • 动态方法解析
  • 备用接收者
  • 完整消息转发

下面这张图可以让你对消息转发的理解更加直观一点

当通过 isa 寻找对象的方法的时候经历了消息传递没有找到并且这三种方法都失败了之后就会执行 doesNotRecognizeSelector: 方法报 unrecognized selector 的错。

动态方法解析

Objective-C 运行时 (也就是 runtime 时期将 OC 的代码转换成 C 的这个时期) 会调用 +resolveInstanceMethod: 或者 +resolveClassMethod:,让你有机会提供一个函数实现来对应对象的调用。如果添加了函数并返回 YES,运行时系统就会重新启动一次消息传递的过程 示例如下:

从代码当中可以看到并没有实现 foo: 函数但是通过 class_addMethod 方法动态添加 fooMethod 函数,并执行它的 IMP, 并且返回 YES,如果返回的是 NO 就执行 forwardingTargetForSelector 方法。

备用接收者

通过观察之前的那张整体的图片可以发现如果目标对象这个时候实现了 (紧接上面的叙述) -forwardingTargetForSelector:Runtime 此时就会调用它,给一个将这个消息转发给其他对象接受的机会,其实可以理解成是这个对象经历了前面的过程发现没有找到这个方法,那既然如此就干脆换一个有对应这个方法的对象去接受吧 (前提是这个对象的方法和你要的效果是一样的) 示例如下:

完整消息转发

如果经历了上面的两个步骤这个未知的消息仍然无法得到处理的话,那么接下来就只能弃用完整的消息转发机制。首先它会发送 -methodSignatureForSelector: 消息获得函数的参数和返回值类型。如果 -methodSignatureForSelector: 返回 nilRuntime 则会调用 -doesNotRecognizeSelector: 消息,也可以理解是凉了,但是如果返回的时候函数签名,Runtime 就会创建一个 NSInvocation 对象该对象调用的就是需要转发的消息并发送 -forvardInvocation: 消息给目标对象。 一个完整的消息转发示例如下:

通过签名,Runtime 生成了一个对象 Invocation,发送给了-forwardInvocation:,我们在 forwardInvocation 方法里面让Test 对象去执行了 foo 函数。

Runtime 应用

因为现在写的东西不是很多,因此对 Runtime 的应用没什么认识,但是随着后面学习的深度和广度的打开我再过来把这个坑给填上。

参考

  • 掘金 jackyshan_ 很感谢这位大佬比较系统的讲解,本文是在理解了这篇文章的基础上对这篇文章进行了相关的补充。
  • objc_msgSend 方法定义如下:
OBJC_EXPORT id objc_msgSend(id self, SEL op,...)
  • Class 定义如下:
typedef struct objc_class *Class; 
  • 签名参数 v@: ,可以参考苹果文档 Type Encodings

  • Invocation n.调用

代码演示环境