美文网首页
iOS-OC对象原理_objc_msgSend(一)

iOS-OC对象原理_objc_msgSend(一)

作者: 泽泽伐木类 | 来源:发表于2020-09-24 14:22 被阅读0次

    前言

    在前面我们探索了objc_class,以及该结构体内部的cache_t cacheclass_data_bits_t bits,class_rw_t data等成员内部的结构分布,通过结合之前探索的基础,本片文章就深入探索下非常基础且重要的objc_msgSend方法。

    开始

    我们都知道,在OC中,我们定义的方法通常被称之为消息(Message),调用方法的过程就是发送消息的过程。首先我们在main.m下添加一些简单的代码:

    @interface ZZPerson : NSObject
    - (void)sayHello;
    - (void)sayNB;
    @end
    
    @implementation ZZPerson
    - (void)sayHello
    {
        NSLog(@"sayHello");
    }
    - (void)sayNB
    {
        NSLog(@"sayNB");
    }
    @end
    @interface ZZStudent : ZZPerson
    @end
    @implementation ZZStudent
    @end
    
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            // insert code here...
            ZZPerson *person = [ZZPerson alloc];
            [person sayHello];
            ZZStudent *student = [ZZStudent alloc];
            [student sayNB];
        }
        return 0;
    }
    

    在上面的代码中,我们创建了一个ZZPerson类,添加并实现了两个实例方法:
    - (void)sayHello- (void)sayNB;然后在main方法中创建实例并调用实例方法。然后我们通过clang方式,将main.m转换为main.cpp代码,看发生了什么变化:
    终端执行:

    zeze@localhost ZZObjc % clang -rewrite-objc -fobjc-arc -fobjc-runtime=ios-13.0.0 -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator13.4.sdk main.m
    

    回车后,会在main.m同级位置生成main.cpp,打开main.cpp,并找到对应的main函数:

    int main(int argc, const char * argv[]) {
         /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
    
             ZZPerson *person = ((ZZPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("ZZPerson"), sel_registerName("alloc"));
             ((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("sayHello"));
             ZZStudent *student = ((ZZStudent *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("ZZStudent"), sel_registerName("alloc"));
             ((void (*)(id, SEL))(void *)objc_msgSend)((id)student, sel_registerName("sayNB"));
         }
         return 0;
    }
    

    此时我们会发现,我们之前调用的alloc,sayHello,sayNB等方法都被转换为了objc_msgSend()的方式调用,比如:

    ZZPerson *person = [ZZPerson alloc];
    //编译前
    [person sayHello]; 
    //编译后
    ((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("sayHello"));
    

    这也就意味着我们OC代码中调用的方法在编译后都会转化为objc_msgSend()的方式,而且我们可以通过#import <objc/message.h>直接访问这些方法完成方法的调用,也能达到同样的效果,我们修改下:

    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            // insert code here...
            ZZPerson *person = [ZZPerson alloc];
            [person sayHello];
            objc_msgSend(person,sel_registerName("sayHello"));
            
            ZZStudent *student = [ZZStudent alloc];
            [student sayNB];
            objc_msgSend(student,sel_registerName("sayNB"));
        }
        return 0;
    }
    2020-09-23 14:38:48.415766+0800 KCObjc[34398:60843655] sayHello
    2020-09-23 14:38:48.417288+0800 KCObjc[34398:60843655] sayHello
    2020-09-23 14:38:48.417697+0800 KCObjc[34398:60843655] sayNB
    2020-09-23 14:38:48.417830+0800 KCObjc[34398:60843655] sayNB
    

    注意:可能在编译时会遇到以下报错:Too many arguments to func call....

    截屏2020-09-23 下午2.35.53.png
    此时打开TARGETS->Build Setting:
    截屏2020-09-23 下午2.36.56.png

    Enable Strict Checking of objc_msgSend Calls设置为NO,报错就消失了。

    objc_msgSend()

    objc_msgSend()是如何实现消息发送的呐?或者是怎么通过sel找到对应的imp的呐?
    首先,我们来看下objc_msgSend()的源码实现:

    OBJC_EXPORT id _Nullable
    objc_msgSend(id _Nullable self, SEL _Nonnull op, ...)
        OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);
    

    这里我们发现根本无法跳转到objc_msgSend的实现代码。全局搜索下:

    截屏2020-09-23 下午2.55.29.png
    此时发现message.h中只有方法声明,然后就没有对应的.cpp.mm文件了,再往下看是一些xx.s文件(汇编代码文件),难道是用汇编写的?尝试打开objc-msg-arm64.s,然后会看到:
    /********************************************************************
     *
     * id objc_msgSend(id self, SEL _cmd, ...);
     * IMP objc_msgLookup(id self, SEL _cmd, ...);
     * 
     * objc_msgLookup ABI:
     * IMP returned in x17
     * x16 reserved for our use but not used
     *
     ********************************************************************/
    
    #if SUPPORT_TAGGED_POINTERS
        .data
        .align 3
        .globl _objc_debug_taggedpointer_classes
    _objc_debug_taggedpointer_classes:
        .fill 16, 8, 0
        .globl _objc_debug_taggedpointer_ext_classes
    _objc_debug_taggedpointer_ext_classes:
        .fill 256, 8, 0
    #endif
    
        ENTRY _objc_msgSend            //ENTRY 入口
        UNWIND _objc_msgSend, NoFrame
    ............ 省略
    

    果不其然,还真的是用汇编实现的,苹果你真牛逼。大家都知道汇编的执行效率:更快,更高,更强。所以苹果使用了C++ , 汇编等混编的方式,高效完成一些业务。关于汇编的简单知识,可以看下我的这篇文章:iOS中常见的汇编指令
    关于汇编我也只是了解,但是结合之前的文章和汇编代码旁边的注释就会非常清晰它在做什么,尽管我们不是很懂汇编指令。接下来我们就详细的看下:

        ENTRY _objc_msgSend       //开始入口
        UNWIND _objc_msgSend, NoFrame   // 无窗口界面的方法
    
        cmp p0, #0          // nil check and tagged pointer check
    #if SUPPORT_TAGGED_POINTERS
        b.le    LNilOrTagged        //  (MSB tagged pointer looks negative)
    #else
        b.eq    LReturnZero
    #endif
        ldr p13, [x0]       // p13 = isa  这里是isa_t
        GetClassFromIsa_p16 p13     // p16 = class = clsShift
    LGetIsaDone:
        // calls imp or objc_msgSend_uncached
        CacheLookup NORMAL, _objc_msgSend
    

    简单解释:
    cmp p0, #0: p0 = person实例对象,p0 == 0 ?
    ldr p13, [x0]: p13 = isa(isa_t) 读取对象首地址也就是isa_t isa
    GetClassFromIsa_p16 p13: 从p13中读取Class,即isa.ISA()(64bit中的clsShift),并将class赋值给p16
    CacheLookup NORMAL, _objc_msgSend:开始根据p16(Class) 查询。
    读取到class后执行CacheLookup指令:

    .macro CacheLookup
    
    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
        //.....省略
    #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
    

    ldr p11, [x16, #CACHE] : p11 = x16 + #CACHE = *cache(这里就是从isa平移16个字节)p11 = (cache_t *)cache = _maskAndBuckets;

    #define CACHE            (2 * __SIZEOF_POINTER__)
    

    and p10, p11, #0x0000ffffffffffff : p10 = p11 & 0x0000ffffffffffff = buckets
    add p12, p10, p12, LSL #(1+PTRSHIFT): 从begin位置的bucket开始查询,也就是:

    mask_t begin = cache_hash(sel, m);
    static inline mask_t cache_hash(SEL sel, mask_t mask) 
    {
        return (mask_t)(uintptr_t)sel & mask;
    }
    

    这里的((_cmd & mask) << (1+PTRSHIFT)) ,做了一个左移的操作其实就是为了防止越界,在cache_t::insert()中体现在:

    mask_t m = capacity - 1;
    

    ldp p17, p9, [x12]: p9 = sel , p17 = imp
    cmp p9, p1 : p9 != _cmd ? : b.ne 2f : CacheHit $0,如果成立就跳转到2:否则就是找到CacheHit $0 并返回,其实这里的逻辑跟cache_t::insert()是一样的,我就不一一解释了,代码注释已经很清楚。这里执行完1:后会进入一个循环,知道条件成立后执行CheckMiss $0CacheHit $0;
    CacheHit $0:找到了sel执行,并通过TailCallCachedImp指令返回

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

    CheckMiss $0:当没有找到sel执行该指令

    .macro CheckMiss
        // miss if bucket->sel == 0
    .if $0 == GETIMP
        cbz p9, LGetImpMiss
    .elseif $0 == NORMAL
        cbz p9, __objc_msgSend_uncached
    

    此时,将进入__objc_msgSend_uncached指令:

    .endmacro
    
        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, sp
        // save parameter registers: x0..x8, q0..q7
           // 省略........
        // 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
            //省略 .......
    .endmacro
    

    这里再次发生跳转_lookUpImpOrForward,但该指令在当前汇编源码中已经看不到具体实现了,这里推测应该是又跳转回C++代码了,关于_lookUpImpOrForward的具体实现,我们在另一篇文章中详细说。这里我们先通过简单的流程图做一下总结:

    总结

    objc_msgSend()被调用后,首先会根据传入的对象找到他的类,在objc_class中首先会进入cache_t cache中找到buckets,在buckets中查找是否有存在bucket_t.sel == _cmd,如果找到,就返回,快速查找流程结束;如果没有找到会进入__objc_msgSend_uncached ->MethodTableLookup->_lookUpImpOrForward慢速查找流程
    iOS-OC对象原理_objc_msgSend(二)

    相关文章

      网友评论

          本文标题:iOS-OC对象原理_objc_msgSend(一)

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