开篇
好好学习,不急不躁。
上篇文章cache分析,讲述了类的cache,cache_t内部结构分析,cache数据插入流程;但在什么时候开始插入数据,什么场景触发数据插入,都是未知的。所以今天就讲讲,是如何触发数据插入;
cache 触发数据写入
在 insert 方法处,打断点,通过断点流程,可以看到,触发 cache::insert 方法,是由log_and_fill_cache()
这个方法来触发的;
log_and_fill_cache(Class cls, **IMP** imp, **SEL** sel, **id** receiver, Class implementer)
{
#if SUPPORT_MESSAGE_LOGGING
if (slowpath(objcMsgLogEnabled && implementer)) {
bool cacheIt = logMessageSend(implementer->isMetaClass(),
cls->nameForLogging(),
implementer->nameForLogging(),
sel);
if (!cacheIt) return;
}
#endif
cls->cache.insert(sel, imp, receiver);
}
在log_and_fill_cache
内部,调用了cache.insert()方法;顺着方法的执行顺序,最终看到,是由objc_msgSend发出的。现在就带领大家,进入新领域,objc_msgSend;
Runtime
Runtime
在讲述 objc_msgSend前,需要了解什么是Runtime;因为objc_msgSend是在Runtime中执行的;这里稍微简单讲解一下Runtime,后续再出文章,针对Runtime具体讲解;
都知道Objective-C是一门动态的语言。OC将源码转换为可执行的文件,需要经过三个步骤:编译、链接、运行;
编译阶段
:是无法知道函数调用,或者是变量的具体类型,编译阶段,只负责将代码转换成机器能识别的语言,不负责内存的分配;
运行阶段
:把需要用到的方法,变量等,从磁盘装载到内存中; 只有进入内存,程序才能跑起来; 底层是通过dyld链接
的形式,将代码装载到内存中;而Runtime
就是运行时的机制的基础;
Runtime 调用方式
1、通过Objective-C方法,调用Runtime相关方法;
2、通过NSObject,调用Runtime相关方法;
3、使用底层objc提供的API来发起;
Runtime API 调用原理
在之前的文章中说过,可以通过.cpp文件,查看OC代码的源码执行逻辑,所以今天我们也使用.cpp文件,来查看Runtime 底层方法是如何调用的;
在项目中,执行-(void)sayNB 方法,查看sayNB方法,如何调用的;并生成.cpp文件的方式;
在.cpp文件中,查看 main()方法,可以看到,在编译底层,OC上层的代码都被编译为C++执行,并且是使用 Runtime的方式执行;
OC的方法,都转换为objc_msgSend
函数调用,传递一个消息接收者,和消息体,objc_msgSend(消息接收者,消息体)
,消息体包含:sel,参数
;
所以,执行-(void)sayNB方法,与直接调用objc_msgSend(person,sel_registerName("sayNB"))一样;
由此可以看出,Runtime具备运行时功能,而OC方法的本质,其实就是消息的发送,通过objc_msgSend进行发送
objc_msgSend
objc_msgSend 汇编
在源码中全局查找,发现objc_msgSend在不同的系统架构中,源码不太一样,我们采用真机架构来分析objc_msgSend流程;
查看真机arm64的框架中,objc_msgSend的源码;
objc_msgSend源码非常简单,并且是采用汇编语言
编写的;是不是看不懂,🤣🤣🤣而且很奇怪,为什么在这里,苹果就使用汇编语言
,而不是C/C++;并且p0,x0
等这些是什么含义呢?
看源码,得知objc_msgSend是汇编语言,在运行中,汇编存在寄存器结构,模拟器的寄存器结构标记为:rax、rbx、rcx、rdx等
,里面保存着self,和sel等数据,如下图:
在 arm64上,寄存器结构为x0~x30等;self 是保存在x0中,sel 在x1里,如下图:
objc_msgSend 源码分析
cmp p0,#0
:
cmp,对比指令;对比p0,#0;
p0的地址,消息接收者是person,意味着p0,代表的就是person的地址;
#0,对比对象,值为0
cmp p0, #0 //对比当前消息接收者,是否存在,是否有消息接收者;不存在,则执行这个if else 逻辑
#if SUPPORT_TAGGED_POINTERS //接着判断当前是否为 tagged pointer (小对象)类型
b.le LNilOrTagged // (MSB tagged pointer looks negative)
#else
b.eq LReturnZero //person不是个小对象类型,则执行这里的代码;b.eq 相等,则返回默认值->空消息;因为没有消息接收者,所以返回空消息
#endif //消息接收者存在的话,执行下面的逻辑
ldr p13, [x0] // p13 = isa [x0] 为对比对象
GetClassFromIsa_p16 p13, 1, x0 // p16 = class
LGetIsaDone: // 取值结束后,执行下面的逻辑
// calls imp or objc_msgSend_uncached
CacheLookup NORMAL, _objc_msgSend, __objc_msgSend_uncached
ldr p13, [x0]
:
ldr,加载指令,加载p13、[x0]
p13:是isa
[x0]:消息接收者
GetClassFromIsa_p16 p13, 1, x0
:对p13、x0、1等这些数据,进行处理;
GetClassFromIsa_p16
源码分析:
__has_feature
:判断编译器,对()内的条件,是否支持; ptrauth_calls
:针对 arm64 架构,进行指针验证;使用Apple A12或更高版本A系列处理器的设备(如iPhone XS、iPhone XS Max和iPhone XR或更新的设备)支持arm64e架构,具体可参考苹果原文; developer.apple.com
所以,发消息之前,需要通过消息接收者,去获取class。相当于,需要通过person对象,去获取LGPerson这个class;
获取class,主要是因为,objc_msgSend是存在cache内,而cache是保存在class中,所以,必须得到class,才能执行objc_msgSend;
CacheLookup
我们知道,objc_msgSend其实就是通过 sel 去查找imp的一个过程;那么我们就来分析一下这个过程;
上一章节,主要讲通过对比消息接收者,去获取isa,再根据isa,获取class,接下来讲解为什么要获取的class,它的用处是什么。
获取 index
在源码中,LGetIsaDone
结束后,执行了CacheLookup
这个函数;
在上篇文章中,我们知道,sel、imp是存储在bucket中,而buckets存在cache_t中,所以在上图中,通过掩码&cache_t,可以拿到buckes;
但是在arm架构下,汇编的源码,比较复杂,所以我们切换架构,采用mac
架构分析接下来的流程;
毕竟现在我们是在mac系统下,源码结合汇编,能更简单直接的分析源码;
我们需要通过sel去获取方法对应的imp,而imp存在所bucket内,buckets里,包含很多个bucket,所以需要知道imp对应的bucket,获取bucket,需要知道bucket对应的下标,下标可以通过哈希获取;
通过上述流程,我们获取到了buckets、index等数据,得到这些数据后,接下来,我们就开始获取对应的bucket;
获取要查找bucket
查找bucket的流程,也是通过不断的内存地址平移获取,那么得到bucket后,我们需要查找sel对应的imp。那么就是执行CacheHit
函数;
CacheHit
总结
在代码的基础上,我们分析了objc_msgSend的发消息流程。最后简单做一下总结:
通过对比消息接收者,获取isa,再通过isa获取到class对象;class 通过内存平移,得到cache;sel、imp 保存在 bucket结构内,buckets内部包含多个bucket结构; buckets 结构是cache结构的组成部分,通过掩码&cache_t获取buckets,再通过index下标,再buckets内部,获取对应的bucket;得到bucket,sel、imp就能获取出来;
快捷键小知识
搜索代码关键词结果比较多的时候,如何快捷查看;
按住command,再点击下拉箭头即可收起
结果如下图:
如此,就能查看搜索的关键词所在文件。