第九节—objc_msgSend(一)方法快速查找流程

本文为L_Ares个人写作,以任何形式转载请表明原文出处。

objc_msgSend可谓是Runtime中的重点,本节重点的重点是探索objc_msgSend的快速发送机制,即通过缓存查找进行消息转发,慢速的查找流程后面再说。

objc_msgSend的快速转发机制是通过汇编来实现的。选择汇编的原因 :

  • C语言中不可能写通过写一个函数来保留未知的参数,并且还要跳转到任一函数的指针,因为C语言是静态的。

  • 汇编是更接近机器指令的语言,而objc_msgSend的重要性决定了它的速度必须够快。

本节又需要使用到objc4-781源码,为什么又要用到它了呢?因为objc_msgSend的源码是在libobjc.A.dyld这个库里面的。

一、找到objc_msgSend

即然objc_msgSendOC方法调用的本质,那么我们就在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 selfSEL,这里的接收者就是那个target,一般情况下,我们传入的都是实例,也可以是类。

2. 获取类信息

还是看图1。

接受者也是类,是类就有相应的结构,就有isa,就可以通过isa中的shiftcls获取类的信息,获取到的类信息会被存储到p16寄存器上。

3. 怎么获取到的类信息

到这里,我们可以在本文件下搜索一下GetClassFromIsa_p16,看看它里面怎么从isa把类信息拿到存储到p16寄存器的。

GetClassFromIsa_p16 :

图7.png

特别熟悉的思路吧,在isa的章节,见过这个思路吧。

isa存储类信息的具体位置就是isa中的shiftcls,在isa的章节中,已经介绍过了如何可以取到shiftcls的类信息,可以通过平移地址,更简单的是使用掩码maskshiftcls与类无关的信息遮盖住,仅留出类信息的展示,然后得到类的信息。

然后通过这个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. 拆分maskbuckets

#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 & 0x0000ffffffffffffp11里面不是有maskbuckets吗,我们不要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)

我们拆开看

  • p10bucketsp12_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. 把拿到的bucketimpsel放入寄存器

ldp p17, p9, [x12]      // {imp, sel} = *bucket

p17 = impp9 = sel

5. 判断_cmdbucket中的sel是否相等

1:  cmp p9, p1          // if (bucket->sel != _cmd)

6. _cmdsel不一样

    b.ne    2f          //     scan more

不一样的话就跳到2f这个函数中,2f会在下面写出来,前面带有2 :的就是。

7. _cmdsel一样

CacheHit $0         // call or return imp

_cmdsel一样,那么就命中了缓存,直接返回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_cmdsel不一样的时候跳进来了。

  • CheckMiss $0 :如果一直都找不到, 因为是normal ,跳转至__objc_msgSend_uncached,这个下面我会再重点说,这里先这么记着。

  • 比较p12p10p12是我们缓存中找到的bucketp10buckets,这个比较就是判断bucket是不是已经是buckets里面的第一个元素了。

  • 如果是第一个,跳到下面的3f

  • 如果不是第一个,就从最后一个元素开始往前一个找,p17p9就会存储再往前一个的impsel

  • 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),这就很明显了。

maskcache_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_tinsert非常相似的,只不过这是查询,cache_insert是插入。

三、objc_msgSend快速查找机制流程图

objc_msgSend快速查找机制.png
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,919评论 6 502
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,567评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,316评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,294评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,318评论 6 390
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,245评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,120评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,964评论 0 275
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,376评论 1 313
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,592评论 2 333
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,764评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,460评论 5 344
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,070评论 3 327
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,697评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,846评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,819评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,665评论 2 354