美文网首页
objc_msgSend() 快速查找流程

objc_msgSend() 快速查找流程

作者: spades_K | 来源:发表于2020-11-02 16:48 被阅读0次

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

    快速查找整个过程值变化

    相关文章

      网友评论

          本文标题:objc_msgSend() 快速查找流程

          本文链接:https://www.haomeiwen.com/subject/yjxjvktx.html