美文网首页
8、方法的调用原理(1)

8、方法的调用原理(1)

作者: 白马啸红中 | 来源:发表于2020-12-23 15:31 被阅读0次
    1.编译后的方法调用

    还是之前的Person类的源码:

    @interface Person : NSObject
    @property (nonatomic,assign)   int age;
    @property (nonatomic,strong)   NSString *nickname;
    @property (nonatomic,assign)   float height;
    @property (nonatomic,strong)   NSString *name;
    -(void)laugh;
    -(void)cry;
    -(void)run;
    -(void)jump;
    -(void)doNothing;
    @end
    
    @interface Saler : Person
    @property (nonatomic,strong)   NSString *brand;
    @end
    
    @implementation Saler
    @end
    
    @implementation Person
    -(void)laugh
    {
        NSLog(@"LMAO");
    }
    
    -(void)cry
    {
        NSLog(@"cry me a river");
    }
    
    -(void)run
    {
        NSLog(@"run! Forrest run!");
    }
    
    -(void)jump
    {
        NSLog(@"you jump,I jump!");
    }
    
    -(void)doNothing
    {
        NSLog(@"Today,I dont wanna do anything~");
    }
    @end
    
    //main方法中添加
    Saler *person =  [Saler alloc];
            
    person.age = 10;
    person.nickname = @"pp";
    person.height = 180.0;
    person.name = @"ppext";
    person.brand = @"apple";
    
    [person laugh];
    [person cry];
    [person run];
    [person jump];
    [person doNothing];
    [person run];
    [person laugh];
    [person doNothing];
    

    要研究方法调用就绕不过编译后的代码,这里用clang编译一下:

    //编译命令
    clang -rewrite-objc main.m -o main.cpp
    

    并找到main方法中方法调用这段代码:

    Saler *person = ((Saler *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Saler"), sel_registerName("alloc"));
    
    ((void (*)(id, SEL, int))(void *)objc_msgSend)((id)person, sel_registerName("setAge:"), 10);
    ((void (*)(id, SEL, NSString *))(void *)objc_msgSend)((id)person, sel_registerName("setNickname:"), (NSString *)&__NSConstantStringImpl__var_folders_6h_y4xl5w1s77xfp_vrjn_15k200000gp_T_main_2043d5_mi_5);
    ((void (*)(id, SEL, float))(void *)objc_msgSend)((id)person, sel_registerName("setHeight:"), (float)180.);
    ((void (*)(id, SEL, NSString *))(void *)objc_msgSend)((id)person, sel_registerName("setName:"), (NSString *)&__NSConstantStringImpl__var_folders_6h_y4xl5w1s77xfp_vrjn_15k200000gp_T_main_2043d5_mi_6);
    ((void (*)(id, SEL, NSString *))(void *)objc_msgSend)((id)person, sel_registerName("setBrand:"), (NSString *)&__NSConstantStringImpl__var_folders_6h_y4xl5w1s77xfp_vrjn_15k200000gp_T_main_2043d5_mi_7);
    
    ((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("laugh"));
    ((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("cry"));
    ((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("run"));
    ((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("jump"));
    ((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("doNothing"));
    
    ((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("run"));
    ((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("laugh"));
    ((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("doNothing"));
    

    可以总结,不论是类方法alloc,还是对象方法setAgelaugh都是通过objc_msgSend来实现的,翻译翻译什么是这个方法:

    (void)objc_msgSend(id self,SEL _cmd,args)
    

    第一个参数是id类型,大概是实现该方法的对象,包括类和对象;
    第二个参数SEL类型,可以理解为方法名,这是一个包含名字和地址的结构;
    后面的参数args类型不定,数目不定,是根据方法的参数多少来添加的。

    分析的这个是c++的方法,对比Objective-c中,方法只有后续args的参数,没有idSEL,在实例方法中这个id参数显然就是这个对象即self,而_cmd也是可以在方法中打印的,但是这两个参数在方法中是隐藏的,也许是为了保守类对象的秘密,不然类方法需要传类对象,这个可能就暴露的这方面的设计吧,-_-!

    如果说这只是一个推测不能当确信的证据,那就直接开启lldb调试,并且打开显示汇编选项:Debug——Debug Workflow——Always Show Disassembly,然后在方法调用出打个断点,command+R运行,可以得到:

    断点调试打印调用objc_msgSend 这就是对objc_msgSend方法的调用,后续参数调用好想只显示了args部分,和_cmd的部分,self部分可能是隐式调用。
    2.方法调用的流程
    2.1——Cache中查找(快速查找)

    在objc-781.1源码中,全局搜索objc_msgSend可以在objc-msg-arm.sobjc-msg-arm64.sobjc-msg-x86_64.sobjc-msg-i386.s等文件中找到,由于是iOS设备中,所以这里分析arm64的。

    ENTRY _objc_msgSend
        UNWIND _objc_msgSend, NoFrame
    //判断p0是否是空
        cmp p0, #0          // nil check and tagged pointer check
    //如果支持tagged pointer类型
    #if SUPPORT_TAGGED_POINTERS
    //判断 tagged pointer类型或对象是否为nil
        b.le    LNilOrTagged        //  (MSB tagged pointer looks negative)
    #else
        b.eq    LReturnZero
    #endif
        ldr p13, [x0]       // p13 = isa
        GetClassFromIsa_p16 p13     // p16 = class
    //上面是从isa的到类信息的流程  下面是获取到类信息后调用CacheLookup方法
    LGetIsaDone:
        // calls imp or objc_msgSend_uncached
        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
    

    上面注释了部分简单的分析,可以知道最后是调用了CacheLookup方法,在cache_t中查找方法缓存,在源码中搜索这个方法源码:

    .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]              // p11 = mask|buckets
    
    #if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
        and p10, p11, #0x0000ffffffffffff   // p10 = buckets
        and p12, p1, p11, LSR #48       // x12 = _cmd & mask
    #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))
    
        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
    
    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)
    #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:  // double wrap
        JumpMiss $0
    
    .endmacro
    

    这里的汇编代码配合注释可以知道就类似之前文章Cache_t分析中的lldb调试打印,不管是哪个分支,其实大致就是通过位运算将buckets地址计算出来,对比每个bucket中的sel发现找到了bucket就调用CacheHit命中方法:

    // CacheHit: x17 = cached IMP, x12 = address of cached IMP, x1 = SEL, x16 = isa
    .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
    

    CacheHit方法中有三个分支,这里走哪个呢,找到_objc_msgSend的汇编源码进入CacheLookup的语句:

    CacheLookup NORMAL, _objc_msgSend
    

    其他代码并没有对$0进行操作,所以这里$0=NORLMAL,在CacheHit进入NORMAL分支,调用TailCallCachedImp方法,这里通过注释就知道是授权并调用方法的IMPIMP也是方法的具体实现了。

    再看未找到bucket,就继续循环,如果循环比较cmp p12, p10if bucket == buckets是否回到了起始指针位置,然后结束循环,如果回到原始指针位置就进行下一句b.eq 3f即转到第3步,也就是调用JumpMiss:

    .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
    

    CacheHit一样,这里$0=NORLMAL,在JumpMiss进入NORMAL分支,调用__objc_msgSend_uncached方法:

    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
    

    __objc_msgSend_uncached中主要调用的就是MethodTableLookup——在方法列表中查找:

    .macro MethodTableLookup
        
        SAVE_REGS
    
        // lookUpImpOrForward(obj, sel, cls, LOOKUP_INITIALIZE | LOOKUP_RESOLVER)
        // receiver and selector already in x0 and x1
        mov x2, x16
        mov x3, #3
        bl  _lookUpImpOrForward
    
        // IMP in x0
        mov x17, x0
    
        RESTORE_REGS
    
    .endmacro
    

    再次在MethodTableLookup源码中发现了最闪亮的它——_lookUpImpOrForward,通过注释就知道这里已经准备跳出汇编方法,转到源码方法lookUpImpOrForward,继续全局搜索就可以在objc_runtime_new.mm文件中找到这个方法的调用。分析至此可以知道lookUpImpOrForward方法已经跳出了cache的查找。

    小总结
    方法调用之cache查找(快速查找):
    function()——objc_msgSend()——汇编方法调用——得到isa——得到cache——得到buckets——循环查找
    查找命中——CacheHit——调用方法IMP
    查找未命中——JumpMiss——__objc_msgSend_uncached——MethodTableLookup——lookUpImpOrForward()

    相关文章

      网友评论

          本文标题:8、方法的调用原理(1)

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