前言
之前探索了cache_t的底层结构和一些方法。了解到了cache_t的插入流程,那么这个insert流程是在哪里调起的呢,一起来探索一下。
探索怎么调用insert()
首先找到在哪里调用了insert()方法,全局搜索.insert(
。
找到了两个地方调用了,分别是lookupMethodInClassAndLoadCache()
和log_and_fill_cache()
。
lookupMethodInClassAndLoadCache()
这里有注释,大致意思是这个方法不完整,没有解析和构造程序,但是它只用于.cxx_construct
和.cxx_destruct
,所以没关系。
如此看来这个函数不是我们想找的,但是我还是特别好奇什么时候会调用它,同样全局搜索,发现分别是object_cxxConstructFromClass()
和object_cxxDestructFromClass()
在调用,顺着这两个方法最终找到了alloc()
和dealloc()
。
可以看出这个方法就是在构造和析构的时候才调动的。但是通过断掉调试的时候发现alloc和dealloc都没有走到这个方法来。
比如dealloc方法,要进入object_dispose()
才有可能进入lookupMethodInClassAndLoadCache()
。
我们把对象做了一个弱引用,就进入了object_dispose()
,但是后面的方法里还有has_cxx_dtor
判断。这个暂时还不知道怎么弄,就这样吧,只要满足条件,应该是会进入lookupMethodInClassAndLoadCache()的。
log_and_fill_cache()
通过全局搜索,最终只找到了class_getInstanceMethod
和class_getClassMethod
,看到这两个函数想必大家应该比较熟悉了。但是这个并不是我们的目的,我们是想找到系统是怎么调用insert()的。所以得想其他办法了。
查找到objc_msgSend
既然找不到,还是回到原点,再看看有没有什么线索,最终在objc-cache.mm
文件里,我们注意到了这样一段注释,下面是读/写,有几个方法的使用,这里的方法我们基本都研究过了,上面是读,分别是objc_msgSend
和cache_getImp
,这两个之前没研究过,那或者重点就在这里了。
objc_msgSend
runtime
首先来看看编译时是什么,编译时顾名思义就是正在编译的时候。那啥叫编译呢?就是编译器帮你把源代码翻译成机器能识别的代码(当然只是一般意义上这么说,实际上可能只是翻译成某个中间状态的语言。)。那编译时就是简单的作一些翻译工作,比如检查有没有写错啥关键字了啊,有啥词法分析,语法分析之类的过程,如果发现啥错误编译器就告诉你,这时的错误就叫编译时错误。这个过程中做的类型检查也就叫编译时类型检查,或静态类型检查(所谓静态,就是没把真把代码放内存中运行起来,而只是把代码当作文本来扫描下)。所以有时一些人说编译时还分配内存啥的肯定是错误的说法。
运行时就是代码跑起来了,被装载到内存中去了。
(你的代码保存在磁盘上没装入内存之前是个死家伙,只有跑到内存中才变成活的)。而运行时类型检查就与前面讲的编译时类型检查(或者静态类型检查)不一样,不是简单的扫描代码,而是在内存中做些操作,做些判断。
Runtime有两个版本 一个Legacy
版本(早期版本) ,一个Modern
版本(现行版本)
- 早期版本对应的编程接口:
Objective-C 1.0
,用于Objective-C 1.0, 32位的Mac OS X的平台上 - 现行版本对应的编程接口:
Objective-C 2.0
,用于iPhone程序和Mac OS X v10.5 及以后的系统中的 64 位程序
runtime官方文档:Objective-C Runtime Programming Guide
runtime有三种调用方式,分别是:
Objective-C Code
,比如[obj sayNB]
;Framework&Serivce
,比如isKindofClass
;Runtime API
,比如class_getInstanceSize
;
方法调用的本质
通过查看方法的底层代码,看看是怎么调用的。
OC代码
c++代码
可以看到方法调用的本质就是发送消息,上面有是两种调用obj_msgSend
消息发送的方式,一种是方法不带参数,一种是方法带参数。
- 第一个参数:id – 方法调用的对象,即消息的接收者;
- 第二个参数:SEL – 调用的方法,带参数的sel有”:”,不带参数的则没有;
- 第三个参数或更多:参数 – 方法传入的参数,一个或者更多。
objc_msgSend底层源码
objc_msgSend是运行时runtime实现的,并且是使用汇编语言来完成的,为什么会用汇编呢,是因为汇编语言所编写的程序具有存储空间占用少、执行速度快的特点,而消息发送正需要这样的特点。
开始查找底层代码,直接在源码中全局搜索。
搜索出来的结果很多,但因为是汇编写的,所以直接看.s
文件,可以看到都是objc-msg
,只是版本不同,那我们就直接看arm64
版本的。
根据搜索找到ENTRY _objc_msgSend
,这里就是源码所在。
本人并没有学习过汇编,只能通过查看资料来一句一句的了解其中的意思。
UNWIND _objc_msgSend, NoFrame
UNWIND : 它的原理是记录每个函数的入栈指令到特殊的段。
#define NoFrame 0x02000000
这一句应该是记录_objc_msgSend到这个地址。
cmp p0, #0
// nil check and tagged pointer check
cmp : 比较.(两操作数作减法,仅修改标志位,不回送结果). 。
将p0和#0,进行比较。
#if SUPPORT_TAGGED_POINTERS
b.le LNilOrTagged
// (MSB tagged pointer looks negative)
#else
b.eq LReturnZero
#endif
如果是64位
,SUPPORT_TAGGED_POINTERS = 1
,所以是进入b.le LNilOrTagged
。
b.le LNilOrTagged
b.le :判断上面cmp的值是小于等于,如果满足执行标号,否则直接往下走。
我们传了有值进来所以不满足,往下走。
ldr p13, [x0]
// p13 = isa
是指 p13 = x0栈中的值,而x0在一系列的赋值后就是消息接收者的isa。
而x0来自ldp x0, x1, [sp, #(8*16+0*8)]
,
这一句是指将sp偏移8*16个字节的值取出来,放入x1 和 x0延伸:栈就是指令执行时存放临时变量的内存空间,具有特殊的访问方式:后进先出, Last In Out Firt。
栈是从高地址到低地址存储数据的,栈底是高地址,栈顶是高地址。
FP指向栈底
SP指向栈顶
GetClassFromIsa_p16 p13, 1, x0
// p16 = class
这里是指调用GetClassFromIsa_p16
。
下面分别是LGetIsaDone
、LNilOrTagged
和LReturnZero
,这里面的内容过多,我们下一回再说。那么先开看看GetClassFromIsa_p16
里面做了什么。
GetClassFromIsa_p16
首先判断是否SUPPORT_INDEXED_ISA
,不是,然后再判断是否64位,是64位的,再判断是否\needs_auth
,这个是外面传来的值,传入的是1,所以应该进入else,
即调用ExtractISA p16, \src, \auth_address
。
.macro ExtractISA
and $0, $1, #ISA_MASK
.endmacro
// GetClassFromIsa_p16 p13, 1, x0
// ExtractISA p16, \src, \auth_address
// 这里的操作主要是将x0与上p13,最后赋值给p16。
复制代码
这一步就获取到了消息接收者的class,为什么消息发送要获取到class呢,主要是因为实例对象的cache就是存送在class中。
小结
这一段objc_msgSend的源码差不多就这些,大致就是检查接收者是否为nil,再通过isa拿到class。可是这并没有和cache_t联系上,想要知道后面怎么操作的,请听下回分解。