方法调用(一)-- objc_msgSend快速查找流程

方法调用(一)-- objc_msgSend快速查找流程
方法调用(二)-- 慢速查找流程
方法调用(三)-- 动态方法决议&消息转发


开场白

前一篇文章cache_t分析,对方法调用后,类中会对该方法进行缓存。
而完整的缓存流程,要先进行查找,简要流程图如下:

  • 缓存中不存在:进行缓存,这一步就是cache_t中‘保存’方法实现的内容。
  • 缓存中存在:就直接返回

上图只是简要的缓存流程,本文主要研究的方法调用时,查找过程中做了什么?

1.切入点 - objc_msgSend

定义DZStu类,类中有方法sayHello,并且进行了实现。相关代码如下:

@interface DZStu : NSObject
- (void)sayHello;
@end

@implementation DZStu
- (void)sayHello {
    NSLog(@"%s", __func__);
}
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        DZStu *stu = [DZStu alloc];
        [stu sayHello];
    }
    return 0;
}

通过两种方式,可以知道底层调用的函数是什么。

1.1 汇编

通过在方法调用的位置下断点,也就是[stu sayHello];此行代码,运行后打开汇编

可以看到底层调用的是objc_msgSend函数

1.2 clang

使用clang命令

  • 进入文件所在的目录,示例代码写在main.m文件中,进入main.m文件的路径。
  • 执行命令clang -rewrite-objc -fobjc-arc -fobjc-runtime=ios-13.0.0 -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator14.0.sdk main.m
  • 会在同级目录中生成main.cpp文件。
  • 打开生成的文件,找到main函数

此处也可以看到底层调用的是objc_msgSend函数

1.3 用objc_msgSend函数模拟方法调用

我们可以直接使用objc_msgSend函数来调用sayHello方法:

#import <objc/message.h>
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        DZStu *stu = [DZStu alloc];
        [stu sayHello];
        
        objc_msgSend(stu, sel_registerName("sayHello"));
        
    }
    return 0;
}

运行结果,如图:


此处需要注意:

  1. 需要引用文件objc/message.h
  2. 会有如图中的报错

报错信息是参数过多,此处需要修改xcode中配置,BuildSettings中搜索msg

将如图中的Enable Strict Checking of Objc_msgSend Calls设置为NO

2. objc_msgSend 快速查找流程

2.1 objc_msgSend

ENTRY _objc_msgSend
    UNWIND _objc_msgSend, NoFrame

    cmp p0, #0          // nil check and tagged pointer check
#if SUPPORT_TAGGED_POINTERS
    b.le    LNilOrTagged        //  (MSB tagged pointer looks negative)
#else
    b.eq    LReturnZero
#endif
    ldr p13, [x0]       // p13 = isa
    GetClassFromIsa_p16 p13     // p16 = class
LGetIsaDone:
    // calls imp or objc_msgSend_uncached
    CacheLookup NORMAL, _objc_msgSend
    
#if SUPPORT_TAGGED_POINTERS
LNilOrTagged:
    ...//省略部分代码

#endif

LReturnZero:
    // x0 is already zero
    ...//省略部分代码

    END_ENTRY _objc_msgSend
  • 判断objc_msgSend第一个参数,也就是接受者是否有效,此处也进行nil或者tagged pointer类型的判断。获取到对象的isa
  • GetClassFromIsa_p16 p13,通过获取到的isa,进而获取到类。
  • 最终调用CacheLookup

下面分析GetClassFromIsa_p16这个函数

2.2 GetClassFromIsa_p16

.macro GetClassFromIsa_p16 /* src */

#if SUPPORT_INDEXED_ISA
    // Indexed isa
    mov p16, $0         // optimistically set dst = src
    tbz p16, #ISA_INDEX_IS_NPI_BIT, 1f  // done if not non-pointer isa
    // isa in p16 is indexed
    adrp    x10, _objc_indexed_classes@PAGE
    add x10, x10, _objc_indexed_classes@PAGEOFF
    ubfx    p16, p16, #ISA_INDEX_SHIFT, #ISA_INDEX_BITS  // extract index
    ldr p16, [x10, p16, UXTP #PTRSHIFT] // load class from array
1:

#elif __LP64__
    // 64-bit packed isa
    and p16, $0, #ISA_MASK

#else
    // 32-bit raw isa
    mov p16, $0

#endif

.endmacro
  • #if SUPPORT_INDEXED_ISA:这个值等于0,所以这个条件判断不会进入
  • #elif __LP64__:主要研究的方向是这个分支:
    • $0:传入的参数,就是对象的isa
    • #ISA_MASK:在64位设备上取值是0x0000000ffffffff8ULL
    • 通过操作,也就是通过isa的掩码获取到类信息。并存储到p16中。
  • #else:32位情况下的isa

2.3 CacheLookup

重点部分,真正的快速查找流程:

.macro CacheLookup

LLookupStart$1:

    // p1 = SEL, p16 = isa
    //1.宏CACHE表示两个指针的大小,p11中就是cache中的首地址,也就是mask|buckets
    ldr p11, [x16, #CACHE]              // p11 = mask|buckets

#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
    //2.获取buckets,保存在p10中
    and p10, p11, #0x0000ffffffffffff   // p10 = buckets
    
    //3.获取_cmd & mask,存在p12中,这个值是在buckets中开始查找位置的下标
    and p12, p1, p11, LSR #48       // x12 = _cmd & mask
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
    ......
#else
    ......
#endif

//4.调整p12指向循环的开始位置
    add p12, p10, p12, LSL #(1+PTRSHIFT)
                     // p12 = buckets + ((_cmd & mask) << (1+PTRSHIFT))
//5.获取当前的「imp,sel」
    ldp p17, p9, [x12]      // {imp, sel} = *bucket
    
    //6.查找的bucket的sel与_cmd比较,相等调用CacheHit;不相等时走2
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
    //7.当前的bucket == buckets数组首元素地址
    cmp p12, p10        // wrap if bucket == buckets
    b.eq    3f //相等处理
    //不相等,继续循环
    ldp p17, p9, [x12, #-BUCKET_SIZE]!  // {imp, sel} = *--bucket
    b   1b          // loop

3:  // wrap: p12 = first bucket, w11 = mask
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
//8.遍历到第一个元素时,再次指向最后一个元素
    add p12, p12, p11, LSR #(48 - (1+PTRSHIFT))
                    // p12 = buckets + (mask << 1+PTRSHIFT)
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
    ......
#else
#error Unsupported cache mask storage for ARM64.
#endif

//9.同上,再次进行循环,此次是从最后一个元素开始,向前找
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
    b   1b          // loop

LLookupEnd$1:
LLookupRecover$1:
3:  // double wrap
    JumpMiss $0

.endmacro

关键步骤解释:

  1. ldr p11, [x16, #CACHE]:x16中保存的就是isa,#CACHE是一个宏(#define CACHE (2 * __SIZEOF_POINTER__)表示两个指针大小),p11中保存就是mask|buckets(arm64环境中,mask和buckets存在一起)
  2. and p10, p11, #0x0000ffffffffffff:将buckets保存到p10
  3. and p12, p1, p11, LSR #48:将_cmd & mask保存到p12
  4. add p12, p10, p12, LSL #(1+PTRSHIFT)
    • PTRSHIFT是个宏,值为3,此步相当于p12 = buckets + ((_cmd & mask) << (1+PTRSHIFT))
    • buckets进行指针偏移,偏移的步长是1+PTRSHIFT = 4,偏移步数是:_cmd & mask
  5. ldp p17, p9, [x12]:获取当前定位到的bucket
  6. cmp p9, p1:判断当前的获取到sel与传入的参数p1(_cmd)是否相等
    • 相等:直接走CacheHit流程,从缓存中找到了
    • 不相等:进入循环
  7. 循环中的判断cmp p12, p10:此处是判断当前遍历到的bucket是否就是数组buckets的首地址。如果不是:向前查找
  8. 如果是:执行add p12, p12, p11, LSR #(48 - (1+PTRSHIFT)),相当于p12 = buckets + (mask << 1+PTRSHIFT),buckets偏移到最后的元素(mask总个数。1+PTRSHIFT = 4,同上,偏移的步长)
  9. 再次进行循环,此次是从最后一个元素开始,同上面的循环一样,从后往前查找。

2.4 CheckMiss & JumpMiss

CheckMiss & JumpMiss源码:

.macro CheckMiss
    // miss if bucket->sel == 0
.if $0 == GETIMP
    cbz p9, LGetImpMiss
.elseif $0 == NORMAL
    cbz p9, __objc_msgSend_uncached    //****
.elseif $0 == LOOKUP
    cbz p9, __objc_msgLookup_uncached
.else
.abort oops
.endif
.endmacro

.macro JumpMiss
.if $0 == GETIMP
    b   LGetImpMiss
.elseif $0 == NORMAL
    b   __objc_msgSend_uncached  //****
.elseif $0 == LOOKUP
    b   __objc_msgLookup_uncached
.else
.abort oops
.endif
.endmacro

⏬⏬⏬

STATIC_ENTRY __objc_msgSend_uncached
UNWIND __objc_msgSend_uncached, FrameWithNoSaves

// THIS IS NOT A CALLABLE C FUNCTION
// Out-of-band p16 is the class to search
    
MethodTableLookup   //在方法列表中查找
TailCallFunctionPointer x17

END_ENTRY __objc_msgSend_uncached

⏬⏬⏬

.macro MethodTableLookup
......//省略部分代码
    bl  _lookUpImpOrForward
......//省略部分代码

.endmacro

简要流程:
CheckMissJumpMiss调用到__objc_msgSend_uncached,再调用MethodTableLookup,最后调用_lookUpImpOrForward。这样就结束快速查找流程,开始进入慢速查找。

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