OC方法执行流程
对OC的runtime机制稍有了解的都知道,OC调用方法,实际上是通过objc_msgSend
进行的。其调用方法的基本步骤为:
- 判断调用者(receiver 或者 self)是否为空。
- 如果为空,则直接返回。
- 如果不为空,进行步骤2。
- 从当前类的方法缓存中查找该方法的实现。
- 如果查找到,则调用缓存中的imp。
- 如果没有查找到,则进步骤3。
- 调用
_class_lookupMethodAndLoadCache3
方法查找- 从本class以及继承体系中逐级向上查找并填充到缓存中。如果继承关系中也没有寻找,则:
- 调用
_class_resolveMethod
方法,进行补救。但是方法不缓存。 - 转发该方法。
- 抛出异常。
这里,着重分析方法缓存。
方法缓存
在源码中,可以找到类的定义:
1 |
|
从定义中不难看出,方法缓存的字段cache
存储在类中,其定义为:
1 |
|
一个类的所有方法缓存,都在cache_t
中的_buckets
数组中。
那么,objc_msgSend是怎么查找这个方法缓存的呢?
这就需要借助源码来进一步分析(源码可以去官网下载,或者直接调试,断点objc_msgSend方法)。
这里直接调试,断点objc_msgSend
函数来进行分析:
1 | //MsgObject.h |
1 | //MsgObject.m |
1 | //main.m |
运行程序,触发断点,进入objc_msgSend方法内部:
1 | libobjc.A.dylib`objc_msgSend: |
整个方法的汇编,就是这个样子的。我们逐条指令分析(重点分析方法缓存查找部分):
1 | 0x1855b8140 <+0>: cmp x0, #0x0 ; =0x0 |
函数一开始,通过cmp
指令判断X0
是否为nil,即判断self是否为nil,如果为nil,则跳到0x1855b81ac
处执行。(实际上不只是nil,taggedpointer也会走这里)。0x1855b81ac
处为处理self为空或者为taggedpointer的情况,这里不做研究。
ldr
指令获取isa指针。事实上,自从引入taggerpoint之后,isa指针已经不是单纯的指针了,所以需要对其进行处理。而and
指令,就是获取了真正的isa指针。
1 | 0x1855b8150 <+16>: ldp x10, x11, [x16, #0x10] |
上面几条指令,已经获取了isa指针,并存储在X16寄存器中,这条指令,是获取cache
的。
其中,X10
中存储的是_buckets
,而X11中存储的是_mask
和_occupied
。低32位,是_mask
,高32位是_occupied
。
1 | 0x1855b8154 <+20>: and w12, w1, w11 |
这里,w1是_cmd
的低32位,而w11是_mask
,所以这句,实际上是w12 = _cmd & _mask
,旨在获取第一个要查找的index
。
而add x12, x10, x12, lsl #4
这条指令的意思是将X12寄存器的值左移4位(即乘以16),然后与X10寄存器中的值相加,并把结果存放在X12寄存器中。上面已经分析出,X10是_buckets
数组,所以这条指令相当于&_buckets[W12]
。
1 | 0x1855b815c <+28>: ldp x9, x17, [x12] |
从上面可以得出,X12=&_buckets[W12]
,所以ldp指令就把该地址处存储的数据取出,分别存放在X9
,x17
中。由上面的bucket_t
结构体定义可以看出,对于arm64,该处存储的方法的imp
和sel
。
cmp x9, x1
指令,说明了X9存储的是key,用key与sel比较,如果相等,则br x17
,所以X17中的是imp。这点,似乎和上面的定义有点出入。
1 | struct bucket_t { |
源码中对bucket_t
的定义,应该是imp在前,key在后的。此处有疑问。
虽然位置不对,但其实并不影响我们分析。继续往下看:
1 | 0x1855b816c <+44>: cbz x9, 0x1855b8440 ; _objc_msgSend_uncached |
还是和key进行比较,如果key为0,则表示,没有缓存。就跳到0x1855b8440
处执行。
1 | 0x1855b8170 <+48>: cmp x12, x10 |
此处,用X12和X10进行比较,上面已经说了,X12是当前index的位置,而X10是_buckets
数组的起始位置,旨在判断当前位置是否在数组起始位置。如果是,则跳转到0x1855b8180
处,而0x1855b8180
处的指令add x12, x12, w11, uxtw #4
,是将X12(可以看成是一个指针)的值赋为数组的最后一个元素,即将指针移到数组末尾。
1 | 0x1855b8178 <+56>: ldp x9, x17, [x12, #-0x10]! |
这两句,是一个loop循环。翻译为伪C代码:
1 | ldp x9, x17, [x12, #-0x10]! ;====>{X9,X17} = *X12 --X12 |
1 | 0x1855b8180 <+64>: add x12, x12, w11, uxtw #4 |
这段汇编,是不是感觉很熟悉。因为在前 出现过一次,这其实也是一个循环。翻译一下:
1 | X12 += w11;//指针加法,步长为16Byte |
这整个过程,就是方法缓存的查找过程。