美文网首页
objc_msgSend:方法的慢速查找

objc_msgSend:方法的慢速查找

作者: Bel李玉 | 来源:发表于2020-09-25 00:41 被阅读0次

    objc_msgSend:方法的快速查找的文章中,我们讲述了方法的快速查找流程,在快速查找的过程中,如果没有找到该方法,就会调用lookUpImpOrForward方法,那么就会进入到方法的慢速查找流程。

    分析

    我们先创建LYPerson类,并添加如下方法:

    @interface LYPerson : NSObject
    - (void)sayHello;
    - (void)say666;
    - (void)sayMaster;
    - (void)sayGoodNight;
    @end
    
    @implementation LYPerson
    - (void)sayHello{
        NSLog(@"LGPerson say : Hello!!!");
    }
    -(void)say666 {
        NSLog(@"66666");
    }
    -(void)sayGoodNight {
        NSLog(@"good Night");
    }
    -(void)sayMaster {
        NSLog(@"Master");
    }
    @end
    

    并且给LYPerson添加一个Category,并且重写say666方法

    @interface LYPerson (Extension)
    
    @end
    @implementation LYPerson (Extension)
    - (void)say666 {
        NSLog(@"888888888");
    }
    @end
    

    我们分别调用这4种方法,并设置断点,进行跟踪分析

    LYPerson *p = [LYPerson alloc] ;
    [p sayMaster];
    [p sayGoodNight];
    [p sayHello];
    [p say666];
    

    1,在cache里面找不到目标imp时,会发送objc_msgSend_uncached,其实现使用汇编实现的:

    STATIC_ENTRY __objc_msgSend_uncached    
        MethodTableLookup NORMAL    // returns IMP in r12 
        bx  r12
    END_ENTRY __objc_msgSend_uncached
    

    2,调用MethodTableLookup,也是用汇编语言实现的:

    .macro MethodTableLookup
        stmfd   sp!, {r0-r3,r7,lr}
        add r7, sp, #16
        sub sp, #8          // align stack
        FP_SAVE
    
        // lookUpImpOrForward(obj, sel, cls, LOOKUP_INITIALIZE | LOOKUP_RESOLVER)
    .if $0 == NORMAL
        // receiver already in r0
        // selector already in r1
    .else
        mov     r0, r1          // receiver
        mov     r1, r2          // selector
    .endif
        mov r2, r9          // class to search
        mov r3, #3          // LOOKUP_INITIALIZE | LOOKUP_INITIALIZE
        blx _lookUpImpOrForward // 1
        mov r12, r0         // r12 = IMP
        
    .if $0 == NORMAL
        cmp r12, r12        // set eq for nonstret forwarding
    .else
        tst r12, r12        // set ne for stret forwarding
    .endif
    
        FP_RESTORE
        add sp, #8          // align stack
        ldmfd   sp!, {r0-r3,r7,lr}
    
    .endmacro
    
    • 参数为normal,就会调用_lookUpImpOrForward方法。

    3,_lookUpImpOrForward的实现如下:

    IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior)
    {
        const IMP forward_imp = (IMP)_objc_msgForward_impcache;
        IMP imp = nil;
        Class curClass;
    
        runtimeLock.assertUnlocked();
        // Optimistic cache lookup
        if (fastpath(behavior & LOOKUP_CACHE)) {
            imp = cache_getImp(cls, sel);
            if (imp) goto done_nolock;
        }
        .....
        curClass = cls;
        for (unsigned attempts = unreasonableClassCount();;) {
            // curClass method list.
            Method meth = getMethodNoSuper_nolock(curClass, sel); // 1
            if (meth) {
                imp = meth->imp;
                goto done;
            }
            ......
            if (slowpath((curClass = curClass->superclass) == nil)) {
                imp = forward_imp;
                break;
            }
            //从父类的Cache里面进行查找
            // Superclass cache.
            imp = cache_getImp(curClass, sel); // 2
            if (slowpath(imp == forward_imp)) {
                // Found a forward:: entry in a superclass.
                // Stop searching, but don't cache yet; call method
                // resolver for this class first.
                break;
            }
            if (fastpath(imp)) {
                // Found the method in a superclass. Cache it in this class.
                goto done;
            }
        }
    
        // No implementation found. Try method resolver once.
        if (slowpath(behavior & LOOKUP_RESOLVER)) {
            behavior ^= LOOKUP_RESOLVER;
            return resolveMethod_locked(inst, sel, cls, behavior);
        }
     done:
        log_and_fill_cache(cls, imp, sel, inst, curClass); // 3
        runtimeLock.unlock();
     done_nolock:
        if (slowpath((behavior & LOOKUP_NIL) && imp == forward_imp)) {
            return nil;
        }
        return imp;
    }
    
    • 1,先在本类methods()里面查找是否有目标方法
    • 2,如果在本类没有找到,然后在其父类先进行快速查找,然后进行慢速查找
    • 3,如果在本类method()中找到目标方法,则将该方法插入到cache中。

    接下来,我们来详细分析getMethodNoSuper_nolock方法

    getMethodNoSuper_nolock

    其实现如下:

    static method_t *
    getMethodNoSuper_nolock(Class cls, SEL sel)
    {
        runtimeLock.assertLocked();
    
        ASSERT(cls->isRealized());
        // fixme nil cls? 
        // fixme nil sel?
    
        auto const methods = cls->data()->methods();
        for (auto mlists = methods.beginLists(),
                  end = methods.endLists();
             mlists != end;
             ++mlists)
        {
            // <rdar://problem/46904873> getMethodNoSuper_nolock is the hottest
            // caller of search_method_list, inlining it turns
            // getMethodNoSuper_nolock into a frame-less function and eliminates
            // any store from this codepath.
            method_t *m = search_method_list_inline(*mlists, sel);
            if (m) return m;
        }
    
        return nil;
    }
    
    ALWAYS_INLINE static method_t *
    search_method_list_inline(const method_list_t *mlist, SEL sel)
    {
        int methodListIsFixedUp = mlist->isFixedUp();
        int methodListHasExpectedSize = mlist->entsize() == sizeof(method_t);
        if (fastpath(methodListIsFixedUp && methodListHasExpectedSize)) {
            return findMethodInSortedMethodList(sel, mlist);
        } else {
            // Linear search of unsorted method list
            for (auto& meth : *mlist) {
                if (meth.name == sel) return &meth;
            }
        }
        return nil;
    }
    
    ALWAYS_INLINE 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); // 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;
    }
    

    我们可以看出,查找逻辑在findMethodInSortedMethodList方法里面。接下来,我们来看下当前 methods的值分布:
    1,查看方法的总数量,可以看出一共有 5个方法

    (lldb) po list->count
    5
    

    2,查看list

    (lldb) p list
    (const method_list_t *) $10 = 0x0000000100002040
    (lldb) p *list
    (const method_list_t) $11 = {
      entsize_list_tt<method_t, method_list_t, 3> = {
        entsizeAndFlags = 26
        count = 5
        first = {
          name = "sayHello"
          types = 0x0000000100000f9c "v16@0:8"
          imp = 0x0000000100000cd0 (KCObjc`-[LYPerson sayHello])
        }
      }
    }
    

    3,分别查看每个方法

    (lldb) p $11.get(0)
    (method_t) $12 = {
      name = "sayHello"
      types = 0x0000000100000f9c "v16@0:8"
      imp = 0x0000000100000cd0 (KCObjc`-[LYPerson sayHello])
    }
    (lldb) p $11.get(1)
    (method_t) $13 = {
      name = "sayMaster"
      types = 0x0000000100000f9c "v16@0:8"
      imp = 0x0000000100000d60 (KCObjc`-[LYPerson sayMaster])
    }
    (lldb) p $11.get(2)
    (method_t) $14 = {
      name = "sayGoodNight"
      types = 0x0000000100000f9c "v16@0:8"
      imp = 0x0000000100000d30 (KCObjc`-[LYPerson sayGoodNight])
    }
    (lldb) p $11.get(3)
    (method_t) $15 = {
      name = "say666"
      types = 0x0000000100000f9c "v16@0:8"
      imp = 0x0000000100000e00 (KCObjc`-[LYPerson(Extension) say666])
    }
    (lldb) p $11.get(4)
    (method_t) $16 = {
      name = "say666"
      types = 0x0000000100000f9c "v16@0:8"
      imp = 0x0000000100000d00 (KCObjc`-[LYPerson say666])
    }
    

    我们在这里可以看出分类的方法放到了类中同名方法的前面

    问题一 :它是按照什么顺序进行排列的呢???
    通过源码可知,它是通过比较 (uintptr_t)probe->name的值进行查找的,那我们来分别输出每个方法的该值。

    (lldb) p (uintptr_t)$11.get(0).name
    (uintptr_t) $18 = 4294971064
    (lldb) p (uintptr_t)$11.get(1).name
    (uintptr_t) $19 = 4294971073
    (lldb) p (uintptr_t)$11.get(2).name
    (uintptr_t) $20 = 4294971083
    (lldb) p (uintptr_t)$11.get(3).name
    (uintptr_t) $21 = 4294971096
    (lldb) p (uintptr_t)$11.get(4).name
    (uintptr_t) $22 = 4294971096
    

    我们可以看出它是按照(uintptr_t)probe->name的升序进行排列的。

    问题二:它是用什么算法进行查找的???
    count >> 1 == count / 2,通过分析我们可知,它是通过二分查找算法进行查找的。

    问题三:如果分类重写了方法,怎样才能保证返回的是分类里面的方法呢?

      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;
            }
    

    在前面我们验证过,分类覆盖的方法会放在本类同名方法的前面。当找到目标probe时,会向前继续查找,直到找到在最前面的probe,这样,我们就确保了,找到的是分类里面的方法。

    总结:

    1,当在类中的cache中,找不到目标方法,会进入方法的慢速查找流程
    2,在类的methods里面进行查找时,使用的是二分查找算法。同时采用向前查找的策略,来保证找到是分类中的同名实现
    3,如果在 自身类中没有找到,会在其父类中先进行快速查找,然后进行慢速查找。直到class == nil为止,即在NSObject中也找不到(NSObject的父类为 nil)
    4,如果找到目标方法,将其插入到cache中。

    其流程如下图所示:


    objc_msgSend_慢速查找流程.png

    相关文章

      网友评论

          本文标题:objc_msgSend:方法的慢速查找

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