本文为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的汇编查看。打上勾。
图1.png
找到objc_msgSend。并且挂上断点,走到断点上。
图2.png
然后按住control,点击step into,进入objc_msgSend。
图3.png
图4.png
现在就找到了objc_msgSend的汇编实现,并且这里也解释了,为什么要用[objc4-781源码],看最上面的libobjc.A.dyld,证明objc_msgSend是在这个库里面的。
即然找到了objc_msgSend的所在库,并且知道objc_msgSend的快速发送机制是汇编,我们就可以全局搜索,去找到它的实现。
图5.png
找我们的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的接受者是否为空
图6.png
接收者(receiver) :
就是objc_msgSend的第一个参数,还记得objc_msgSend的参数吗?
id self和SEL,这里的接收者就是那个target,一般情况下,我们传入的都是实例,也可以是类。
2. 获取类信息
还是看图1。
接受者也是类,是类就有相应的结构,就有isa,就可以通过isa中的shiftcls获取类的信息,获取到的类信息会被存储到p16寄存器上。
3. 怎么获取到的类信息
到这里,我们可以在本文件下搜索一下GetClassFromIsa_p16,看看它里面怎么从isa把类信息拿到存储到p16寄存器的。
GetClassFromIsa_p16 :
图7.png
特别熟悉的思路吧,在isa的章节,见过这个思路吧。
isa存储类信息的具体位置就是isa中的shiftcls,在isa的章节中,已经介绍过了如何可以取到shiftcls的类信息,可以通过平移地址,更简单的是使用掩码mask把shiftcls与类无关的信息遮盖住,仅留出类信息的展示,然后得到类的信息。
然后通过这个isa中存储的类地址和掩码mask我们可以取得父类的信息。
就是isa & mask,具体流程点击上面蓝色的链接可以过去看。
所以现在isa被转移到了p16寄存器上吧,而且是只持有类信息的isa,没有其他杂七杂八的属性了。
然后我们回到主线,继续看。
4. 缓存查找方法实现
类信息获取完成后,就可以去找类中的方法信息。
详细的思路我写在注释里面了。看图
图8.png
下面我们看一下CacheLookup是怎么查找的。
5. 缓存查找方法的实现
CacheLookup :
图9.png
先看一下一会就要看到的宏定义都代表着什么 。这都是一会儿要用到的宏,先记住。
图9.1.png
图10.png
图11.png
-
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的汇编
先看官方给的注释 :
图12.png
一段一段的说明 :
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是插入。
三、objc_msgSend快速查找机制流程图
objc_msgSend快速查找机制.png













网友评论