前言
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\Function- cache_t的- 0号位置是否为- 0,不为- 0跳转到- LLookupPreopt\Function去加载共享缓存,暂时不对- LLookupPreopt\Function进行探索
 
- and p10, p11, #0x0000ffffffffffff- p10=- _bucketsAndMaybeMask & 0x0000ffffffffffff=- buckets
- 0x0000ffffffffffff 
- 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 3
- p13 = buckets + (index << 4),- p13就是- index下的- bucket,是我们第一个要查的- bucket- index << 4相当于- index * 16
 
 
- 1: ldp p17, p9, [x13], #-BUCKET_SIZE- p17 = 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 1b- while (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 2b- if (sel == _cmd),跳到- 2b CacheHit
 
- cmp p9, #0- while (sel != 0 &&
 
- ccmp p13, p12, #0, ne- bucket > 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)
