ios 底层原理之方法慢速查找流程

331 阅读9分钟

前言

在上一篇底层原理之Runimte 运行时我们探索了objc_msgSend的方法查找流程。objc_msgSend底层是用汇编写的快速高效查找缓存的方法,我们称之为快速查找,通过SEL查找IMP,如果命中则返回IMP,如果没有命中就会进入objc_msgSend_uncached,下面就探索一下在快速查找找不到的情况下会怎么处理。附oc源码

1.0 objc_msgSend_uncached

接着上一篇底层原理之Runimte 运行时如果在cache_t中循环查找找不到IMP时会进入objc_msgSend_uncached

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
MethodTableLookup

TailCallFunctionPointer x17

END_ENTRY __objc_msgSend_uncached

分析:objc_msgSend_uncached汇编就两行,MethodTableLookupTailCallFunctionPointer x17,我们具体看下。

1.1 MethodTableLookup

.macro MethodTableLookup
SAVE_REGS MSGSEND
//x0和x1 是receiver和sel,上一篇分析得出x16=p16=class
//x2=x16=class
mov x2, x16
mov x3, #3  //x3=3
// lookUpImpOrForward(obj, sel, cls, LOOKUP_INITIALIZE | LOOKUP_RESOLVER)
bl _lookUpImpOrForward //b跳转指令,lr寄存处,bl意思:执行_lookUpImpOrForward,并把一下一行指令mov x17, x0放到lr寄存器中
// IMP in x0
mov x17, x0  //x0为执行_lookUpImpOrForward返回值IMP
RESTORE_REGS MSGSEND
.endmacro

分析:方法查找的默认两个参数x0=reservier,x1=sel,上述汇编把x2=class,x3=3,然后执行方法_lookUpImpOrForward,x0、x1、x2、x3是_lookUpImpOrForward方法的形参。lr寄存器存入下一行指令mov x17, x0,那么lookUpImpOrForward方法执行完会执行mov x17, x0,此时的x0是lookUpImpOrForward返回值,我们研究此函数的意义就是查找IMP,所以猜测返回值应该是IMP,即x17=x0=IMP

1.2 TailCallFunctionPointer

.macro TailCallFunctionPointer
// $0 =x17=IMP
br $0    
.endmacro

分析:MethodTableLookup执行完成后x17=IMP,TailCallFunctionPointer就是执行IMP方法实现。

2.0 lookUpImpOrForward

lookUpImpOrForwardobjc_msgSend_uncached的关键,因为它返回的是IMP,全局搜索lookUpImpOrForward,发现lookUpImpOrForward已不再是汇编代码。我们先总体看一下具体实现,然后再详细分析。

//根据上面分析传入的参数x0=reservier、x1=sel、x2=class、x3=3 
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();

    if (slowpath(!cls->isInitialized())) {
        // 发送到类的第一条消息通常是 +new 或 +alloc 或 +self
        // 它通过 objc_opt_* 或各种优化的入口点。
        // 但是此时该类尚未通过realized/initialized 实现或初始化,
        // 我们真的想避免缓存这些,因为它会导致 IMP 缓存永远只用一个条目。
        //请注意,此检查很活跃,因为多个线程可能会同时第一次尝试向给定类发送消息,在这种情况下,我们可能会进行缓存
        //behavior=behavior|LOOKUP_NOCACHE= 3|8= 0011|1000=1011=11
        behavior |= LOOKUP_NOCACHE;
    }

    // runtimeLock 在 isRealized 和 isInitialized 检查期间保持,以防止与并发实现竞争。
    //在方法搜索期间持有 runtimeLock 以使方法查找 + 缓存填充相对于方法添加具     有原子性。 
    //否则,可能会添加一个类别,但会被无限期地忽略,因为在代表该类别的缓存刷新后,缓存被重新填充了旧值。
    runtimeLock.lock();
    // 我们要确保这是一个内置在二进制文件中或通过 objc_duplicateClass、     objc_initializeClassPair 或 objc_allocateClassPair 合法注册的类。
    //即被dyld加载的类
    checkIsKnownClass(cls);
    //初始化类、父类、元类,加载方法等   3&1=0011&0001=0001=true
    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)) {
          /*
          查询共享缓存中有没有该方法,缓存中根据sel查询imp
          */
           #if CONFIG_USE_PREOPT_CACHES //真机环境下会走
              imp = cache_getImp(curClass, sel);
              //如果缓存中有就进入done_unlock
              if (imp) goto done_unlock;
              curClass = curClass->cache.preoptFallbackClass();
           #endif
        } else {
            // curClass中采用二分法查找对应的Method
            Method meth = getMethodNoSuper_nolock(curClass, sel);
            if (meth) {//找到method 执行done
                imp = meth->imp(false);
                goto done;
            }
            //curClass赋值为父类,如果为父类为空 imp=forward_imp 跳出循环
            if (slowpath((curClass = curClass->getSuperclass()) == nil)) {
                imp = forward_imp;
                break;
            }
        }
        // 如果父类中有循环,则停止。
        if (slowpath(--attempts == 0)) {
            _objc_fatal("Memory corruption in class list.");
        }
        // 此时curClass = curClass->getSuperclass(),在父类缓存中查找
        imp = cache_getImp(curClass, sel);
        if (slowpath(imp == forward_imp)) {
            //如果父类中找到的是forward_imp 就停止查找,跳出循环
            break;
        }
        //父类中找到Imp 进入done
        if (fastpath(imp)) {
            goto done;
        }
    }
    // 如果查询方法的没有实现,系统会尝试一次动态方法解析
    //第一次 behavior & LOOKUP_RESOLVER =3 & 2=0011 & 0010=0010=2
    //第二次进入下面behavior=1,behavior & LOOKUP_RESOLVER=1&2=0001&0010=0 不会进入动态方法解析
    if (slowpath(behavior & LOOKUP_RESOLVER)) {
        //behavior= behavior ^= LOOKUP_RESOLVER=3^2=0011^0010=0001=1
        behavior ^= LOOKUP_RESOLVER;
        //动态方法解析
        return resolveMethod_locked(inst, sel, cls, behavior);
    }
 done:
   //behavior & LOOKUP_NOCACHE=3&8=0011&1000=0执行
   //alloc或init时behavior=11,即 1011 & 1000=1000!=0,所以不会插入缓存
    if (fastpath((behavior & LOOKUP_NOCACHE) == 0)) {
      #if CONFIG_USE_PREOPT_CACHES //真机下
        while (cls->cache.isConstantOptimizedCache(/* strict */true))         {
            cls = cls->cache.preoptFallbackClass();
        }
     #endif
        //把sel和Imp插入cls中的cache_t
        log_and_fill_cache(cls, imp, sel, inst, curClass);
    }
 done_unlock:
    runtimeLock.unlock();
    //如果没有查询到返回nil
    if (slowpath((behavior & LOOKUP_NIL) && imp == forward_imp)) {
        return nil;
    }
    return imp;
}

分析:整体分析一下lookUpImpOrForward慢速查找流程,慢速是区别于汇编快速查找,此过程是不断的在本类和父类的缓存中查找方法。方法的作用已经注释,注释很重要。

  1. 初始化类、父类、元类,初始化方法、属性、协议,一般是alloc或init才会调用
  2. 判断是否有共享缓存,目的是有可能在查过过程中这个方法被调用缓存了,如果有的话直接从缓存中取,没有共享缓存则开始到本类中查询
  3. 在本类中二分查找sel对应的IMP,如果找到IMP那么插入本类的cache_t
  4. 本类没有找到IMP就在父类的缓存中循环查找,找到就插入到本类的cache_t中,否则跳出循环执行消息转发
  5. 如果本类和父类都没有查询到,此时系统会给你一次机会,判断是否执行过动态方法决议,如果没有则走动态方法决议。 注意:alloc和init不会插入cache_t

根据上面的分析详细分析一下下面几个函数

2.1 realizeAndInitializeIfNeeded_locked

static Class
realizeAndInitializeIfNeeded_locked(id inst, Class cls, bool initialize)
{
    runtimeLock.assertLocked();
    //slowpath和fastpath的意思是发生的概率小和发生的概率大,去掉这个也没影响,只是告诉编译器,让编译器优化
    //判断类是否实现
    if (slowpath(!cls->isRealized())) {
       //跟进realizeClassMaybeSwiftAndLeaveLocked实现,其实是实现了  realizeClassWithoutSwift 初始化了类、父类、元类
        cls = realizeClassMaybeSwiftAndLeaveLocked(cls, runtimeLock);
    }
    //把类关联进表
    if (slowpath(initialize && !cls->isInitialized())) {
        cls = initializeAndLeaveLocked(cls, inst, runtimeLock);
        // 如果 sel == initialize,class_initialize 将发送 +initialize,然后在此过程完成后,messenger 将再次发送 +initialize
    }
    return cls;

}

分析:

  • realizeClassMaybeSwiftAndLeaveLocked中realizeClassWithoutSwift初始化了类、父类、元类的ro、rw,以及初始化了方法、属性、协议。
  • initializeAndLeaveLocked中initializeNonMetaClass有个函数remapClass主要作用是把类关联符号表

2.2 realizeClassWithoutSwift

static Class realizeClassWithoutSwift(Class cls, Class previously)
{
    runtimeLock.assertLocked();
    class_rw_t *rw;
    Class supercls;
    Class metacls;
    if (!cls) return nil;

    if (cls->isRealized()) {
        validateAlreadyRealizedClass(cls);
        return cls;
    }
    ASSERT(cls == remapClass(cls));
    auto ro = (const class_ro_t *)cls->data();
    auto isMeta = ro->flags & RO_META;
    //初始化ro rw
    if (ro->flags & RO_FUTURE) {
        rw = cls->data();
        ro = cls->data()->ro();
        ASSERT(!isMeta);

        cls->changeInfo(RW_REALIZED|RW_REALIZING, RW_FUTURE);
    } else {
        rw = objc::zalloc<class_rw_t>();
        rw->set_ro(ro);
        rw->flags = RW_REALIZED|RW_REALIZING|isMeta;
        cls->setData(rw);
    }
    cls->cache.initializeToEmptyOrPreoptimizedInDisguise();
#if FAST_CACHE_META
    if (isMeta) cls->cache.setBit(FAST_CACHE_META);
#endif
    cls->chooseClassArrayIndex();
    if (PrintConnecting) {
        _objc_inform("CLASS: realizing class '%s'%s %p %p #%u %s%s",
                     cls->nameForLogging(), isMeta ? " (meta)" : "", 
                     (void*)cls, ro, cls->classArrayIndex(),
                     cls->isSwiftStable() ? "(swift)" : "",

                     cls->isSwiftLegacy() ? "(pre-stable swift)" : "");
    }
     //初始化父类ro、rw,这是一个循环父类的父类
    supercls = realizeClassWithoutSwift(remapClass(cls->getSuperclass()), nil);
    //初始化元类ro、rw,这是一个循环,元类的元类
    metacls = realizeClassWithoutSwift(remapClass(cls->ISA()), nil);
#if SUPPORT_NONPOINTER_ISA
    if (isMeta) {
        cls->setInstancesRequireRawIsa();
    } else {
        if (DisableNonpointerIsa) {
            instancesRequireRawIsa = true;
        }
        else if (!hackedDispatch  &&  0 == strcmp(ro->getName(), "OS_object"))
        {
            hackedDispatch = true;
            instancesRequireRawIsa = true;
        }

        else if (supercls  &&  supercls->getSuperclass()  &&
                 supercls->instancesRequireRawIsa())
        {
            instancesRequireRawIsa = true;
            rawIsaIsInherited = true;
        }
        if (instancesRequireRawIsa) {
            cls->setInstancesRequireRawIsaRecursively(rawIsaIsInherited);
        }
    }

// SUPPORT_NONPOINTER_ISA
#endif
    //关联父类以及元类
    cls->setSuperclass(supercls);
    cls->initClassIsa(metacls);
    if (supercls  &&  !isMeta) reconcileInstanceVariables(cls, supercls, ro);
    cls->setInstanceSize(ro->instanceSize);
    if (ro->flags & RO_HAS_CXX_STRUCTORS) {
        cls->setHasCxxDtor();
        if (! (ro->flags & RO_HAS_CXX_DTOR_ONLY)) {
            cls->setHasCxxCtor();
        }
    }
    if ((ro->flags & RO_FORBIDS_ASSOCIATED_OBJECTS) ||
        (supercls && supercls->forbidsAssociatedObjects()))
    {
        rw->flags |= RW_FORBIDS_ASSOCIATED_OBJECTS;

    }
    if (supercls) {
        addSubclass(supercls, cls);
    } else {
        addRootClass(cls);
    }
    //初始化属性、方法、协议,以及分类的方法
    methodizeClass(cls, previously);
    return cls;
}

分析:realizeClassWithoutSwift主要作用如下

  • rw,ro初始化:类、父类、元类,根据isa走位图都会初始化
  • 关联上父类以及元类
  • 初始化属性、方法、协议,以及分类的方法

2.2 getMethodNoSuper_nolock

static method_t *
getMethodNoSuper_nolock(Class cls, SEL sel)
{
    runtimeLock.assertLocked();
    ASSERT(cls->isRealized());
    auto const methods = cls->data()->methods();
    for (auto mlists = methods.beginLists(),
              end = methods.endLists();
         mlists != end;
         ++mlists)
    {
        // search_method_list 的调用者,将其内联将 getMethodNoSuper_nolock 转换为无框架函数,并从此代码路径中消除任何存储。
        method_t *m = search_method_list_inline(*mlists, sel);
        if (m) return m;
    }
    return nil;
}

分析:getMethodNoSuper_nolock主要是获取类中所有方法然后把方法列表和要查找的sel放入函数search_method_list_inline中,跟进此方法,发现极有可能调用了排序方法findMethodInSortedMethodList,这个方法才是最终返回SEL对应IMP的方法。

2.3 findMethodInSortedMethodList

static method_t *
findMethodInSortedMethodList(SEL key, const method_list_t *list, const getNameFunc &getName)
{
    ASSERT(list);
    auto first = list->begin(); //第一个method位置,首地址
    auto base = first;
    decltype(first) probe;
    //把key直接转换成uintptr_t 因为修复过后的method_list_t中的元素是排过序的
    uintptr_t keyValue = (uintptr_t)key;

    uint32_t count;
    //count >>= 1 count右移1位相当于count/2取整,如8>>1=4,7>>1=3
    //假设count=8,keyvalue=7
    //第一次 count=8 base=0 probe=base+4
    //第二次 count=count>>1=7>>1=3 probe=base+5+(count>>1)=base+5+1
    for (count = list->count; count != 0; count >>= 1) {
        probe = base + (count >> 1);
        uintptr_t probeValue = (uintptr_t)getName(probe);
        if (keyValue == probeValue) {
            //分类覆盖,分类中有相同名字的方法,如果有分类的方法我们就获取分类的方法
            while (probe > first && keyValue == (uintptr_t)getName((probe - 1))) {
               probe--;

            }
            return &*probe;
        }
        if (keyValue > probeValue) {
            //第一次 base=probe+1=base+4+1=base+5;
            //第二次 base=base+5+1+1=base+7
            base = probe + 1;
            count--;
        }
    }
    return nil;
}

分析:

  • 方法列表中sel大小是进过排序的二分法循环查找方法列表中SEL与目标SEL是否相等
  • 二分查找就是每次取中间值probeValue跟keyvalue进行比较,相同就返回,不相同继续二分查找

2.4 二分查找示例图

未命名文件.png 假设方法列表count为8,用伪代码实现一下二分法查找IMP流程

    int keyValue=1;
    int first=0;
    int base=first;
    int count;
    int probe=0;
    //假设count=8
    for(count=8;count!=0;count=count/2){
        probe=base+(count/2);
        if(keyValue==probe){
          while(probe >first && keyValue ==(probe - 1)){
              probe--;
              }
              return probe;
        }
        if(keyValue>probe){
            base=probe+1;
            count--;
        }
    }

总结

  • 方法的本质是消息发送objc_msgSend从缓存中通过SEL快速查找IMP
  • 缓存中快速查找不到,会进入lookUpImpOrForward慢速查找,在本类中二分法查找,如果找不到就在父类的缓存中循环查找。
  • 如果本类和父类都没有查询到,此时系统会给你一次机会,判断是否执行过动态方法决议,如果没有则走动态方法决议。