ios objc_msgSend分析

2019-12-29  本文已影响0人  瞬间完善

1、objc_msgSend是什么?

我们要分析objc_msgSend,首先我们要知道objc_msgSend是什么?带着问题找答案:
首先我们创建一个工程,


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"));
    }
    return 0;
}

简化一下:

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

现在是不是objc_msgSend(objc_getClass("LGPerson"), sel_registerName("alloc"))很熟悉,就是objc_msgSend(id self, sel _cmd)这个函数,通过这两行代码我们就可以得出;

2、objc_msgSend底层的探索

通过我们看苹果开源的代码,我们发现objc_msgSend是用汇编写的,那这样写有什么好处呢?

补充:寄存器 可以看一下这篇iOS arm64汇编中寄存器和基本指令

汇编首先我们分析

cmp p0, #0  //cmp就是判断第一个寄存器中是否为空,如果为空说明当前没有接收者,就是下面这个判断
#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

在这里将x0指向内存地址的值isa赋值给p13,然后通过GetClassFromIsa_p16返回当前的类class
我们查找isa已经完毕,接下来CacheLookup也就是从缓存中查询,然后我们在源码中可以看到:

* CacheLookup NORMAL|GETIMP|LOOKUP //正常/获取IMP/慢速查找     
// 我们这里分析正常情况NORMAL
.macro CacheLookup
    // p1 = SEL, p16 = isa 
    //[x16, #CACHE] 平移16个字节得到cache,16个字节包括(isa:8字节,superClass:8字节)
    ldp p10, p11, [x16, #CACHE] // p10 = buckets, p11 = occupied|mask
#if !__LP64__
    and w11, w11, 0xffff    // p11 = mask
#endif
    and w12, w1, w11        // x12 = _cmd & mask
    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
    add p12, p12, w11, UXTW #(1+PTRSHIFT)
                                // p12 = buckets + (mask << 1+PTRSHIFT)

    // 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

3:  // double wrap
    JumpMiss $0
    
.endmacro

上面整了一大堆,我们拆开来分析。其实这一部分:

    ldp p10, p11, [x16, #CACHE] // p10 = buckets, p11 = occupied|mask
#if !__LP64__
    and w11, w11, 0xffff    // p11 = mask
#endif
    and w12, w1, w11        // x12 = _cmd & mask
    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

就是把传入的方法和本地存储的方法进行对比,如果一样进行CacheHit返回imp,缓存命中,不一样就是进入第2个判断

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

进入CheckMiss继续查找,然后进行cmp p12, p10 // wrap if bucket == buckets
判断,找到bucket之后又进行了第3个判断b.eq 3f,这是为什么?

add p12, p12, w11, UXTW #(1+PTRSHIFT)

这是为了存储一份方便下次查找。
然后我们看看CheckMiss是怎么执行的

.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

在这段代码中我们可以知道我们进行的是NORMAL方式查找,所以执行cbz p9, __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

来到了这里,执行MethodTableLookup,我们再看一下这个方法:

.macro MethodTableLookup
    
    // push frame
    SignLR
    stp fp, lr, [sp, #-16]!
    mov fp, sq
    //一些位移操作
    // receiver and selector already in x0 and x1
    mov x2, x16
    bl  __class_lookupMethodAndLoadCache3

    // IMP in x0
    mov x17, x0
    
    //一些位移操作

    mov sp, fp
    ldp fp, lr, [sp], #16
    AuthenticateLR

.endmacro

由这里来到了__class_lookupMethodAndLoadCache3,进入到慢速查找的方法中。
汇编操作到这里就结束了,下面就要进入到了C/C++。

总结:

当我们调用一个对象或者类方法的时候,系统会调用
objc_msgSend函数进行的查找,而objc_msgSend是用汇编写的,为什么用汇编?

上一篇 下一篇

猜你喜欢

热点阅读