objc_msgSend() 快速查找流程

2020-11-02  本文已影响0人  spades_K

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

1. 编译时和运行时

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

Runtime 怎么被调用?

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,下面开始循环,用图标的方式说明总结。

快速查找整个过程值变化
上一篇下一篇

猜你喜欢

热点阅读