本文为L_Ares个人写作,以任何形式转载请表明原文出处。
objc_msgSend
可谓是Runtime
中的重点,本节重点的重点是探索objc_msgSend
的快速发送机制,即通过缓存查找进行消息转发,慢速的查找流程后面再说。
objc_msgSend
的快速转发机制是通过汇编来实现的。选择汇编的原因 :
C
语言中不可能写通过写一个函数来保留未知的参数,并且还要跳转到任一函数的指针,因为C
语言是静态的。汇编是更接近机器指令的语言,而
objc_msgSend
的重要性决定了它的速度必须够快。
本节又需要使用到objc4-781源码,为什么又要用到它了呢?因为objc_msgSend
的源码是在libobjc.A.dyld
这个库里面的。
一、找到objc_msgSend
即然objc_msgSend
是OC方法调用的本质,那么我们就在main.m
中调用OC
的方法来进入objc_msgSend
。
main.m
中代码 :
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
JDPerson *person = [[JDPerson alloc] init];
[person studyWork]; //挂上断点
NSLog(@"Hello, World!");
}
return 0;
}
然后打开xcode
的汇编查看。打上勾。
找到objc_msgSend
。并且挂上断点,走到断点上。
然后按住control
,点击step into
,进入objc_msgSend
。
现在就找到了objc_msgSend
的汇编实现,并且这里也解释了,为什么要用[objc4-781源码],看最上面的libobjc.A.dyld
,证明objc_msgSend
是在这个库里面的。
即然找到了objc_msgSend
的所在库,并且知道objc_msgSend
的快速发送机制是汇编,我们就可以全局搜索,去找到它的实现。
找我们的arm64
架构下的汇编文件。
然后找到objc_msgSend
的入口ENTRY
,点进去,这里就是objc_msgSend
快速转发机制的入口。
二、解析objc_msgSend的汇编
在解析objc_msgSend
的汇编之前,我们要明确几个知识点 :
- 在汇编里面,有个东西叫做寄存器,
arm64
架构下面有31个通用寄存器的存在。每一个都是64位,它们的标记是x0
`x30`,也会看到`w0`w30
,这是用来访问寄存器的低32位用的。- 寄存器的
x0
---x7
位置存储的是函数入参的前8个参数。- 根据上一条可以得知,
objc_msgSend
传入的(id)self
对应着x0
,传入的SEL selector
对应着x1
- 寄存器的
x0
不止是第一个参数的位置,还是返回值在返回后存储的位置。
了解上上面的知识点之后,我们来看汇编。
1. 判断objc_msgSend的接受者是否为空
接收者(receiver
) :
就是objc_msgSend
的第一个参数,还记得objc_msgSend
的参数吗?
id self
和SEL
,这里的接收者就是那个target
,一般情况下,我们传入的都是实例,也可以是类。
2. 获取类信息
还是看图1。
接受者也是类,是类就有相应的结构,就有isa
,就可以通过isa
中的shiftcls
获取类的信息,获取到的类信息会被存储到p16
寄存器上。
3. 怎么获取到的类信息
到这里,我们可以在本文件下搜索一下GetClassFromIsa_p16
,看看它里面怎么从isa
把类信息拿到存储到p16
寄存器的。
GetClassFromIsa_p16 :
特别熟悉的思路吧,在isa的章节,见过这个思路吧。
isa
存储类信息的具体位置就是isa
中的shiftcls
,在isa的章节中,已经介绍过了如何可以取到shiftcls
的类信息,可以通过平移地址,更简单的是使用掩码mask
把shiftcls
与类无关的信息遮盖住,仅留出类信息的展示,然后得到类的信息。
然后通过这个isa
中存储的类地址和掩码mask
我们可以取得父类的信息。
就是isa & mask
,具体流程点击上面蓝色的链接可以过去看。
所以现在isa
被转移到了p16
寄存器上吧,而且是只持有类信息的isa
,没有其他杂七杂八的属性了。
然后我们回到主线,继续看。
4. 缓存查找方法实现
类信息获取完成后,就可以去找类中的方法信息。
详细的思路我写在注释里面了。看图
下面我们看一下CacheLookup
是怎么查找的。
5. 缓存查找方法的实现
CacheLookup :
先看一下一会就要看到的宏定义都代表着什么 。这都是一会儿要用到的宏,先记住。
CACHE
是16,因为一个__SIZEOF_POINTER__
的大小是8位吧。我们是探索
arm64
下的objc_msgSend
,
所以CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
。所以
CACHE_MASK_STORAGE_HIGH_16
才是我们会走的arm64
的架构宏吧。所以PTRSHIFT
= 3还有要理解
buckets
是散列表,是一张表,bucket
只是buckets
中的一个成员,buckets
里面可以有很多的bucket
就像数组和元素一样。
然后继续看CacheLookup
的汇编
先看官方给的注释 :
一段一段的说明 :
1. 获取cache
ldr p11, [x16, #CACHE] // p11 = mask|buckets
这是获取
p16
寄存器上的cache_t
,然后把cache_t
中的mask|buckets
放到p11
寄存器上。cache_t
中高16位存mask
,低48位存buckets
。
2. 拆分mask
和buckets
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
and p10, p11, #0x0000ffffffffffff // p10 = buckets
and p12, p1, p11, LSR #48 // x12 = _cmd & mask
#endif
第1句汇编的意思是 :
p10 = p11 & 0x0000ffffffffffff
。p11
里面不是有mask
和buckets
吗,我们不要mask
,就要buckets
,把buckets
存放在p10
寄存器。第2句汇编拆开看 :
(1).p11, LSR #48
是把p11
也就是mask|buckets
右移48位,就是抹掉buckets
只留mask
,存放在p11
,也就是说p11
=mask
。
(2). 然后,and p12, p1, p11
的意思p12 = p1 & p11
。
(3).p1
在最开始说过了,是objc_msgSend
的第二个参数selector
,也可以说是_cmd
,为了区分sel
,我们就用_cmd
来表示传进来的selector
。
(4). 所以,p12 = _cmd & mask
,也就是cache_t
中说的传进来的sel
的下标。
3. 拿到一个bucket
add p12, p10, p12, LSL #(1+PTRSHIFT)
// p12 = buckets + ((_cmd & mask) << (1+PTRSHIFT))
就是p12 = p10 + p12 << (1 + PTRSHIFT)
我们拆开看
p10
是buckets
,p12
是_cmd & mask
就是下标,PTRSHIFT
上面说了是3。(1 + PTRSHIFT)
是4,LSL #(1+PTRSHIFT)
就是哈希下标向左移4位,即1 << 4 = 16字节
,是一个bucket_t
结构体的大小吧,之前的章节看bucket_t
的时候说过,bucket_t
结构体在arm64
下,第一个元素是imp
,第二个元素是sel
,这句话就是获得了一个bucket
的大小。p12, LSL #(1+PTRSHIFT)
,这里就是计算buckets
首地址的实际偏移量。add p12, p10, p12
就是p12 = p10 + p12
,根据buckets首地址
+首地址的实际偏移量
,我们可以取到这个hash下标对应着的bucket
。
4. 把拿到的bucket
的imp
和sel
放入寄存器
ldp p17, p9, [x12] // {imp, sel} = *bucket
p17
= imp
,p9
= sel
。
5. 判断_cmd
和bucket
中的sel
是否相等
1: cmp p9, p1 // if (bucket->sel != _cmd)
6. _cmd
和sel
不一样
b.ne 2f // scan more
不一样的话就跳到2f
这个函数中,2f
会在下面写出来,前面带有2 :
的就是。
7. _cmd
和sel
一样
CacheHit $0 // call or return imp
_cmd
和sel
一样,那么就命中了缓存,直接返回p17
寄存器里面的imp
就可以。
8. 2f
函数
2: // not hit: p12 = not-hit bucket
CheckMiss $0 // miss if bucket->sel == 0
cmp p12, p10 // wrap if bucket == buckets
b.eq 3f
ldp p17, p9, [x12, #-BUCKET_SIZE]! // {imp, sel} = *--bucket
b 1b // loop
这里就是2f
,_cmd
和sel
不一样的时候跳进来了。
CheckMiss $0
:如果一直都找不到, 因为是normal ,跳转至__objc_msgSend_uncached
,这个下面我会再重点说,这里先这么记着。比较
p12
和p10
,p12
是我们缓存中找到的bucket
,p10
是buckets
,这个比较就是判断bucket
是不是已经是buckets
里面的第一个元素了。如果是第一个,跳到下面的
3f
。如果不是第一个,就从最后一个元素开始往前一个找,
p17
和p9
就会存储再往前一个的imp
和sel
。bucket
还不是buckets
的第一个元素的情况下,循环的向上一个bucket
找,然后继续做这个判断,循环会在p12
==p10
的时候停止,跳入3f
。
9. 3f
函数
这里就是3f
,走到这里就证明bucket
已经是buckets
的第一个元素了。
3: // wrap: p12 = first bucket, w11 = mask
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
add p12, p12, p11, LSR #(48 - (1+PTRSHIFT))
// p12 = buckets + (mask << 1+PTRSHIFT)
#endif
官方给了注释,p12 = buckets + (mask << 1+PTRSHIFT)
,这就很明显了。
mask
在cache_t
的那节说过,mask = buckets的大小 - 1
,相当于buckets
的最后一个元素的索引,那这就是将buckets
首地址偏移到最后一个bucket
上面。
10. 第二次查找
ldp p17, p9, [x12] // {imp, sel} = *bucket
1: cmp p9, p1 // if (bucket->sel != _cmd)
b.ne 2f // scan more
CacheHit $0 // call or return imp
2: // not hit: p12 = not-hit bucket
CheckMiss $0 // miss if bucket->sel == 0
cmp p12, p10 // wrap if bucket == buckets
b.eq 3f
ldp p17, p9, [x12, #-BUCKET_SIZE]! // {imp, sel} = *--bucket
这里的步骤我就不说了吧,和上面的4,5,6,7,8步骤一模一样的吧,逐步的往上找,一直找一圈,直到再次走到3f
,又把p12
寄存器指向了最后一个bucket
的位置上结束。
11. 结束
LLookupEnd$1:
LLookupRecover$1:
3: // double wrap
JumpMiss $0
.endmacro
这里也说明了,double wrap
,做两次这样的循环。还能走到这里,证明还是没找到_cmd == sel
吧,那就JumpMiss
。
放张图,把上面的内容串起来。其实是和cache_t
的insert
非常相似的,只不过这是查询,cache_insert
是插入。