iOS底层学习——慢速查找lookUpImpOrForward

1,038 阅读11分钟

在上一篇文章objc_msgSend快速查找中,探究了函数调用的本质,即消息发送:objc_msgSend,并用汇编代码探究了objc_msgSend快速方法查找(即缓存查找)的流程。 在快速方法查找流程中,如果缓存命中则执行相应的方法;如果没有命中MissLabelDynamic,则会进入慢速方法查找流程,即执行C/C++代码中的lookUpImpOrForward方法。

1.探索慢速查找

除通过解读汇编代码外,还可以采用debug的方式来探索慢速方法查找的入口lookUpImpOrForward方法。设置debug显示汇编运行流程,见下图:

debug设置显示堆栈信息

在示例代码[user sayHello8];处设置断点,执行程序。堆栈显示如下:

方法调用本质是发送消息objc_msgSend

方法调用本质是发送消息objc_msgSend。在断点处按下ctrl键,点击step into,进入objc_msgSend流程。

iShot2021-06-30 22.07.56.png

流程解析:

流程很长看不懂,流程与objc_msg_arm64.s中的汇编流程是一致的。从一些关键点上还是可以发现一些端倪。例如掩码0x7ffffffffff8,很熟悉,其实就是X86_64环境下的ISA_Mask值

#   elif __x86_64__
#   define ISA_MASK        0x00007ffffffffff8ULL

不难理解,即通过isa指针的掩码运算获取类对象,然后在对象中进行一系列的操作。如果缓存没有命中,进入汇编函数_objc_msg_Send_uncached。同样在此处设置断点,程序运行到该出后,按下ctrl键,点击step into,进入_objc_msg_Send_uncached流程。见下图:

image.png

找到慢速方法查找入口lookUpImpOrForward at objc-runtime-new.mm:6394

2.方法查找探索与思考

用一个简单的案例,Son继承自FatherSon类只有方法的声明,没有方法的实现;Father类对方法进行了声明并实现了相关方法。

@interface Father : NSObject
-(void)sayHello;
+(void)sayNB;
@end
@implementation Father

-(void)sayHello{
    NSLog(@"sayHello %s", __func__);
}
+(void)sayNB{
    NSLog(@"sayNB %s", __func__);
}
@end

@interface Son : Father
-(void)say666;
-(void)sayHello;
+(void)sayNB;
@end
@implementation Son
-(void)say666{
    NSLog(@"say666 %s", __func__);
}
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Son *son = [[Son alloc] init];
        [son say666];
        [son sayHello];
        [Son sayNB];
    }
    return 0;
}

运行结构见下图:

image.png

运行结果解析:

  1. 调用对象方法say666sayHello,调用类方法sayNB,均没有报错,均成功的调用了父类Father中的实现。
  2. son对象方法存储在Son类中,son对象isa指向Son类,Sonsuperclass指向Father类对象。
  3. Son类方法存储在Son元类中,Sonisa指向Son元类,Son元类superclass指向Father元类。
  4. Son类和Father类有上面的2层关系,那对象方法sayHello类方法sayNB肯定是因为这个关系,从而成功找到方法实现,才会使程序没有报错。

到底是不是这样呢?继续进行源码的探究。

3.慢速查找探索

objc runtime源码libobjc.A.dylib中全局搜索lookUpImpOrForward方法。在objc_runtime_new.mm文件中查找到lookUpImpOrForward的方法实现。代码如下:

// 慢速查找
// 方法调用 objc_msgSend
// 1 —— 发送消息objc_msgSend 缓存快速查找(cache_t)
// 2 —— 没有命中,lookUpImpOrForward慢速查找
NEVER_INLINE
IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior)
{
    //    forward_imp
    const IMP forward_imp = (IMP)_objc_msgForward_impcache;
    IMP imp = nil;
    Class curClass;

    runtimeLock.assertUnlocked();

    if (slowpath(!cls->isInitialized())) {
        behavior |= LOOKUP_NOCACHE;
    }
    
    //    加锁,目的是保证读取的线程安全
    runtimeLock.lock();

    //     是否是已知类:判断当前类是否是已经被认可的类,即已经加载的类
    checkIsKnownClass(cls);

    // 判断类是否实现,如果没有,需要先实现
    cls = realizeAndInitializeIfNeeded_locked(inst, cls, behavior & LOOKUP_INITIALIZE);
    // runtimeLock may have been dropped but is now locked again
    runtimeLock.assertLocked();
    curClass = cls;

    //    递归
    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 {
            // curClass method list.
            // 在当前的类的方法列表中查找方法(采用二分查找算法),如果找到,则返回,将方法缓存到cache中
            Method meth = getMethodNoSuper_nolock(curClass, sel);
            if (meth) {
                imp = meth->imp(false);
                goto done;
            }

            // 未找到,superclass找到父类或者父元类继续查找,如果父类是nil,默认赋值forward_imp
            if (slowpath((curClass = curClass->getSuperclass()) == nil)) {
                // No implementation found, and method resolver didn't help.
                // Use forwarding.
                // 父类为nil,即继承链都未找到方法实现,跳出循环
                imp = forward_imp;
                break;
            }
        }

        // Halt if there is a cycle in the superclass chain.
        if (slowpath(--attempts == 0)) {
            _objc_fatal("Memory corruption in class list.");
        }

        // Superclass找到父类,在父类的cache中查找.
        // 从父类缓存中查找 - 再次进入汇变查找
        // - 如果查找到done
        // - 查找不到,循环superclass,再查找父类
        // Superclass cache.
        imp = cache_getImp(curClass, sel);
        if (slowpath(imp == forward_imp)) {
            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:
    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;
}

1.源码流程分析

  1. 初始化forward_imp

  2. 判断当前类是否是已经被认可的类,即已经加载的类;然后建立类、父类的双向链表关系,把类的继承链确定下来,此时的目的是为了确定父类链,方便后续的循环。

    // 是否是已知类:判断当前类是否是已经被认可的类,即已经加载的类
    checkIsKnownClass(cls);
    
    // 判断类是否实现,如果没有,需要先实现
    cls = realizeAndInitializeIfNeeded_locked(inst, cls, behavior & LOOKUP_INITIALIZE);
    // runtimeLock may have been dropped but is now locked again
    runtimeLock.assertLocked();
    curClass = cls;
    
  3. 循环开始方法查找,for (unsigned attempts = unreasonableClassCount();;),当前curClass,可以是类,也可以是元类,但是最终调用的都是对象方法。

  4. 首先在当前类的方法列表中查找,采用二分查找方式。

          // curClass method list.
          // 在当前的类的方法列表中查找方法(采用二分查找算法),如果找到,则返回,将方法缓存到cache中
          Method meth = getMethodNoSuper_nolock(curClass, sel);
          if (meth) {
              imp = meth->imp(false);
              goto done;
          }
    

    如果方法列表中找到该方法,跳转至done,并将方法插入缓存,也就是cache_t中的buckets中,并返回imp,即步骤8。如果方法列表中没有找到,则进入步骤5

  5. 根据superclass找到父类或者父元类,并赋值给curClass

            // 未找到,superclass找到父类或者父元类继续查找,如果父类是nil,默认赋值forward_imp
            if (slowpath((curClass = curClass->getSuperclass()) == nil)) {
                // No implementation found, and method resolver didn't help.
                // Use forwarding.
                // 父类为nil,即继承链都未找到方法实现,跳出循环
                imp = forward_imp;
                break;
            }
    

    如果父类是nil,说明已经找到NSObject类了,默认赋值forward_imp,跳出循环,进入步骤7。如果父类不是nil,进入步骤6

  6. 再对当前类进行缓存查找(汇编流程),如果缓存依然未找到,imp = nil,继续循环,进入步骤3。如果找到imp,跳转至done,并将方法插入缓存,也就是cache_t中的buckets中,即步骤8

        // Superclass找到父类,在父类的cache中查找.
        // 从父类缓存中查找 - 再次进入汇变查找
        // - 如果查找到done
        // - 查找不到,循环superclass,再查找父类
        // Superclass cache.
        imp = cache_getImp(curClass, sel);
        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;
        }
    
  7. 动态方法决议流程,可以理解为再给一次机会进行补救。

    if (slowpath(behavior & LOOKUP_RESOLVER)) {
        behavior ^= LOOKUP_RESOLVER;
        return resolveMethod_locked(inst, sel, cls, behavior);
    }
    
  8. 查找成功,将查找到的imp sel插入缓存。也就是cache_t中的buckets中。为下次直接从缓存里面快速查找做准备。

     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);
        }
    
  9. 返回imp,流程结束。

总结: 源码实现确实验证了在第二部分的猜想,确实是通过类的superclass一路向上查找方法。

源码很好理解,和我们设想的也一致,但是需要调试跟踪验证一下!依然是上面的案例进行跟踪调试!

2.本类已实现方法

对象方法say666Son类中有实现,运行代码,跟踪到慢速方法查找流程中,在通过二分查找,从方法列表中,成功获取了对应的对象方法。此时的curClass也就是传入的cls,即Son类,还没有通过superclass查找父类。在见下图:

image.png

在方法列表中查找到方法后,跳转至done。将方法插入到方法缓存,也就是cache_t中,形成闭环!

image.png

最终会调用cache::insert方法,形成了闭环!下次再调用相同的方法时,就会直接从缓存中进行快速方法查找。也就是上一篇文章快速方法查找中探究的内容。

image.png

如果此过程中发生过cache_t的扩容,则还需要重复上面的流程,快速方法查找,查找不到,慢速方法查找,插入缓存!

3.父类中有实现

对象方法sayHelloSon类中没有实现,父类Father中有实现。运行代码,跟踪到慢速方法查找流程中,二分查找从方法列表中查找方法,方法返回为空。在见下图:

image.png

继续跟踪源码,Son通过superclass找到了父类Father,此时的curClass设置为Father,通过cache_getImp从父类的缓存中查找对应的方法,没有找到imp返回为空。见下图:

image.png

父类的缓存中没有找到方法后,继续for循环,从当前类curClass也就是Father类的方法列表中查找,成功找到了方法。见下图:

image.png

方法查找到方法后,跳转至done。将方法插入到方法缓存,也就是cache_t中。

4.类方法

调用sayNB类方法,进入慢速方法查找流程,此时curClassSon元类,在元类中没有找到对应meth。见下图:

image.png

Son元类中没有找到对应的方法,通过superclass找到Father元类,在父元类中找到了对应的方法实现。

image.png

方法查找到方法后,跳转至done。将方法插入到方法缓存,也就是cache_t中。同时,通过该流程可以说明,在底层没有类方法,全部是对象方法。类就是元类的对象!

5.cache_getImp分析

cache_getImp从方法缓存中查找方法实现,cache_getImp的具体流程是怎么样的呢?在objc_msg_arm64.s中找到了汇编实现,见下面源码:

	STATIC_ENTRY _cache_getImp

	GetClassFromIsa_p16 p0, 0
	CacheLookup GETIMP, _cache_getImp, LGetImpMissDynamic, LGetImpMissConstant

LGetImpMissDynamic:
	mov	p0, #0
	ret

LGetImpMissConstant:
	mov	p0, p2
	ret

	END_ENTRY _cache_getImp

最终也是调用了宏CacheLookup,不过参数有所不同,如果没有找到对应的缓存方法,则会LGetImpMissDynamic,也就是返回空0x0,而不是进行慢速方法查找!

4.详解二分查找

二分查找也称折半查找(Binary Search),它是一种效率较高的查找方法。但是,折半查找要求线性表必须采用顺序存储结构,而且表中元素按关键字有序排列。源码如下:

/**********************************************************************
 * 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);

    auto first = list->begin();
    auto base = first;
    decltype(first) probe;

    uintptr_t keyValue = (uintptr_t)key;
    uint32_t count;
    // base相当于low地址,count是max地址,probe是middle地址
    for (count = list->count; count != 0; count >>= 1) {
        // 指针平移至中间位置
        // 从首地址 + 下标 --> 移动到中间位置(count >> 1)
        probe = base + (count >> 1);
        // 获取该位置的sel名称
        uintptr_t probeValue = (uintptr_t)getName(probe);
        // 如果查找的key的keyvalue等于中间位置(probe)的probeValue,则直接返回中间位置
        if (keyValue == probeValue) {
            // `probe` is a match.
            // Rewind looking for the *first* occurrence of this value.
            // This is required for correct category overrides.
            // 分类方法同名- while 平移 -- 向前在查找,判断是否存在相同的方法,保证调用的是分类的
            while (probe > first && keyValue == (uintptr_t)getName((probe - 1))) {
                probe--;
            }
            return &*probe;
        }
        // 如果keyValue 大于 probeValue,就往probe即中间位置的右边查找,即中间位置再右移
        if (keyValue > probeValue) {
            base = probe + 1;
            count--;
        }
    }
    
    return nil;
}

案例说明:

method_list_t看成[0x10,0x20,0x30,0x40,0x50,0x60,0x70,0x80]数量为8的有序数组。

  1. 现在需要查找方法0x30,流程如下:

    • 初始值:base = 0x10,count = 8;
    • 循环一:probe = 0x50,未找到,而0x50大于0x30;左侧查找;
    • 循环二:base = 0x10,count = 4;probe = 0x30,找到!
  2. 查找方法0x60,流程如下:

    • 初始值:base = 0x10,count = 8;
    • 循环一:probe = 0x50,未找到,而0x50小于0x60;右侧查找;
    • 循环二:base = 0x60,count = 3;probe = 0x70,未找到;
    • 循环三:base = 0x60,count = 1;probe = 0x60,找到;

5.方法查找的理解

每个对象都有一个指向所属类的指针isa。通过该指针,对象可以找到它所属的类,也就找到了其全部父类。

当向一个对象发送消息时,objc_msgSend方法根据对象的isa指针找到对象的类,然后在类的调度表(dispatch table)中查找selector。如果无法找到selectorobjc_msgSend通过指向父类的指针找到父类,并在父类的调度表(dispatch table)中查找selector,以此类推直到NSObject类。一旦查找到selectorobjc_msgSend方法根据调度表的内存地址调用该实现。通过这种方式,message与方法的真正实现在执行阶段才绑定。

为了保证消息发送与执行的效率,系统会将全部selector和使用过的方法的内存地址缓存起来。每个类都有一个独立的缓存,缓存包含有当前类自己的selector以及继承自父类的selector。查找调度表(dispatch table)前,消息发送系统首先检查receiver对象的缓存。