前言
OC 类原理探索:cache 结构分析补充 中我们对cache结构进行了补充,也引入到了objc_msgSend,今天主要任务是探索objc_msgSend的汇编源码。
准备工作
一、runtime 的运行时理解
Runtime 简介
Runtime是一个用C、C++、汇编编写的运行时库,包含了很多C语言的API,封装了很多动态性相关的函数。Objective-C是一门动态运行时语言,允许很多操作推迟到程序运行时再进行。OC的动态性就是由Runtime来支撑和实现的,Rumtime就是它的核心。- 我们平时编写的
OC代码,底层都是转换成了Runtime API进行调用。
编译时
编译时顾名思义就是正在编译的时候。
那啥叫编译呢?就是编译器帮你把
源代码翻译成机器能识别的代码。
(当然只是一般意义上这么说,实际上可能只是翻译成某个中间状态的语言。)
那编译时就是简单的作一些翻译工作,比如检查老兄你有没有粗心写错啥关键字了啊。有啥词法分析,语法分析之类的过程。就像个老师检查学生的作文中有没
有错别字和病句一样,如果发现啥错误编译器就告诉你。
如果你用微软的VS的话,
点下build。那就开始编译,如果下面有errors或者warning信息,那都是编译器检查
出来的。所谓这时的错误就叫编译时错误,这个过程中做的啥类型检查也就叫编译
时类型检查,或静态类型检查(所谓静态嘛就是没把真把代码放内存中运行起来,而只是把代码当作文本来扫描下)。
所以有时一些人说编译时还分配内存啥的肯定是错误的说法。
运行时
运行时就是代码跑起来了,被装载到内存中去了。(你的代码保存在磁盘上没装入内存之前是个死家伙,只有跑到内存中才变成活的)。
而运行时类型检查就与前面讲的编译时类型检查(或者静态类型检查)不一样,不是简单的扫描代码,而是在内存中做些操作,做些判断。
Runtime 版本
Runtime有两个版本,一个Legacy版本(早期版本) ,一个Modern版本(现行版本)。
- 早期版本对应的编程接口:
Objective-C 1.0; - 现行版本对应的编程接口:
Objective-C 2.0; - 早期版本用于
Objective-C 1.0,32位的Mac OS X的平台上; - 现行版本:
iPhone程序和Mac OS X v10.5及以后的系统中的64位程序。
可以在官方文档 Objective-C Runtime Programming Guide 中找到相关定义。

Runtime 的发起方式
Runtime的层级结构:

Runtime的三种发起方式OC 方法、NSObject 接口、objc api:

方法调用的本质 objc_msgSend
创建SSPerson类,say1:方法有实现,say2方法没有实现:

实例化SSPerson,调用say1:方法、say2方法,command + B进行编译,编译成功:

command + R运行项目,项目报错:

这就是编译时和运行时的区别,接下来clang -rewrite-objc main.m -o main.cpp编译main.m得到main.cpp文件。
在文件中查看main函数:
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
SSLPerson *person = objc_msgSend((id)objc_getClass("SSLPerson"), sel_registerName("alloc"));
objc_msgSend((id)person, sel_registerName("say1:"), (NSString *)&__NSConstantStringImpl__var_folders_tb_rpsdqw797gn7n3l8myk82s5w0000gn_T_main_d1fdf2_mi_1);
((id)person, sel_registerName("say2"));
}
return 0;
}
复制代码
- 这里的方法调用,采用的是
Runtime API调用方式; - 可以看到,不管是
类方法还是对象方法都是用到了objc_msgSend,方法调用的本质是消息的发送; - 调用方法 =
objc_msgSend(消息的接受者,消息的主体(sel + 参数));
我们添加一个say3方法,然后用objc_msgSend进行调用:

可以正常调用,注意下面的设置要改为NO,正常是YES的。

objc_msgSendSuper
找到objc_msgSendSuper的定义:

我们看到有函数中有objc_super参数,我们去源码中看下它的定义:

objc_super中有receiver和super_class两个成员变量;super_class是第一查找对象,如果没有方法实现的话会继续向上寻找,直到NSObject类。
我们来重新定义下类,SSLPerson继承SSLAnimal,SSLAnimal去实现say2方法:

把super_class赋值为SSLAnimal.class,执行代码可以正常打印:

接下来把super_class赋值为SSLPerson.class,执行代码:

同样可以正常打印,这也证明了我们上面的说法,方法寻找是会向上寻找的。
二、objc_msgSend 流程
objc_msgSend 源码查找
打断点到方法调用处,用汇编跟源码的方式找到objc_msgSend所在源码库objc:

源码中搜索objc_msgSend,调用的地方会非常非常的多,很难查看:

按住command + 点击下拉箭头,收缩文件:

objc_msgSend的源码在汇编中,汇编文件是以.s结尾的;- 我们来看
objc-msg-arm64.s文件,因为arm64是真机架构,i386是模拟器架构,x86_64是Mac OS架构。
我们接下来通过汇编来探索objc_msgSend的流程,ENTRY是汇编程序的入口点,我们找到它:

汇编源码 解析
1. 汇编源码 消息接受者判空
汇编源码:

- 源码解析
- 判断消息接受者
person是否为空,如果为空; - 判断是否为
tagged pointer,如果是的话进行相关处理; - 如果不是
tagged pointer,置空,结束方法调用;

- 如果消息接受者
person不为空,向下继续执行。
- 判断消息接受者
2. 汇编源码 获取isa

- 源码解析
- 将
person的isa赋值给p13; p13以参数形式,传进GetClassFromIsa_p16;- 通过
ExtractISA,isa & ISA_MASK赋值给p16,也就得到了class; - 得到
class,是为了获取其成员变量cache,进行方法的查找; GetIsaDone:获取isa结束。
- 将
3. 汇编源码 CacheLookup

CacheLookup宏定义函数,Mode = NORMAL,Function = _objc_msgSend,MissLabelDynamic = __objc_msgSend_uncached;
4. 汇编源码 获取hash index
汇编源码:

mov x15, x16- 保存原始
isa值,p16 = class,x15 = x16 = isa。
- 保存原始
CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16真机架构,也是我们此流程分析的架构。
ldr p11, [x16, #CACHE]#define CACHE (2 * __SIZEOF_POINTER__)CACHE=2 * __SIZEOF_POINTER__=16
p11=isa 平移 16=cache_t
CONFIG_USE_PREOPT_CACHES- 真机环境值为
1
- 真机环境值为
__has_feature(ptrauth_calls)- 判断是否为
A12以后机型
- 判断是否为
tbnz p11, #0, LLookupPreopt\Functioncache_t的0号位置是否为0,不为0跳转到LLookupPreopt\Function去加载共享缓存,暂时不对LLookupPreopt\Function进行探索
and p10, p11, #0x0000ffffffffffffp10=_bucketsAndMaybeMask & 0x0000ffffffffffff=buckets0x0000ffffffffffff
0000000000000000111111111111111111111111111111111111111111111111
eor p12, p1, p1, LSR #7,and p12, p12, p11, LSR #48- 根据
hash 函数获取index,p12 = (_cmd ^ (_cmd >> 7)) & mask 
- 根据
5. 汇编源码 向前遍历查找
汇编源码:

add p13, p10, p12, LSL #(1+PTRSHIFT)PTRSHIFT的定义:#define PTRSHIFT 3p13 = buckets + (index << 4),p13就是index下的bucket,是我们第一个要查的bucketindex << 4相当于index * 16
1: ldp p17, p9, [x13], #-BUCKET_SIZEp17 = imp,p9 = sel,然后*bucket--
cmp p9, p1- 比较
sel和_cmd是否不等
- 比较
b.ne 3f- 如果不相等,向前查找下一个
bucket,跳到3f
- 如果不相等,向前查找下一个
2: CacheHit- 如果相等,进入
CacheHit缓存命中,返回
- 如果相等,进入
3: cbz p9, \MissLabelDynamic- 判断
sel是否为0,如果为0进入__objc_msgSend_uncached函数
- 判断
cmp p13, p10,b.hs 1bwhile (bucket >= buckets),如果bucket >= buckets,跳到上面1b,否则进行下面的代码
6. 汇编源码 最后位置 向前遍历查找
汇编源码:

add p13, p10, p11, LSR #(48 - (1+PTRSHIFT))p13 = buckets + (mask << 1+PTRSHIFT),平移到buckets的最后一个位置
add p12, p10, p12, LSL #(1+PTRSHIFT)4: ldp p17, p9, [x13], #-BUCKET_SIZE{imp, sel} = *bucket--,向前推移去取值
cmp p9, p1,b.eq 2bif (sel == _cmd),跳到2b CacheHit
cmp p9, #0while (sel != 0 &&
ccmp p13, p12, #0, nebucket > first_probed)
b.hi 4b,回到4b继续执行
三、真机跑汇编
新创建一个项目,接下来用汇编跟源码的方式进行探索,汇编跟源码有不懂的可以去 OC 对象原理探索(一)中查看。

如下是真机调试的汇编,可以发现跟汇编源码非常相似。

我们进行一些简单的调试,和对汇编源码的一些验证。

- 根据打印可以看到,
x1确实是sel。

- 读取
x0得到<SSLPerson: 0x280d78060>,确实是消息接受者person。 - 读取
x16地址为0x00000001044415e0,与SLPerson.class相同,可以证明x16是class,x16 = x13 & 0x7ffffffffffff8也就是isa & isaMask。

- 读取
x12得到0x0000000000000001也就是1,通过cache_hash函数得到的哈希 index。
























![[桜井宁宁]COS和泉纱雾超可爱写真福利集-一一网](https://www.proyy.com/skycj/data/images/2020-12-13/4d3cf227a85d7e79f5d6b4efb6bde3e8.jpg)

![[桜井宁宁] 爆乳奶牛少女cos写真-一一网](https://www.proyy.com/skycj/data/images/2020-12-13/d40483e126fcf567894e89c65eaca655.jpg)