iOS -底层原理慢速查找

65 阅读10分钟

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

1.探索慢速查找

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

2.png 在示例代码[person saySomething1];处设置断点,执行程序。堆栈显示如下:

4.png

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

1.png

流程解析:

流程很长看不懂,流程与objc_msg_arm64.s中的汇编流程是一致的。从一些关键点上还是可以发现一些端倪。例如掩码0x7ffffffffff8,很熟悉,其实就是X86_64环境下的ISA_Mask值。 不难理解,即通过isa指针的掩码运算获取类对象,然后在对象中进行一系列的操作。如果缓存没有命中,进入汇编函数_objc_msg_Send_uncached。同样在此处设置断点,程序运行到该出后,按下ctrl键,点击step into,进入_objc_msg_Send_uncached流程。见下图:

3.png

2.方法查找探索与思考

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

@interface JHSTeacher : NSObject

- (void)saySomething1;

+ (void)saySomething2;

@end

@implementation JHSTeacher

- (void)saySomething1{

    NSLog(@"saySomething1 %s",__func__);

}

+ (void)saySomething2{

    NSLog(@"saySomething2");

}

@end


@interface JHSPerson : JHSTeacher

- (void)saySomething1;

+ (void)saySomething2;

@end

@implementation JHSPerson

@end


int main(int argc, const char * argv[]) {

    @autoreleasepool {

        JHSPerson *person = [JHSPerson alloc];

        JHSTeacher *teach = [JHSTeacher alloc];

        [person saySomething1];

        [JHSPerson saySomething2];
        }
        return 0;
        
   }

运行结果:

5.png 运行结果解析:

  1. 调用对象方法saySomething1,调用类方法saySomething2,均没有报错,均成功的调用了父类JHSTeacher中的实现。
  2. person对象方法存储在JHSPerson类中,person对象isa指向JHSPerson类,JHSPersonsuperclass指向JHSTeacher类对象。
  3. JHSPerson类方法存储在JHSPerson元类中,JHSPersonisa指向JHSPerson元类,JHSPerson元类superclass指向JHSTeacher元类。
  4. JHSPerson类和JHSTeacher类有上面的2层关系,那对象方法saySomething1类方法saySomething2肯定是因为这个关系,从而成功找到方法实现,才会使程序没有报错。

3.慢速查找探索

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

// 慢速查找
// 方法调用 objc_msgSend 
// 1 —— 发送消息objc_msgSend 缓存快速查找(cache_t)
// 2 —— 没有命中,lookUpImpOrForward慢速查找

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.
// Superclass找到父类,在父类的cache中查找.
// 从父类缓存中查找 - 再次进入汇变查找 
// - 如果查找到done
// - 查找不到,循环superclass,再查找父类 

        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;

        }

    }

    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;
  1. 循环开始方法查找,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;
    }

  1. 动态方法决议流程,可以理解为再给一次机会进行补救。
if (slowpath(behavior & LOOKUP_RESOLVER)) {
    behavior ^= LOOKUP_RESOLVER;
    return resolveMethod_locked(inst, sel, cls, behavior);
}

  1. 查找成功,将查找到的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.本类已实现方法

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

6.png

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

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

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

3.父类中有实现

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

9.png

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

10.png

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

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

4.类方法

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

12.png

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

13.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与方法的真正实现在执行阶段才绑定。