上一节已了解类的cache结构和插入操作。但是有几个问题:
- 1. 何时
插入缓存? - 2. 缓存
读取机制是怎样?

现在开始探索之旅
1. 探索插入操作
2. 介绍Runtime
3. 了解方法的本质
4. objc_msgSend解析
1. 探索插入操作
我们从insert开始寻找谁在调用它
- 在objc4源码下搜索
->insert((c++的调用方式->)

- 发现
cache_fill调用了它,我们继续搜索cache_fill:

我们发现,在缓存写入之前,我们需要先知道:
-
给谁进行写入(objc_msgSend) -
写入内容是什么(cache_getImp)
到这里,我们必须引出OC非常重要的机制:Runtime运行时
2. 介绍Runtime
👉 Objective-C Runtime Programming Guide,此文档
不再更新,适合初步了解Runtime
2.1 什么是Runtime
- Runtime是一个由
C、C++、汇编混合开发的API库,它将程序的一些决定性工作从编译器推迟到运行期,使得OC语言具备动态特性。内部使用消息机制进行通信。
2.2 什么是运行时? 什么是编译时?
-
编译时:编译器将源代码翻译成机器能识别的代码。这是一个静态操作,并不会把代码写入内存中进行运行。
- 编译过程中,会
分析语法是否正确。- 编译时提示的
error、warning都是编译时错误- 编译过程检查就叫
编译时类型检查或静态类型检查
-
运行时: 将代码装载入内存中,让代码运行起来
- 代码在
装载入内存之前,只是个"死家伙",静静地趴在磁盘中。只有载入内存,才是"活的"。运行时类型检查与前面所说的编译时类型检查(或叫静态类型检查)不一样,它不是简单的扫描代码,而是在内存中做些操作,做些判断。(是动态活动的)
例如一个函数,只声明,未实现。 command+B编译时不会报错,但是command+R运行时会报错。

2.3 Runtime版本
-
Runtime有两个版本,Legacy(早期版本) 和Modern(现行版本)
早期版本:Objective-C 1.0,用于32位的Mac OS X的平台上,实例变量发生改变后,需要重新编译其子类。现行版本:Objective-C 2.0,用于iPhone程序和Mac OS X v10.5及以后的系统中的64 位程序,实例变量发生改变后,不需要重新编译其子类。
2.4 运行时让OC具备多态特性
OC的运行时机制:将
数据类型的确定由编译时,推迟到运行时。OC的这种运行时机制使对象的类型及对象的属性和方法在运行时才能确定。多态:不同对象以自己的方式响应相同的消息的能力叫做多态
例如:
自然界中的人类(Person)都有一个相同的方法-sing,男人(Man)类属于人类,女人(Wonan)类也属于人类,都继承了人类后,会实现各自的-sing方法。但是自然界中男人和女人的sing的风格又不一样,男人唱的豪迈,女人唱的委婉,但都继承了person唱的能力,这就是多态的现象。
也就是不同的对象以自己的方式响应相同消息的能力叫多态。 也可以说运行时机制是多态的基础。
描述参考👉: 码上江湖
2.5 Runtime的三种调用
-
oc代码调用、framework调用 、RuntimeAPI调用
image.png
2.6 探索runtime
先准备好静态资源的,咱们才能动态起来。所以先获取compiler层文件。
- 测试代码:
@interface HTPerson : NSObject
- (void)sayHello;
@end
@implementation HTPerson
- (void)sayHello{
NSLog(@"%s",__func__);
}
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
HTPerson *p = [HTPerson alloc];
[p sayHello];
}
return 0;
}
-
clang静态编译main.m文件:clang -rewrite-objc main.m -o main.cpp

我们发现,所有OC方法,不管是类方法alloc,还是实例方法sayHello都是调用了objc_msgSend发送消息。
调用格式:
objc_msgSend: (消息接收者, 消息主体)尝试手动使用
objc_msgSend执行方法:
- 导入头文件
#import <objc/message.h>- 手动
关闭运行时的编译警告:buildSetiing->Enable Strict Checking of objc_msgSend Calls->设置为No- 加入测试代码
objc_msgSend(p, sel_registerName("sayHello"));- 打印查看:
image.png
崩溃暂时不理会,我们现在发现sayHello打印成功了
3. 了解方法的本质
我们知道,方法的本质就是一个方法名和对应的函数代码。
OC中,我们使用执行对象+函数名进行函数调用 (例如:[person sayHello])。内部完整的调用流程是:
objc_msgSend发送消息(class sel) -> 通过sel(方法编号)找到imp(函数指针地址) -> 找到函数内容
- 第一步: 发送消息我们有三种API调用方法(
oc代码调用、framework调用 、RuntimeAPI调用)- 第三步:可直接 从
函数指针地址读取函数内容。
上述流程,我们唯一不知道的就是系统如何通过sel(方法编号)找到imp(函数指针地址)?
4. objc_msgSend解析
了解sel如何找到imp就是探究 objc_msgSend内部机制。
函数的调用是极其频繁的,所以对
性能的要求非常高。objc_msgSend使用汇编进行编写。imp的查找分为2个阶段,快速查找(缓存cache中,汇编编写)和慢速查找(方法列表methodTable中,c和c++编写),今天先介绍快速查找
接下来的知识,需要大家先熟悉cache_t的结构
汇编查找函数的流程: 从指定类开始->定位cache->定位buckets->哈希运算获取首次位置->循环寻找位置->返回imp或null
打开
objc4源码,搜索objc_msgsend,我们选择arm64真机环境进行探索(其他环境也是类似逻辑)。找到
objc_msgSend入口:

-
初始化数据, 从receiver中读取isa中的类。
image.png 当前
objc-msg-arm64.s汇编文件中搜索CacheLookup,找到.macro CacheLookup定义处:

- 如果
匹配sel成功,调用Cachehit命中缓存流程, 返回找到的imp

-
如果
匹配失败,触发CheckMiss和JumpMiss流程, 告知外部Cache中未找到imp
image.png 未找到
imp时,进入__objc_msgSend_uncached流程,搜索__objc_msgSend_uncached:

- 发现
缓存找不到后,进入方法列表去查找。 搜索MethodTableLookup:

- 跳转
_lookUpImpOrForward,进入慢速查找阶段。
奉上完整流程





