了解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 | // 检查对象在继承层级结构的位置; |
运行时函数
运行时系统是一个共享的动态库,公共的接口在目录下/usr/include/objc
。其中包含了一些函数和数据结构,许多函数可以使用纯C来复制编译器在编写 Objective-C 代码时所做的事情。这其实也就是我们常常看到的,使用某些运行时函数可以达到可以NSObject方法一样的效果,其实也正是这些底层的函数构成了NSObject的基础功能。这里是官方文档Objective-C 运行时参考。
现在,我们知道了运行时交互有哪些,那么接下来,我们再看看Objective-C
中的一些基本概念:类、对象、Method、SEL、IMP。熟悉这些概念之后,会更加理解运行时做了哪些事。
类、对象、Method、SEL、IMP
类
类对象(Class
)是由程序设置后在运行时由编译器创建的,当一个对象的实例方法被调用时,会通过isa
找到这个类,然后在该类中方法列表中查找。
1 | // Class 定义 |
1 | // 类结构体 |
结构体里有指向父类的指针、类名、版本、实例大小、实例变量列表、方法列表、缓存、遵守的协议列表等。
那疑问来了,请问类方法是存在哪里的?我们在调用类方法的时候,我们如何去找呢?这里就引入一个概念:元类(meta-class)。(想要深入了解元类可以查看这篇文章 What is a meta-class in Objective-C?)
元类就是类对象的isa指向的类,也可以说是类对象的类
对象
实例对象(Object
)是我们对类对象alloc或者new操作时所创建的,在这个过程中会拷贝实例所属的类的成员变量,但并不拷贝类定义的方法。调用实例方法时,系统会根据实例的isa指针去类的方法列表及父类的方法列表中寻找与消息对应的selector指向的方法
1 | // 对象结构体 |
由此,我们得出了一个结论,类对象和实例对象的查找机制是一样的:
- 对象的实例方法调用时,通过对象的 isa 在类中获取方法的实现。
- 类对象的类方法调用时,通过类的 isa 在元类中获取方法的实现。
对应关系如下图,描述了对象,类,元类之间的关系:
图中实线是 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
通过selector
和IMP
两个属性,实现了快速查询方法及实现,相对提高了性能,又保持了灵活性。
1 | typedef struct objc_method *Method; |
SEL
SEL是方法选择器, 常见的写法有:@selector()
1 | /// 代表一个方法的不透明类型 |
1 | 声明方式: |
IMP
IMP
是指向最终实现程序的内存地址的指针,下面是它的定义:
1 | /// 指向一个方法实现的指针 |
理解了前面的这些概念之后,接下来我们进入正题。
在 Objective-C 中,消息直到运行时才绑定到方法实现。编译器转换消息表达式,调用objc_msgSend方法。
消息发送
Objective-C
中所有方法的调用/类的生成都在运行时进行,我们可以通过类名/方法名反射得到相应的类和方法,也可以替换某个类的方法为新的实现,理论上你可以在运行时通过类名/方法名调用到任何 Objective-C
方法,替换任何类的实现以及新增任意类。
比方说我们写一个调用方法[receiver message]
,那这个方法会被编译器转化成:
1 | // 第一个参数类型是发送者, 第二个参数类型是SEL。SEL在OC中是selector方法选择器 |
不同类中相同名字的方法所对应的方法选择器是相同的,即使方法名字相同而变量类型不同也会导致它们具有相同的方法选择器。由于这点特性,也导致了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的过程
消息传递框架:
为了加快消息传递过程,运行时系统会在使用方法时缓存方法的选择器和地址。每个类都有一个单独的缓存,它可以包含继承方法以及类中定义的方法的选择器。在消息传递过程中,会首先检查接收对象类的缓存。
消息转发
转发阶段,会调用_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
方法,抛出异常。