objc_msgSend() 快速查找流程

上次objc_class 中 cache 分析 分析了方法是怎样缓存的,这篇文章分析下方法是怎么被查找的。

1. 编译时和运行时

  • 编译时: 即编译器对语言的编译阶段,只是对语言进行最基本的检查报错,包括语法分析、词法分析等,将程序代码翻译成计算机能够识别的语言(例如汇编等),编译通过并不意味程序可以成功运行。
  • 运行时: 即程序通过了编译这一关后,把编译好的代码装载到内存中跑起来的阶段,这时候会具体对类型进行检查,不仅仅是对代码的简单扫描分析,若出错程序会崩溃。

可以说编译时是一个静态的阶段,类型错误很明显可以直接检查出来,而运行时动态阶段,开始具体与运行环境结合。

Runtime 怎么被调用?

  • 通过OC代码, [person say666]
  • 通过NSObject代码, isKindOfClass()
  • 通过Runtime APIclass_getInstanceSize
    调用关系

compiler是编译器,runtime system libarary底层库

1、方法本质

定义一个 LGPerson 类。

@interface LGPerson : NSObject
- (void)sayHello;
- (void)sayNB;
@end

@implementation LGPerson
- (void)sayNB{
    NSLog(@"666");
}
- (void)sayHello
{
    NSLog(@"hello");
}
@end

main.m调用

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

通过clang编译器指令编译出来main.cpp文件。

int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 



        LGPerson *person = ((LGPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("LGPerson"), sel_registerName("alloc"));
        ((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("sayNB"));
        ((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("sayHello"));

        NSLog((NSString *)&__NSConstantStringImpl__var_folders_hr_l_56yp8j4y11491njzqx6f880000gn_T_main_93c270_mi_1);
    }
    return 0;
}

可以看出不管是alloc还是 sayNBsayHello都被编译为 objc_msgSend(),可以得出结论方法的本质其实就是 objc_msgSend 消息发送

我们来验证下objc_msgSend效果是否与方法调用一致,加上代码。

        objc_msgSend(person,sel_registerName("sayNB"));
        objc_msgSend(person, sel_registerName("sayHello"));

Tip:
需要引入 #import <objc/message.h>

修改设置

打印结果:


修改设置

修改下代码:

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

@implementation LGTeacher
- (void)sayHello{
    NSLog(@"hello");
}
@end

@interface LGPerson : LGTeacher
- (void)sayHello;
- (void)sayNB;
@end

@implementation LGPerson
- (void)sayNB{
    NSLog(@"666");
}
@end


int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        
        // sel_registerName = �@seletor() = NSSeletorFromString()
        
        // 方法: 消息 : (消息的接受者 . 消息主体)
        LGPerson *person = [LGPerson alloc];
        LGTeacher *teacher = [LGTeacher alloc];

        
        objc_msgSend(person,sel_registerName("sayNB"));
        [person sayNB];
        
        // 消息的接受者其实是 LGTeacher
        [person sayHello];
        
        // objc_msgSendSuper(struct objc_super * _Nonnull super, SEL _Nonnull op, ...)

        struct objc_super lgsuper;
        lgsuper.receiver = person;
        lgsuper.super_class = [LGTeacher class];
        
        objc_msgSendSuper(&lgsuper, sel_registerName("sayHello"));        
    }
    return 0;
}

打印结果

2、objc_msgSend 快速查找流程源码分析

打开源码工程搜索objc_msgSend 查找objc-msg-arm64.s,主要分析arm64架构的实现。

ENTRY _objc_msgSend 
    UNWIND _objc_msgSend, NoFrame 
    
//p0(receiver)和空对比
    cmp p0, #0       
//支持taggedpointer(小对象类型)
#if SUPPORT_TAGGED_POINTERS
    b.le    LNilOrTagged        //  (MSB tagged pointer looks negative) 
#else
// p0 等于 0 时,直接返回 空
    b.eq    LReturnZero 
#endif 

// 通过 指针首地址拿到 isa存入p13
    ldr p13, [x0]       // p13 = isa 
//在64位架构下通过 p16 = isa(p13) & ISA_MASK 拿到类信息
    GetClassFromIsa_p16 p13     // p16 = class 
LGetIsaDone:
// 从类的缓存中寻找 imp
    CacheLookup NORMAL, _objc_msgSend

#if SUPPORT_TAGGED_POINTERS
LNilOrTagged:
//等于空,返回空
    b.eq    LReturnZero     // nil check 

    // tagged
    adrp    x10, _objc_debug_taggedpointer_classes@PAGE
    add x10, x10, _objc_debug_taggedpointer_classes@PAGEOFF
    ubfx    x11, x0, #60, #4
    ldr x16, [x10, x11, LSL #3]
    adrp    x10, _OBJC_CLASS_$___NSUnrecognizedTaggedPointer@PAGE
    add x10, x10, _OBJC_CLASS_$___NSUnrecognizedTaggedPointer@PAGEOFF
    cmp x10, x16
    b.ne    LGetIsaDone

    // ext tagged
    adrp    x10, _objc_debug_taggedpointer_ext_classes@PAGE
    add x10, x10, _objc_debug_taggedpointer_ext_classes@PAGEOFF
    ubfx    x11, x0, #52, #8
    ldr x16, [x10, x11, LSL #3]
    b   LGetIsaDone
// SUPPORT_TAGGED_POINTERS
#endif

LReturnZero:
    // x0 is already zero
    mov x1, #0
    movi    d0, #0
    movi    d1, #0
    movi    d2, #0
    movi    d3, #0
    ret

    END_ENTRY _objc_msgSend
第一步分析

重点来了!

.macro CacheLookup
    //
    // Restart protocol:
    //
    //   As soon as we're past the LLookupStart$1 label we may have loaded
    //   an invalid cache pointer or mask.
    //
    //   When task_restartable_ranges_synchronize() is called,
    //   (or when a signal hits us) before we're past LLookupEnd$1,
    //   then our PC will be reset to LLookupRecover$1 which forcefully
    //   jumps to the cache-miss codepath which have the following
    //   requirements:
    //
    //   GETIMP:
    //     The cache-miss is just returning NULL (setting x0 to 0)
    //
    //   NORMAL and LOOKUP:
    //   - x0 contains the receiver
    //   - x1 contains the selector
    //   - x16 contains the isa
    //   - other registers are set as per calling conventions
    //
LLookupStart$1:

    // p1 = SEL, p16 = isa
    ldr p11, [x16, #CACHE]              // #define CACHE            (2 * __SIZEOF_POINTER__) 通过首地址 内存偏移(2*8) 找到 cache 首地址,在arm64架构中 cahe首地址为 p11 = _maskAndBuckets.

#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
    and p10, p11, #0x0000ffffffffffff   // p10 = buckets 类似isa&ISA_MASK取shiftcls。
    and p12, p1, p11, LSR #48       // x12 = _cmd & mask p11 右移48位得到 mask, mask&sel(第二个参数 p1)得到 hash下标 p12。(insert的时候就是这么计算的下标。)
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
    and p10, p11, #~0xf         // p10 = buckets
    and p11, p11, #0xf          // p11 = maskShift
    mov p12, #0xffff
    lsr p11, p12, p11               // p11 = mask = 0xffff >> p11
    and p12, p1, p11                // x12 = _cmd & mask
#else
#error Unsupported cache mask storage for ARM64.
#endif


    add p12, p10, p12, LSL #(1+PTRSHIFT)
                     // p12 = buckets + ((_cmd & mask) << (1+PTRSHIFT(3)))  buckets指针地址偏移 下标(一个buket占的大小 16)

    ldp p17, p9, [x12]      // {imp, sel} = *bucket 从p12中取出 imp 存到 p17 sel存到p9
1:  cmp p9, p1          // if (bucket->sel != _cmd) 比较取出的p9和p1是否是相同的sel
    b.ne    2f          //     没有找到 调到 2
    CacheHit $0         // call or return imp  命中返回imp
    
2:  // not hit: p12 = not-hit bucket 如果一直都找不到, 因为是normal ,跳转至__objc_msgSend_uncached
    CheckMiss $0            // miss if bucket->sel == 0
    cmp p12, p10        // wrap if bucket == buckets  p12是否等于第一个元素p10
    b.eq    3f          // 等于跳到 3(第二次递归)
    ldp p17, p9, [x12, #-BUCKET_SIZE]!  // {imp, sel} = *--bucket // 首地址平移 一个bucket大小,向前查找
    b   1b          // loop 回到第一步做对比

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)  p11 右移48为mask, -(4) 为左移 4 相当于乘以16(一个bucket大小),其实为把p12置到最后一个位置。
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
    add p12, p12, p11, LSL #(1+PTRSHIFT)
                    // p12 = buckets + (mask << 1+PTRSHIFT)
#else
#error Unsupported cache mask storage for ARM64.
#endif

    // Clone scanning loop to miss instead of hang when cache is corrupt.
    // The slow path may detect any corruption and halt later.

    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:  // 跳转至JumpMiss
    JumpMiss $0

.endmacro


//*********************其他声明******************
.macro CacheHit
.if $0 == NORMAL
    TailCallCachedImp x17, x12, x1, x16 // authenticate and call imp
.elseif $0 == GETIMP
    mov p0, p17
    cbz p0, 9f          // don't ptrauth a nil imp
    AuthAndResignAsIMP x0, x12, x1, x16 // authenticate imp and re-sign as IMP
9:  ret             // return IMP
.elseif $0 == LOOKUP
    // No nil check for ptrauth: the caller would crash anyway when they
    // jump to a nil IMP. We don't care if that jump also fails ptrauth.
    AuthAndResignAsIMP x17, x12, x1, x16    // authenticate imp and re-sign as IMP
    ret             // return imp via x17
.else
.abort oops
.endif
.endmacro

.macro CheckMiss
    // miss if bucket->sel == 0
.if $0 == GETIMP 
//--- 如果为GETIMP ,则跳转至 LGetImpMiss
    cbz p9, LGetImpMiss
.elseif $0 == NORMAL 
//--- 如果为NORMAL ,则跳转至 __objc_msgSend_uncached
    cbz p9, __objc_msgSend_uncached
.elseif $0 == LOOKUP 
//--- 如果为LOOKUP ,则跳转至 __objc_msgLookup_uncached
    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

流程分析:
通过 p16(isa)地址平移,isa + superClass = 16 得到 cache首地址 -->p11,在arm 64中是这么储存的,即p11也为_maskAndBuckets的首地址。

cache_t结构

p11 & #0x0000ffffffffffff 抹掉高 16位得到后48位即为 -->p10bukets,p11 右移48位得到 mask, mask&sel(第二个参数 p1)得到 hash下标 p12。(insert的时候就是这么计算的下标),p12 = buckets + ((_cmd & mask) << (1+PTRSHIFT(3))) buckets指针地址偏移 下标(一个buket占的大小 16),然后从p12中取出 imp 存到 p17 sel存到p9,下面开始循环,用图标的方式说明总结。

快速查找整个过程值变化

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