新生代农民工的主页

深入理解 iOS Runtime

字数统计: 2.3k阅读时长: 8 min
2022/02/19

了解Runtime有助于我们理解Objective-C运行时系统的工作原理以及如何利用它。本章将介绍NSObject类以及Objective-C程序如何与运行时系统进行交互,如何在运行时查找对象的信息,如何将消息转发给其他对象。

Runtime简介

Runtime 又称运行时,是iOS系统的核心,它是一套底层的C语言API。它会将一些工作放在代码运行时才处理而并非编译时,所以有很多类和成员变量在我们编译时是不知道的,而在运行时,我们所编写的代码会转换成完整的确定的代码运行。

运行时交互

Objective-C中与运行时系统有三个层次的交互:

  • 通过Objective-C源码
  • 通过Foundation中的NSObject定义的方法
  • 通过直接调用运行时函数

Objective-C 源代码

顾名思义,只要编写和编译 Objective-C代码即可使用它。编译包含Objective-C 类和方法的代码时,编译器会创建实现语言动态特性的数据结构和函数。

NSObject 方法

Cocoa 中,大多数对象都是NSObject的子类,因此都继承了他定义的方法。(NSProxy是个特例)
NSObject中有一些方法可以简单地向运行时系统查询信息。支持对象执行自省。例如class方法,有isKindOfClass:isMemberOfClass:, 检查对象在继承层级结构的位置是否正确;
conformsToProtocol:,对象是否要实现特定协议中定义的方法;respondsToSelector:,表示对象是否可以接受特定消息;

1
2
3
4
5
6
7
8
9
10
11
12
// 检查对象在继承层级结构的位置;
- (BOOL)isKindOfClass:(Class)aClass;
- (BOOL)isMemberOfClass:(Class)aClass;

// 指示对象是否声称要实现特定协议中定义的方法
- (BOOL)conformsToProtocol:(Protocol *)aProtocol;

// 表示对象是否可以接受特定消息 (检查是否实现某个方法)
- (BOOL)respondsToSelector:(SEL)aSelector;

// 方法实现的地址
- (IMP)methodForSelector:(SEL)aSelector;

运行时函数

运行时系统是一个共享的动态库,公共的接口在目录下/usr/include/objc。其中包含了一些函数和数据结构,许多函数可以使用纯C来复制编译器在编写 Objective-C 代码时所做的事情。这其实也就是我们常常看到的,使用某些运行时函数可以达到可以NSObject方法一样的效果,其实也正是这些底层的函数构成了NSObject的基础功能。这里是官方文档Objective-C 运行时参考

现在,我们知道了运行时交互有哪些,那么接下来,我们再看看Objective-C中的一些基本概念:类、对象、Method、SEL、IMP。熟悉这些概念之后,会更加理解运行时做了哪些事。

类、对象、Method、SEL、IMP

类对象(Class)是由程序设置后在运行时由编译器创建的,当一个对象的实例方法被调用时,会通过isa找到这个类,然后在该类中方法列表中查找。

1
2
// Class 定义
typedef struct objc_class *Class;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 类结构体
struct objc_class {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;

#if !__OBJC2__
Class _Nullable super_class OBJC2_UNAVAILABLE;
const char * _Nonnull name OBJC2_UNAVAILABLE;
long version OBJC2_UNAVAILABLE;
long info OBJC2_UNAVAILABLE;
long instance_size OBJC2_UNAVAILABLE;
struct objc_ivar_list * _Nullable ivars OBJC2_UNAVAILABLE;
struct objc_method_list * _Nullable * _Nullable methodLists OBJC2_UNAVAILABLE;
struct objc_cache * _Nonnull cache OBJC2_UNAVAILABLE;
struct objc_protocol_list * _Nullable protocols OBJC2_UNAVAILABLE;
#endif

} OBJC2_UNAVAILABLE;

结构体里有指向父类的指针、类名、版本、实例大小、实例变量列表、方法列表、缓存、遵守的协议列表等。

那疑问来了,请问类方法是存在哪里的?我们在调用类方法的时候,我们如何去找呢?这里就引入一个概念:元类(meta-class)。(想要深入了解元类可以查看这篇文章 What is a meta-class in Objective-C?)

元类就是类对象的isa指向的类,也可以说是类对象的类

对象

实例对象(Object)是我们对类对象alloc或者new操作时所创建的,在这个过程中会拷贝实例所属的类的成员变量,但并不拷贝类定义的方法。调用实例方法时,系统会根据实例的isa指针去类的方法列表及父类的方法列表中寻找与消息对应的selector指向的方法

1
2
3
4
// 对象结构体
struct objc_object {
Class _Nonnull isa; OBJC_ISA_AVAILABILITY;
};

由此,我们得出了一个结论,类对象和实例对象的查找机制是一样的:

  • 对象的实例方法调用时,通过对象的 isa 在类中获取方法的实现。
  • 类对象的类方法调用时,通过类的 isa 在元类中获取方法的实现。

对应关系如下图,描述了对象,类,元类之间的关系:

23_7.png

图中实线是 super_class指针,虚线是isa指针。

  • Root class (class)就是NSObject,NSObject没有超类,所以Root class(class)的superclass指向nil。
  • 每个Class都有一个isa指向唯一的Meta class
  • Root class(meta)的superclass指向Root class(class),也就是NSObject,形成一个回路。
  • 每个Meta class的isa都指向Root class (meta)。

Method

Method就是我们平时所说的函数,它表示的是能够独立完成一个功能的一段代码;Method通过selectorIMP两个属性,实现了快速查询方法及实现,相对提高了性能,又保持了灵活性。

1
2
3
4
5
typedef struct objc_method *Method;
struct objc_method {
SEL method_name OBJC2_UNAVAILABLE;
char *method_types OBJC2_UNAVAILABLE;
IMP method_imp

SEL

SEL是方法选择器, 常见的写法有:@selector()

1
2
/// 代表一个方法的不透明类型
typedef struct objc_selector *SEL;
1
2
3
声明方式:
SEL s1 = @selector(test1);
SEL s2 = NSSelectorFromString(@"test2");

IMP

IMP是指向最终实现程序的内存地址的指针,下面是它的定义:

1
2
3
/// 指向一个方法实现的指针
typedef id (*IMP)(id, SEL, ...);
#endif

理解了前面的这些概念之后,接下来我们进入正题。

在 Objective-C 中,消息直到运行时才绑定到方法实现。编译器转换消息表达式,调用objc_msgSend方法。

消息发送

Objective-C 中所有方法的调用/类的生成都在运行时进行,我们可以通过类名/方法名反射得到相应的类和方法,也可以替换某个类的方法为新的实现,理论上你可以在运行时通过类名/方法名调用到任何 Objective-C 方法,替换任何类的实现以及新增任意类。

比方说我们写一个调用方法[receiver message],那这个方法会被编译器转化成:

1
2
// 第一个参数类型是发送者, 第二个参数类型是SEL。SEL在OC中是selector方法选择器
id objc_msgSend ( id _Nullable self, SEL op, ... );

不同类中相同名字的方法所对应的方法选择器是相同的,即使方法名字相同而变量类型不同也会导致它们具有相同的方法选择器。由于这点特性,也导致了Objective-C不支持函数重载。

实际上,我们在调用的方法的过程,其实在Runtime中就是消息发送。

objc_msgSend的实现是由汇编语言实现,根据CPU架构实现的过程各不相同,如果想阅读相关的代码要有一定的汇编基础;

objc_msgSend会做以下几件事情:

  • 1.检测这个 selector是不是要忽略

  • 2.检查target是不是为nil

    • 如果这里有相应的nil的处理函数,就跳转到相应的函数中
    • 如果没有处理nil的函数,就自动清理并返回。这一点就是为何在Objective-C中给nil发送消息不会崩溃的原因
  • 3.确定不是给nil发消息之后,在该class的缓存中查找方法对应的IMP实现

    • 如果找到,就跳转进去执行
    • 如果没有找到,就在方法列表里面继续查找,一直找到NSObject为止
  • 4.如果还没有找到,那就需要开始消息转发阶段了。至此,发送消息Messaging阶段完成。这一阶段主要完成的是通过select()快速查找IMP的过程

消息传递框架:

messaging1.gif

为了加快消息传递过程,运行时系统会在使用方法时缓存方法的选择器和地址。每个类都有一个单独的缓存,它可以包含继承方法以及类中定义的方法的选择器。在消息传递过程中,会首先检查接收对象类的缓存。

消息转发

转发阶段,会调用_objc_msgForward(id self, SEL _cmd,...)方法

1
_objc_msgForward(id _Nonnull receiver, SEL _Nonnull sel, ...) 

_objc_msgForward会做以下几件事情:

  • 1.先调用 forwardingTargetForSelector 方法获取新的 target 作为 receiver 重新执行 selector,
    • 如果返回的内容合法, 跳转去执行
    • 如果返回的内容不合法(为 nil 或者跟旧 receiver 一样),继续执行后续方法。
  • 2.调用 methodSignatureForSelector 获取方法签名后,判断返回类型信息是否正确,再调用 forwardInvocation 执行 NSInvocation 对象,并将结果返回。如果对象没实现 methodSignatureForSelector 方法,继续执行后面方法。
  • 3.调用 doesNotRecognizeSelector 方法,抛出异常。

image.png

CATALOG
  1. 1. Runtime简介
    1. 1.1. 运行时交互
      1. 1.1.1. Objective-C 源代码
      2. 1.1.2. NSObject 方法
      3. 1.1.3. 运行时函数
    2. 1.2. 类、对象、Method、SEL、IMP
      1. 1.2.1.
      2. 1.2.2. 对象
      3. 1.2.3. Method
      4. 1.2.4. SEL
      5. 1.2.5. IMP
    3. 1.3. 消息发送
    4. 1.4. 消息转发