方法的本质

作者: 只写Bug程序猿 | 来源:发表于2019-12-29 20:15 被阅读0次

    探索方法的本质

    一个最基本的方法调用代码

    void run(){
        NSLog(@"%s",__func__);
    }
    
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            LGPerson *person = [[LGPerson alloc] init];;
            [person sayNB];
        }
        return 0;
    }
    
    

    方法的调用底层到底是个什么东西呢
    我们可以利用clang的一些命令 clang -rewrite-objc main.m -o main.cpp,将main文件编译成c之后的代码

    clang之后的代码
    其中有个问题
    run()方法直接调用,而我们的oc方法被编译成一个objc_msgSend函数(runtime里的消息发送机制)
    run函数在编译器就确定了函数的调用与实现,
    因此,oc的方法的本质就是objc_msgSend(或者objc_msgSendSuper等函数)的调用
    objc_msgSend两个参数,第一个参数是对象是哪个对象的操作,第二个参数就是sel也就是方法,通过sel找到imp的实现,完成方法的调用.就叫做消息发送机制.

    objc_msgSend流程

    方法的查找流程分为两种

    • 快速查找:利用汇编直接从缓存中找
    • 慢速查找:快速查找没有命中,从方法表中查找
      现在方法调用处打一个断点


      打断点

    然后debug->debug WorkFlow ->Always Show Disassembly

    查看汇编
    然后进入汇编
    汇编
    然后按住cotrol + stepIn
    源码位置
    可以看到objc_msgSendlibobjc

    打开源码搜索objc_msgSend,我们直接看汇编,找到.s文件,现在架构大部分都是arm64,所以我们直接看objc-msg-arm64.s文件
    看汇编重要的一点事看ENTRY表示入口,如下图

    找到文件 image.png
    // person - isa - 类
        ldr p13, [x0]       // p13 = isa
    GetClassFromIsa_p16 p13     // p16 = class
    

    p13为isa,因为x0为首地址,[]就是首地址的值,首地址就是isa指针
    GetClassFromIsa_p16是一个宏,下边是实现

    .macro GetClassFromIsa_p16 /* src */
    
    #if SUPPORT_INDEXED_ISA
        // Indexed isa
        mov p16, $0         // optimistically set dst = src
        tbz p16, #ISA_INDEX_IS_NPI_BIT, 1f  // done if not non-pointer isa
        // isa in p16 is indexed
        adrp    x10, _objc_indexed_classes@PAGE
        add x10, x10, _objc_indexed_classes@PAGEOFF
        ubfx    p16, p16, #ISA_INDEX_SHIFT, #ISA_INDEX_BITS  // extract index
        ldr p16, [x10, p16, UXTP #PTRSHIFT] // load class from array
    1:
    
    #elif __LP64__
        // 64-bit packed isa
        and p16, $0, #ISA_MASK
    
    #else
        // 32-bit raw isa
        mov p16, $0
    
    #endif
    
    .endmacro
    

    SUPPORT_INDEXED_ISAindexIsa一般不常用,watchos开发是indexIsa
    and p16, $0, #ISA_MASK这句才是重点,$0是传进来的参数,也就是p13 isa 拿$0与isa_mask进行&运算得到类,在前两篇文章中介绍了对象和类之间是怎么联系起来的,所以我们的p16是一个类
    拿到isa之后进行下边操作

    LGetIsaDone:
        CacheLookup NORMAL  
    

    CacheLookup是一个宏定义,下边是实现代码

    .macro CacheLookup
        // p1 = SEL, p16 = isa
        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
    
    1:   cmp    p9, p1          // if (bucket->sel != _cmd)
        b.ne    2f          //     scan more
        CacheHit $0         // call or return imp
    

    b.ne 2f如果bucket的sel != _cmd找2,否则直接命中返回$0
    快速查找流程结束,如果没有命中就进入慢速查找流程

    2:  // not hit: p12 = not-hit bucket
        CheckMiss $0            // miss if bucket->sel == 0
        cmp p12, p10        // wrap if bucket == buckets
        b.eq    3f              //进行3主要是为了保存一份方便下次查找
        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
    

    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,那么找__objc_msgSend_uncached
    我们搜索__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
    ...
    bl  __class_lookupMethodAndLoadCache3
    ...
    

    这里只展示了一个最重要的一行代码,bl跳转到__class_lookupMethodAndLoadCache3
    我们根据以往经验,汇编会自动在前边加一个_,那么我们去掉一个下划线全局搜_class_lookupMethodAndLoadCache3

    IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls)
    {
        return lookUpImpOrForward(cls, sel, obj, 
                                  YES/*initialize*/, NO/*cache*/, YES/*resolver*/);
    }
    

    来一个demo

    
    @interface LGPerson : NSObject
    
    - (void)sayNB;
    + (void)sayHappay;
    
    @end
    @implementation LGPerson
    
    - (void)sayNB{
        NSLog(@"%s",__func__);
    }
    
    + (void)sayHappay{
        NSLog(@"%s",__func__);
    }
    @end
    
    @interface LGStudent : LGPerson
    - (void)sayHello;
    + (void)sayObjc;
    @end
    @implementation LGStudent
    - (void)sayHello{
        NSLog(@"%s",__func__);
    }
    
    + (void)sayObjc{
        NSLog(@"%s",__func__);
    }
    @end
    
    @interface NSObject (LG)
    - (void)sayMaster;
    + (void)sayEasy;
    @end
    @implementation NSObject (LG)
    
    - (void)sayMaster{
        NSLog(@"%s",__func__);
    }
    + (void)sayEasy{
        NSLog(@"%s",__func__);
    }
    
    @end
    

    LGStuednt继承与LGPerson,LGPerson继承与NSObject
    然后调用

    LGStudent *student = [[LGStudent alloc] init];
            // 对象方法
            // 自己有 - 返回自己
            [student sayHello];
            // 自己没有 - 老爸 -
            // [person sayNB]; // CACHE
            
            [student sayNB];
            // 自己没有 - 老爸没有 - NSObject
            [student sayMaster];
            // 自己没有 - 老爸没有 - NSObject 没有
            // unrecognized selector sent to instance 0x103000450
           [student performSelector:@selector(saySomething)];
    
    • 自己有的时候返回自己的方法
    • 自己没有的时候找父类
    • 老爸没有时找NSObject
    • 都没有,抛出异常
      实例方法的查找会根据继承连去一层一层的找

    类方法的调用

    // 类方法
            // 自己有 - 返回自己
            [LGStudent sayObjc];
            // 自己没有 - 老爸 -
            [LGStudent sayHappay];
            // 自己没有 - 老爸没有 - NSObject
            [LGStudent sayEasy];
            // 自己没有 - 老爸没有 - NSObject 没有
    

    这里会有一个问题如果我这样调用会不会崩溃

     [LGStudent performSelector:@selector(sayMaster)];
    

    会打印

    -[NSObject(LG) sayMaster]
    

    发现不会蹦,因为类方法存在元类里边,我们调用类方法

    • 首先去元类里边找,
    • 元类里边没有找到就招父元类,
    • 没有找到继续根据继承链找,
    • 最后找到根元类(NSObject的元类),根元类又继承与NSObject
    isa流程图.png
    最后找到sayMaster,所以不会崩溃
    看下源码来验证下到底是不是这个流程
    我们上边看汇编,如果缓存没有命中就来到慢速查找流程,也就是_class_lookupMethodAndLoadCache3方法
    IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls)
    {
        return lookUpImpOrForward(cls, sel, obj, 
                                  YES/*initialize*/, NO/*cache*/, YES/*resolver*/);
    }
    
    • 当调用实例方法的时候,cls就是当前类,sel就是调用的方法,obj就是当前实例
    • 当调用类方法的时候,cls则代表的是当前类的元类(MetaClass),因为静态方法是存放在元类里边的
    IMP lookUpImpOrForward(Class cls, SEL sel, id inst, 
                           bool initialize, bool cache, bool resolver)
    {
        IMP imp = nil;
        bool triedResolver = NO;
    
        runtimeLock.assertUnlocked();
    
        // Optimistic cache lookup
        if (cache) {
            imp = cache_getImp(cls, sel);
            if (imp) return imp;
        }
    
        // runtimeLock is held during isRealized and isInitialized checking
        // to prevent races against concurrent realization.
    
        // runtimeLock is held during method search to make
        // method-lookup + cache-fill atomic with respect to method addition.
        // Otherwise, a category could be added but ignored indefinitely because
        // the cache was re-filled with the old value after the cache flush on
        // behalf of the category.
    
        runtimeLock.lock();
        checkIsKnownClass(cls);
    
        if (!cls->isRealized()) {
            realizeClass(cls);
        }
    
        if (initialize  &&  !cls->isInitialized()) {
            runtimeLock.unlock();
            _class_initialize (_class_getNonMetaClass(cls, inst));
            runtimeLock.lock();
            // If sel == initialize, _class_initialize will send +initialize and 
            // then the messenger will send +initialize again after this 
            // procedure finishes. Of course, if this is not being called 
            // from the messenger then it won't happen. 2778172
        }
    
        
     retry:    
        runtimeLock.assertLocked();
    
        // Try this class's cache.
    
        imp = cache_getImp(cls, sel);
        if (imp) goto done;
    
        // Try this class's method lists.
        {
            Method meth = getMethodNoSuper_nolock(cls, sel);
            if (meth) {
                log_and_fill_cache(cls, meth->imp, sel, inst, cls);
                imp = meth->imp;
                goto done;
            }
        }
    
        // Try superclass caches and method lists.
        {
            unsigned attempts = unreasonableClassCount();
            for (Class curClass = cls->superclass;
                 curClass != nil;
                 curClass = curClass->superclass)
            {
                // Halt if there is a cycle in the superclass chain.
                if (--attempts == 0) {
                    _objc_fatal("Memory corruption in class list.");
                }
                
                // Superclass cache.
                imp = cache_getImp(curClass, sel);
                if (imp) {
                    if (imp != (IMP)_objc_msgForward_impcache) {
                        // Found the method in a superclass. Cache it in this class.
                        log_and_fill_cache(cls, imp, sel, inst, curClass);
                        goto done;
                    }
                    else {
                        // Found a forward:: entry in a superclass.
                        // Stop searching, but don't cache yet; call method 
                        // resolver for this class first.
                        break;
                    }
                }
                
                // Superclass method list.
                Method meth = getMethodNoSuper_nolock(curClass, sel);
                if (meth) {
                    log_and_fill_cache(cls, meth->imp, sel, inst, curClass);
                    imp = meth->imp;
                    goto done;
                }
            }
        }
        if (resolver  &&  !triedResolver) {
            runtimeLock.unlock();
            _class_resolveMethod(cls, sel, inst);
            runtimeLock.lock();
            // Don't cache the result; we don't hold the lock so it may have 
            // changed already. Re-do the search from scratch instead.
            triedResolver = YES;
            goto retry;
        }
        imp = (IMP)_objc_msgForward_impcache;
        cache_fill(cls, sel, imp, inst);
     done:
        runtimeLock.unlock();
        return imp;
    }
    

    因为是慢速查找,这里传进来的cache为NO,所以下边这几行代码不看,

    if (cache) {
            imp = cache_getImp(cls, sel);
            if (imp) return imp;
        }
    

    这里加了一把锁,是防止多线程情况下产生错乱,比如同时调用a方法和b方法,那么这里在调用a的时候返回一个b的imp,会产生问题
    checkIsKnownClass,是判断类是否合法

     if (!cls->isRealized()) {
            realizeClass(cls);
        }
    

    这行代码是拿到父类元类,以及类的data里边的rw里的ro里的methodlist等等一系列信息,是为方法查找做准备条件,这里不做重点研究.

    // Try this class's method lists.
        {
            Method meth = getMethodNoSuper_nolock(cls, sel);
            if (meth) {
                log_and_fill_cache(cls, meth->imp, sel, inst, cls);
                imp = meth->imp;
                goto done;
            }
        }
    

    先在本类里边找,getMethodNoSuper_nolock

    getMethodNoSuper_nolock(Class cls, SEL sel)
    {
        runtimeLock.assertLocked();
    
        assert(cls->isRealized());
        // fixme nil cls? 
        // fixme nil sel?
    
        for (auto mlists = cls->data()->methods.beginLists(), 
                  end = cls->data()->methods.endLists(); 
             mlists != end;
             ++mlists)
        {
            method_t *m = search_method_list(*mlists, sel);
            if (m) return m;
        }
    
        return nil;
    }
    

    拿到类的data里的methodList进行循环遍历,
    search_method_list进行二分法查找,查找速度更快,下边为查找算法,这里不做过多研究

    static method_t *findMethodInSortedMethodList(SEL key, const method_list_t *list)
    {
        assert(list);
    
        const method_t * const first = &list->first;
        const method_t *base = first;
        const method_t *probe;
        uintptr_t keyValue = (uintptr_t)key;
        uint32_t count;
        
        for (count = list->count; count != 0; count >>= 1) {
            probe = base + (count >> 1);
            
            uintptr_t probeValue = (uintptr_t)probe->name;
            
            if (keyValue == probeValue) {
                // `probe` is a match.
                // Rewind looking for the *first* occurrence of this value.
                // This is required for correct category overrides.
                while (probe > first && keyValue == (uintptr_t)probe[-1].name) {
                    probe--;
                }
                return (method_t *)probe;
            }
            
            if (keyValue > probeValue) {
                base = probe + 1;
                count--;
            }
        }
        
        return nil;
    }
    

    通过一些列算法找到方法,然后看

    if (meth) {
                log_and_fill_cache(cls, meth->imp, sel, inst, cls);
                imp = meth->imp;
                goto done;
            }
    

    如果找到了method那么下次还慢速查找么,苹果爸爸肯定不会这样蠢的,如果找到就调用log_and_fill_cache进行缓存,下次直接利用汇编快速查找,缓存的操作跟我们研究cache_t结构时是一模模一样样的.
    goto done 然后查找结束

    如果找不到呢,那么就去找父类Try superclass caches and method lists.根据代码注释我们也可以看出来接下来要找父类了.
    如果找父类,就不开始跳汇编了,因为刚开始父类元类等条件我们已经准备好了,那么我们现在直接找父类的cache.

    // Superclass cache.
                imp = cache_getImp(curClass, sel);
    

    如果找到imp,直接goto done直接返回,如果找不到,然后找到父类的方法列表进行查找(流程同在本类中查找流程)
    如果都找不到方法呢,直接报错我们很熟悉的一个错误+[LGStudent sayLove]: unrecognized selector sent to class 0x1000012e8.
    imp = (IMP)_objc_msgForward_impcache;看这句代码.发现点不进去.那么按照国际惯例,全局搜索
    然后选择objc_msg_arm64.s没错又是恶心人的汇编,为什么找他呢,因为其他地方都是调用,没有实现,按照经验STATIC_ENTRY __objc_msgForward_impcache ,是不是很熟悉

    STATIC_ENTRY __objc_msgForward_impcache
    
        // No stret specialization.
        b   __objc_msgForward
    
        END_ENTRY __objc_msgForward_impcache
    ENTRY __objc_msgForward
    
        adrp    x17, __objc_forward_handler@PAGE
        ldr p17, [x17, __objc_forward_handler@PAGEOFF]
        TailCallFunctionPointer x17
        
        END_ENTRY __objc_msgForward
    

    __objc_forward_handler是个啥玩意儿啊,来搜一下(经验告诉我搜不到的时候去掉一个下划线),

    void *_objc_forward_handler = (void*)objc_defaultForwardHandler;
    

    发现他是个这么个玩意儿objc_defaultForwardHandler,点进去看一下我的天啊,出现了好熟悉的代码

    objc_defaultForwardHandler(id self, SEL sel)
    {
        _objc_fatal("%c[%s %s]: unrecognized selector sent to instance %p "
                    "(no message forward handler is installed)", 
                    class_isMetaClass(object_getClass(self)) ? '+' : '-', 
                    object_getClassName(self), sel_getName(sel), self);
    }
    

    这不就是经常出现的报错原因么
    但是找不到方法直接报错,体验很不好,有没有什么方法补救一下呢?
    苹果爸爸告诉我们,可以的,在给你一次机会.可以利用消息转发机制
    https://www.jianshu.com/p/03383d2d395d

    相关文章

      网友评论

        本文标题:方法的本质

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