美文网首页
消息慢速查找

消息慢速查找

作者: 浅墨入画 | 来源:发表于2021-07-31 11:22 被阅读0次

    真机跑汇编

    工程编写如下代码,[person sayHello];添加断点,打开汇编Debug -> Debug Workflow -> Always show Disasemmbly,运行工程(注意真机运行!!!)

    @interface LGPerson : NSObject
    - (void)sayHello;
    @end
    
    @implementation LGPerson
    - (void)sayHello{
        NSLog(@"%s",__func__);
    }
    @end
    int main(int argc, char * argv[]) {
        @autoreleasepool {
            LGPerson *person = [LGPerson alloc];
            [person sayHello];
        }
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
    }
    
    // lldb调试信息
    // 确认当前操作的类是LGPerson
    (lldb) register read x0
          x0 = 0x0000000283924080
    (lldb) po 0x0000000283924080
    <LGPerson: 0x283924080>
    // 确认查找的方法是sayHello
    (lldb) register read x1
          x1 = 0x0000000104fd714a  "sayHello"
    
    image.png

    添加符号断点objc_msgSend,查看汇编

    libobjc.A.dylib`objc_msgSend:
        // 判断receiver是否存在
    ->  0x19a2275c0 <+0>:   cmp    x0, #0x0                  ; =0x0 
        0x19a2275c4 <+4>:   b.le   0x19a227668               ; <+168>
        // 将实例对象person的isa赋值给x13寄存器
        0x19a2275c8 <+8>:   ldr    x13, [x0]
        // 将isa & ISA_MASK得到类LGPerson
        0x19a2275cc <+12>:  and    x16, x13, #0xffffffff8
        // 将x16赋值给x15
        0x19a2275d0 <+16>:  mov    x15, x16
        // 将LGPerson的isa向右移16字节,得到cache_t
        0x19a2275d4 <+20>:  ldr    x11, [x16, #0x10]
        // 将x11 & 0xfffffffffffe,可以保留cache_t的低48位,拿到buckets
        0x19a2275d8 <+24>:  and    x10, x11, #0xfffffffffffe
        // w11的0号位置不为0,跳转不为0,继续向下执行
        0x19a2275dc <+28>:  tbnz   w11, #0x0, 0x19a227630    ; <+112>
        // x12 = _cmd & (_cmd >> 7),mask = x11 >> 48,x12 = x12 & mask,x12为第一个检索的位置,即first_preopt
        0x19a2275e0 <+32>:  eor    x12, x1, x1, lsr #7
        0x19a2275e4 <+36>:  and    x12, x12, x11, lsr #48
        // buckets + first_preopt * 16 得到第一个探索的bucket_t的地址
        0x19a2275e8 <+40>:  add    x13, x10, x12, lsl #4
        // 将bucket_t的sel和imp取出,分别存在x17和x9,然后bucket--,指向当前位置的前一个bucket_t,准备下一次提取
        0x19a2275ec <+44>:  ldp    x17, x9, [x13], #-0x10
        // 比较sel和_cmd,不相等就跳转,否则命中缓存  buckets = buckets & _cmd  buckets = buckets & isa
        0x19a2275f0 <+48>:  cmp    x9, x1
        0x19a2275f4 <+52>:  b.ne   0x19a227600               ; <+64>
        0x19a2275f8 <+56>:  eor    x17, x17, x16
        0x19a2275fc <+60>:  br     x17
        // 判断sel是否存在
        0x19a227600 <+64>:  cbz    x9, 0x19a227940           ; _objc_msgSend_uncached
        // 比较bucket_t与buckets
        0x19a227604 <+68>:  cmp    x13, x10
        0x19a227608 <+72>:  b.hs   0x19a2275ec               ; <+44>
        // 取末尾缓存,取第一次查找的位置,将末位bucket_t的imp和sel赋值给x17和x9,末位bucket--
        0x19a22760c <+76>:  add    x13, x10, x11, lsr #44
        0x19a227610 <+80>:  add    x12, x10, x12, lsl #4
        0x19a227614 <+84>:  ldp    x17, x9, [x13], #-0x10
        // 比较sel和_cmd,相等:命中缓存,不相等:判断sel是否存在,判断当前搜索的bucket_t是否已经搜索到了,如果大于:继续循环搜索,否则跳转0x19a227940,进入慢速查找流程
        0x19a227618 <+88>:  cmp    x9, x1
        0x19a22761c <+92>:  b.eq   0x19a2275f8               ; <+56>
        0x19a227620 <+96>:  cmp    x9, #0x0                  ; =0x0 
        0x19a227624 <+100>: ccmp   x13, x12, #0x0, ne
        0x19a227628 <+104>: b.hi   0x19a227614               ; <+84>
        0x19a22762c <+108>: b      0x19a227940               ; _objc_msgSend_uncached 
        0x19a227630 <+112>: adrp   x9, 213085
        0x19a227634 <+116>: add    x9, x9, #0xfbb            ; =0xfbb 
    ......
    

    汇编缓存找不到

    cache内查找bucket_t的过程中,如果查找了所有的缓存也无法命中的时候,接下来就要进入消息的慢速查找流程了,也就是由汇编查找 -> C/C++代码查找。最终都会走到__objc_msgSend_uncached汇编函数

    • objc-msg-arm64.s文件中查找__objc_msgSend_uncached的汇编实现,其中的核心是MethodTableLookup(即查询方法列表),其源码如下
    注意:

    慢速查找就是不断的遍历methodlist,遍历是一个非常耗时的操作,所以放入C/C++里面提高效率。

    STATIC_ENTRY __objc_msgSend_uncached
    UNWIND __objc_msgSend_uncached, FrameWithNoSaves
    
    // THIS IS NOT A CALLABLE C FUNCTION
    // Out-of-band p15 is the class to search
    // imp  开始查询方法列表
    MethodTableLookup
    TailCallFunctionPointer x17
    
    END_ENTRY __objc_msgSend_uncached
    
    • 全局搜索MethodTableLookup其中的核心是_lookUpImpOrForward源码如下
    .macro MethodTableLookup
        SAVE_REGS MSGSEND
    
        // lookUpImpOrForward(obj, sel, cls, LOOKUP_INITIALIZE | LOOKUP_RESOLVER)
        // receiver and selector already in x0 and x1
        mov x2, x16
        // 核心源码
        bl  _lookUpImpOrForward
        // IMP in x0 --
        mov x17, x0
        RESTORE_REGS MSGSEND
    
    .endmacro
    

    慢速查找流程

    • 全局搜索lookUpImpOrForward,查看objc-runtime-new.mm文件源码如下
    注意:
    1. C/C++中调用汇编 ,去查找汇编时,C/C++调用的方法需要多加一个下划线
    2. 汇编中调用C/C++方法时,去查找C/C++方法,需要将汇编调用的方法去掉一个下划线,所以这里搜索的是lookUpImpOrForward
    lookUpImpOrForward(nil, sel, cls, LOOKUP_RESOLVER);
    
    // 跳转查看lookUpImpOrForward方法,这里的实现使用的汇编,为什么不使用C/C++   ->   因为使用汇编执行流程会更快,而且更加安全
    NEVER_INLINE
    IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior)
    {
        //创建forward_imp,并给定默认值_objc_msgForward_impcache
        const IMP forward_imp = (IMP)_objc_msgForward_impcache;
        //创建imp,用于接收通过sel查找的imp
        IMP imp = nil;
        //创建要查找的类,这个类通过isa的指向关系是会一直变化的,
        //直到最终指向NSObject的父类nil为止
        Class curClass;
    
        runtimeLock.assertUnlocked();
    
        if (slowpath(!cls->isInitialized())) {
            /**
            发送到类的第一条消息通常是 +new 或 +alloc 或 +self
            但是,此时该类尚未初始化,此时behavior = 3|8 = 11
            当将new等方法inset进缓存的时候
            不满足behavior & LOOKUP_NOCACHE) == 0这个条件,8 & 11 = 8
            所以上述这些方法不会加载进缓存。
            
            如果类已经初始化了,就不会修改behavior的值了,behavior=3
            我们自定义的方法是可以正常加载进缓存的。
            */
            behavior |= LOOKUP_NOCACHE;
        }
    
        // runtimeLock 在 isRealized 和 isInitialized 检查期间被持有
        // 防止与并发实现竞争。
        runtimeLock.lock();
    
        //检查类是否被注册了
        checkIsKnownClass(cls);
    
        /**初始化跟cls实例对象在isa指向图中的每一个类(class和metaClass)
        以便后续自己类里面找不到方法去父类里面找
        依次向上找
        所以在此处对所有相关的类进行了初始化
        */
        cls = realizeAndInitializeIfNeeded_locked(inst, cls, behavior & LOOKUP_INITIALIZE);
        // runtimeLock may have been dropped but is now locked again
        runtimeLock.assertLocked();
        //curClass为当前实例对象的类
        curClass = cls;
    
        /**
        * 循环查找类对象的methodList,当前类没有的话就找父类
        * 父类没有就找父类的父类,一直找到NSObject类
        * 如果NSObject都找不到的话最终curClass会指向nil
        * 将事先准备好的forward_imp赋值给imp
        * 然后结束慢速查找流程,接下来进入Runtime消息转发机制
        */
        for (unsigned attempts = unreasonableClassCount();;) {
            if (curClass->cache.isConstantOptimizedCache(/* strict */true)) {
    #if CONFIG_USE_PREOPT_CACHES
                /**
                * 第一步先找共享缓存里面有没有我们的方法
                * 通常情况下我们的自定义方法不会出现在共享缓存中
                */
                imp = cache_getImp(curClass, sel);
                if (imp) goto done_unlock;
                curClass = curClass->cache.preoptFallbackClass();
    #endif
            } else {
                /**
                *在当前类的方法列表里面查找,这是重点
                *查找算法是二分法
                */
                Method meth = getMethodNoSuper_nolock(curClass, sel);
                if (meth) {
                    imp = meth->imp(false);
                    goto done;
                }
                /**
                *如果当前类找不到,取将curClass指向superclass
                *查询父类的methodList,一直找到NSObject的父类nil为止
                * 将事先准备好的forward_imp赋值给imp
                * 然后结束慢速查找流程,接下来进入Runtime消息转发机制
                * 结束循环遍历
                */
                if (slowpath((curClass = curClass->getSuperclass()) == nil)) {
                    imp = forward_imp;
                    break;
                }
            }
    
            // 类列表中的内存损坏
            if (slowpath(--attempts == 0)) {
                _objc_fatal("Memory corruption in class list.");
            }
    
            // 在父类的缓存中查找,这里再次进入汇编查找流程
            imp = cache_getImp(curClass, sel);
            //如果没有找到,将默认的forward_imp赋值给imp
            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)) {
                //将找到的method插入到缓存中,以便下次查找使用快速缓存查找
                goto done;
            }
        }
    
        // No implementation found. Try method resolver once.
    
        if (slowpath(behavior & LOOKUP_RESOLVER)) {
            behavior ^= LOOKUP_RESOLVER;
            return resolveMethod_locked(inst, sel, cls, behavior);
        }
    
    //找到了sel对应的imp,将method方法加载进缓存
     done:
        if (fastpath((behavior & LOOKUP_NOCACHE) == 0)) {
    #if CONFIG_USE_PREOPT_CACHES
            while (cls->cache.isConstantOptimizedCache(/* strict */true)) {
                cls = cls->cache.preoptFallbackClass();
            }
    #endif
            log_and_fill_cache(cls, imp, sel, inst, curClass);
        }
     done_unlock:
        runtimeLock.unlock();
        if (slowpath((behavior & LOOKUP_NIL) && imp == forward_imp)) {
            return nil;
        }
        return imp;
    }
    
    • 查看二分法查找方法getMethodNoSuper_nolock源码
    static method_t *
    getMethodNoSuper_nolock(Class cls, SEL sel)
    {
        runtimeLock.assertLocked();
    
        ASSERT(cls->isRealized());
    
        //获取methodList,methodList因为数据类型的原因可能为二维数组
        //循环条件是数组不为空,即开始位置不等于结束位置
        auto const methods = cls->data()->methods();
        for (auto mlists = methods.beginLists(),
                  end = methods.endLists();
             mlists != end;
             ++mlists)
        {
            //进入search_method_list_inline修复为有序list
            method_t *m = search_method_list_inline(*mlists, sel);
            if (m) return m;
        }
    
        return nil;
    }
    
    • 跳转查看排序方法search_method_list_inline源码
    • 查看search_method_list_inline源码
    • 查看findMethodInSortedMethodList源码(已经排好序的methodlist),其中isSmallList代表的是m1电脑
    ALWAYS_INLINE static method_t *
    findMethodInSortedMethodList(SEL key, const method_list_t *list)
    {
        if (list->isSmallList()) {
            if (CONFIG_SHARED_CACHE_RELATIVE_DIRECT_SELECTORS && objc::inSharedCache((uintptr_t)list)) {
                return findMethodInSortedMethodList(key, list, [](method_t &m) { return m.getSmallNameAsSEL(); });
            } else {
                return findMethodInSortedMethodList(key, list, [](method_t &m) { return m.getSmallNameAsSELRef(); });
            }
        } else {
            return findMethodInSortedMethodList(key, list, [](method_t &m) { return m.big().name; });
        }
    }
    
    二分查找流程
    • findMethodInSortedMethodList源码如下(二分查找真正的实现方法)
    template<class getNameFunc>
    ALWAYS_INLINE static method_t *
    findMethodInSortedMethodList(SEL key, const method_list_t *list, const getNameFunc &getName)
    {
        ASSERT(list);
        //开始位置:0
        auto first = list->begin();
        //base开始也为0
        auto base = first;
        //probe也为0
        decltype(first) probe;
        //要查找imp对应的sel
        uintptr_t keyValue = (uintptr_t)key;
        //list的个数
        uint32_t count;
        
        /**
        * 举例:假设要查找的sel在第7位
        * 首先count = list.count,这里假定count=8
        * 进入循环,probe = base + ( 8 >> 1 ) = 0 + 4 = 4
        * 那么第一次查找的范围就是4-8,匹配元素位置是4,判定结果keyValue(7)> prebeValue(4),未匹配
        * 满足keyValue > probeValue,base = probe + 1 = 4 + 1 = 5,count-- = 7
        * 第二次进入循环,此时count = 7 >> 1 = 3, probe = 5 + 3 >> 1 = 6
        * 第二次查找的范围是6-7,匹配元素位置是6,判定结果keyValue(7)> prebeValue(6),未匹配
        * 满足keyValue > probeValue,base = probe + 1 = 6 + 1 = 7,count-- = 2
        * 第三次进入循环,此时count = 2 >> 1 = 1, probe = 7 + 1 >> 1 = 7
        * 第三次查找的元素是7,匹配,返回imp
        */
        for (count = list->count; count != 0; count >>= 1) {
            probe = base + (count >> 1);
            
            uintptr_t probeValue = (uintptr_t)getName(probe);
            
            if (keyValue == probeValue) {
                //向前寻找第一个出现的imp,为了避免分类方法与主类方法相同的问题
                //这也就是为什么分类方法会被加载的原因
                while (probe > first && keyValue == (uintptr_t)getName((probe - 1))) {
                    probe--;
                }
                return &*probe;
            }
            
            if (keyValue > probeValue) {
                base = probe + 1;
                count--;
            }
        }
        
        return nil;
    }
    

    上面二分法查找到方法imp之后,lookUpImpOrForward方法的循环查找会执行到goto done。进行缓存填充将其插入缓存,下一次的查找就会进行快速缓存查找,缓存填充源码如下

    static void
    log_and_fill_cache(Class cls, IMP imp, SEL sel, id receiver, Class implementer)
    {
    #if SUPPORT_MESSAGE_LOGGING
        if (slowpath(objcMsgLogEnabled && implementer)) {
            bool cacheIt = logMessageSend(implementer->isMetaClass(), 
                                          cls->nameForLogging(),
                                          implementer->nameForLogging(), 
                                          sel);
            if (!cacheIt) return;
        }
    #endif
        cls->cache.insert(sel, imp, receiver);
    }
    
    慢速查找流程
    image.png

    慢速查找流程案例

    //继承链: LGStudent -> LGPerson -> NSObject
    <!-- LGPerson.h -->
    #import <Foundation/Foundation.h>
    NS_ASSUME_NONNULL_BEGIN
    @interface LGPerson : NSObject
    - (void)sayNB;
    + (void)sayHappay;
    - (void)sayMaster;
    @end
    NS_ASSUME_NONNULL_END
    
    <!-- LGPerson.m -->
    #import "LGPerson.h"
    @implementation LGPerson
    - (void)sayNB{
        NSLog(@"%s",__func__);
    }
    + (void)sayHappay{
        NSLog(@"%s",__func__);
    }
    @end
    
    <!-- LGStudent.h -->
    #import "LGPerson.h"
    NS_ASSUME_NONNULL_BEGIN
    @interface LGStudent : LGPerson
    - (void)sayHello;
    - (void)sayMaster;
    + (void)sayObjc;
    @end
    NS_ASSUME_NONNULL_END
    
    <!-- LGStudent.m -->
    #import "LGStudent.h"
    @implementation LGStudent
    - (void)sayHello{
        NSLog(@"%s",__func__);
    }
    + (void)sayObjc{
        NSLog(@"%s",__func__);
    }
    @end
    
    <!-- main.m -->
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            
    #pragma clang diagnostic push
    // 让编译器忽略错误
    #pragma clang diagnostic ignored "-Wundeclared-selector"
            LGStudent *student = [[LGStudent alloc] init];
            // 对象方法
            [student sayHello];
            [student sayNB];
            [LGStudent sayHappay];
    #pragma clang diagnostic pop
        }
        return 0;
    }
    
    // 控制台成功打印sayNB、sayHappay,说明调用的父类实例方法与类方法
    2021-07-31 10:41:28.194966+0800 002-方法的查找流程[99733:8712527] -[LGStudent sayHello]
    2021-07-31 10:41:28.195823+0800 002-方法的查找流程[99733:8712527] -[LGPerson sayNB]
    2021-07-31 10:41:28.195909+0800 002-方法的查找流程[99733:8712527] +[LGPerson sayHappay]
    

    通过上面打印可以看出,成功调用了父类的实例方法类方法

    <!-- NSObject+LGCate.h -->
    #import <Foundation/Foundation.h>
    NS_ASSUME_NONNULL_BEGIN
    @interface NSObject (LGCate)
    - (void)sayEasy;
    @end
    NS_ASSUME_NONNULL_END
    
    <!-- NSObject+LGCate.m -->
    #import "NSObject+LGCate.h"
    @implementation NSObject (LGCate)
    - (void)sayEasy{
        NSLog(@"%s",__func__);
    }
    @end
    
    <!-- main.m -->
    int main(int argc, const char * argv[]) {
        @autoreleasepool {     
    #pragma clang diagnostic push
    // 让编译器忽略错误
    #pragma clang diagnostic ignored "-Wundeclared-selector"
            [LGStudent performSelector:@selector(sayEasy)];
    #pragma clang diagnostic pop
        }
        return 0;
    }
    
    // 控制台打印
    2021-07-31 10:48:51.810227+0800 002-方法的查找流程[99783:8716929] -[NSObject(LGCate) sayEasy]
    

    疑问? 上面sayEasy是实例方法,为什么类能够调用呢?
    因为在OC的底层是没有类方法、实例方法区分的。获取类方法实际上就是获取元类的实例方法。LGStudent通过isa不断向上查找,最终通过根元类的父类NSObject找到sayEasy方法。

    为什么快速查找缓存要使用汇编编写,而不是用C++编写呢?
    • 汇编更接近机器语言,执行效率快又安全
    • 方法多的话,通过汇编查找缓存的方式可以最大程度的优化执行效率
    • 汇编在执行的时候,参数可以是未知的,这一点区别与C/C++(参数一定是调用前就指定好了)。汇编的这一优点也就体现了,相对于C/C++更加的动态化。
    为什么要执行慢速查找流程,全部使用汇编进行快速查找不是更好吗?
    • 因为在缓存中找不到了,必须进入慢速查找流程
    • 这个时候不断的遍历methodList去查找方法,是一个耗时过程,所以放在C/C++中执行,以提高效率。

    相关文章

      网友评论

          本文标题:消息慢速查找

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