iOS 底层原理探索 之 Runtime运行时慢速查找流程

938 阅读6分钟
写在前面: iOS底层原理探究是本人在平时的开发和学习中不断积累的一段进阶之
路的。 记录我的不断探索之旅,希望能有帮助到各位读者朋友。

目录如下:

  1. iOS 底层原理探索 之 alloc
  2. iOS 底层原理探索 之 结构体内存对齐
  3. iOS 底层原理探索 之 对象的本质 & isa的底层实现
  4. iOS 底层原理探索 之 isa - 类的底层原理结构(上)
  5. iOS 底层原理探索 之 isa - 类的底层原理结构(中)
  6. iOS 底层原理探索 之 isa - 类的底层原理结构(下)
  7. iOS 底层原理探索 之 Runtime运行时&方法的本质
  8. iOS 底层原理探索 之 objc_msgSend

以上内容的总结专栏


细枝末节整理


前言

上一篇,我们探索了objc_msgSend的整个流程。有一个疑问,为什么要用汇编写objc_msgSend呢?因为,汇编执行的整个流程的速度更接近于机器语言,速度飞,而且安全,缓存机制无非就是要实现效率高一点速度快一点,并且可以更加的动态化。那么,为什么又会回到c++呢?因为缓存中没有找到,就会进入到慢速查找流程,不断的遍历methodList的一个过程。这会很耗时。最终会bl _lookUpImpOrForward,这一步其实是从汇编跳转到了c++。那么,今天就 继续 详细看下 慢速查找流程 IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior)

IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior)

lookUpImpOrForward根据sel查询imp,具体是怎么查询imp下面探究下

IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior)
{

    //定义消息转发forward_imp  //behavior传入的是 3  = LOOKUP_INITIALIZE|LOOKUP_RESOLVER
    const IMP forward_imp = (IMP)_objc_msgForward_impcache;
    IMP imp = nil;
    Class curClass;

    runtimeLock.assertUnlocked();

    if (slowpath(!cls->isInitialized())) {
        //判断类是否初始化 没有初始化 behavior = LOOKUP_NOCACHE|LOOKUP_INITIALIZE|LOOKUP_RESOLVER
        //发送到类的第一个消息通常是+new或+alloc或+self
        behavior |= LOOKUP_NOCACHE;
    }

    //防止并发实现的竞赛。
    runtimeLock.lock();

    
        
    // 检查是否为注册类 被dyld加载 或者是通过合法注册的类
    checkIsKnownClass(cls);
    
    //实现类包括实现isa走位中的父类和元类 //初始化类和父类
    cls = realizeAndInitializeIfNeeded_locked(inst, cls, behavior & LOOKUP_INITIALIZE);

    runtimeLock.assertLocked();
    curClass = cls;

    //之后用于再次查找类缓存的代码 我们采取锁,
    //但在绝大多数情况下 证据表明,这在大多数情况下是一种缺失,因此是一种时间损失。
    //唯一的代码路径调用这个没有执行一些 类缓存查找是class_getInstanceMethod()。
    for (unsigned attempts = unreasonableClassCount();;) {
        
        //判断是否有共享缓存缓存优化,一般是系统的方法比如NSLog,一般的方法不会走
        if (curClass->cache.isConstantOptimizedCache(/* strict */true)) {
#if CONFIG_USE_PREOPT_CACHES
            /*
            再一次查询共享缓存,目的可能在你查询过程中
            别的线程可能调用了这个方法共享缓存中有了直接去查询
            */
            imp = cache_getImp(curClass, sel);
            
            //如果imp存在即缓存中有 跳转到done_unlock流程
            if (imp) goto done_unlock;
            curClass = curClass->cache.preoptFallbackClass();
#endif
        } else {
            // curClass method list.
            // 在curClass类中采用二分查找算法查找methodlist
            Method meth = getMethodNoSuper_nolock(curClass, sel);
            if (meth) {                 // 如果找到了sel对应的方法
                imp = meth->imp(false); //获取对应的imp
                goto done;              //跳转到 done 流程
            }
          // curClass = curClass->getSuperclass() 直到为nil走if里面的流程,不为nil走下面流程
          // 没有找到后开始找 curClass的父类是否有实现,一直知道 父类为 nil
            if (slowpath((curClass = curClass->getSuperclass()) == nil)) {
                //就是在循环里面没有找到对应的sel的方法,把定义息转发forward_imp赋值给imp
                imp = forward_imp;
                break;
            }
        }

        // 如果父类的链中存在一个循环 则 停止
        if (slowpath(--attempts == 0)) {
            _objc_fatal("Memory corruption in class list.");
        }

        // 先去父类的缓存中昂查找
        imp = cache_getImp(curClass, sel);
        if (slowpath(imp == forward_imp)) {
            // 在超类中发现了forward::条目。 
            // 停止搜索,但不要缓存;调用方法 这个类的解析器。
            // 调用方法 这个类的解析器。
            break;
        }
        if (fastpath(imp)) {
            //  在超类中找到该方法。在这个类中缓存它
            goto done;
        }
    }

    //  没有实现。尝试一次方法解析器。

    if (slowpath(behavior & LOOKUP_RESOLVER)) {
        behavior ^= LOOKUP_RESOLVER;
        //动态方法决议
        return resolveMethod_locked(inst, sel, cls, behavior);
    }

 done:
    if (fastpath((behavior & LOOKUP_NOCACHE) == 0)) {
#if CONFIG_USE_PREOPT_CACHES
        while (cls->cache.isConstantOptimizedCache(/* strict */true)) {
            cls = cls->cache.preoptFallbackClass();
        }
#endif
        //将查询到的sel和imp插入到缓存 注意:插入的是当前类的缓存
        log_and_fill_cache(cls, imp, sel, inst, curClass);
    }
 done_unlock:
    runtimeLock.unlock();
    /*
    如果 (behavior & LOOKUP_NIL)成立则 behavior != LOOKUP_NIL
    且imp == forward_imp 没有查询到直接返回 nil
    */
    if (slowpath((behavior & LOOKUP_NIL) && imp == forward_imp)) {
        return nil;
    }
    return imp;
}

慢速查找流程

  • 检查是否为注册类,如果不是的话,直接报错。
  • 如果还没有初始化,则实现给定的类,如果还没有初始化,则初始化它。Inst是CLS的一个实例或一个子类,如果不知道则为nil。CLS是要初始化和实现的类。初始化器为true来初始化类,为false来跳过初始化。

cls 查询

  • 判断共享缓存中是否存在,因为有可能在查找的过程中,这个方法被调用 而 缓存过,
  • 在类中使用 二分查找算法,查找methodlist,如果找到则插入到缓存中去,循环结束

父类的缓存中查询

  • 如果父类链中存在循环,则终止查询,跳出循环
  • 将superclass赋值给curClass,在父类中进行查找,如果父类中返回 forward_imp 则 跳出遍历,进行消息转发。
  • 如果父类中没有找到,则 再次将 superclass赋值给curClass 再在父类中进行查找,知道 curClass == nil, imp = forward_imp 进行消息转发

动态方法决议

  • 如果cls 和 父类链中 都没有查找到 imp, 这个时候,系统会给一次机会,判断是否进行过动态方法决议,如果没有则走动态方法决议, 已经执行过了则进行消息转发。

lookUpImpOrForward 流程图

慢速查找流程.001.jpeg

二分查找算法

/***********************************************************************
 * search_method_list_inline
 **********************************************************************/
template<class getNameFunc>
ALWAYS_INLINE static method_t *
findMethodInSortedMethodList(SEL key, const method_list_t *list, const getNameFunc &getName)
{
    ASSERT(list);

    //第一个method的位置
    auto first = list->begin();
    auto base = first;
    decltype(first) probe; //相当于mid
    //把key直接转换成uintptr_t 因为修复过后的method_list_t中的元素是排过序的
    uintptr_t keyValue = (uintptr_t)key;
    uint32_t count;
    // count = 数组个数  count >>= 1 = count = count >> 1)
    // count >> 1 就相当于 (count / 2) 取整
    // 1.假如 count = list->count = 8   //2 count =  7 >> 1 = 3
    for (count = list->count; count != 0; count >>= 1) {
        
        /*
          1. 首地址 + (下标) //地址偏移 中间值 probe = base + 4
          2. 中间值  probe = base(首地址)+ 6
         */
        probe = base + (count >> 1);
        
        //获取中间的sel的值也是强转后的值
        uintptr_t probeValue = (uintptr_t)getName(probe);
        
        if (keyValue == probeValue) { // 如果 目标key ==  中间位置的key 匹配成功
            
            //分类覆盖,分类中有相同名字的方法,如果有分类的方法我们就获取分类的方法,多个分类看编译的顺序
            while (probe > first && keyValue == (uintptr_t)getName((probe - 1))) {
                probe--;
            }
            
            //返回方法的地址
            return &*probe;
        }
        
        //如果 keyValue > 中间的位置的值
        if (keyValue > probeValue) {
            
            /*
               1.base = probe + 1 =  4 + 1 = base(首地址) + 5 向上移 一位
               2.base = probe + 1 ;向上移 一位
             */
            base = probe + 1;
            
            // 8 -1 = 7 因为比过一次没中 然后循环
            count--;//查询完没找到返回nil
        }
    }
    
    return nil;
}

以上代码就是二分查找法的代码,真的很🐂🍺,那么,代码看的不够直观,我们尝试用几张图来阐述一下上面的过程,让大家一起来领略下 二分查找法的魅力

二分查找法.001.jpeg

总结

方法的查找流程实际是一个很复杂的流程。现在我们也只是探讨了快速汇编超找流程和慢速超找流程,后面还有动态方法决议,以及消息转发流程,大家加油!