上一节已了解类的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调用
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"));
- 打印查看:
崩溃
暂时不理会,我们现在发现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
中的类
。
当前
objc-msg-arm64.s
汇编文件中搜索CacheLookup
,找到.macro CacheLookup
定义处:
- 如果
匹配sel成功
,调用Cachehit
命中缓存流程, 返回找到的imp
-
如果
匹配失败
,触发CheckMiss
和JumpMiss
流程, 告知外部Cache
中未找到imp
未找到
imp
时,进入__objc_msgSend_uncached
流程,搜索__objc_msgSend_uncached
:
- 发现
缓存找不到
后,进入方法列表
去查找。 搜索MethodTableLookup
:
- 跳转
_lookUpImpOrForward
,进入慢速查找
阶段。
奉上完整流程