OC底层原理07:消息流程分析之慢速查找过程

831 阅读5分钟

慢速查找

当objc_msgSend在缓存cache中找不到方法时,即源码中执行CheckMissJumpMiss,就会进行慢速查找。

CheckMiss 为loop循环中,bucket->sel未命中时调用的方法。JumpMiss为结束递归后,仍旧查找到了第一个bucket时,进行调用的方法。

CheckMissJumpMiss源码中,都会因NORMAL而进行__objc_msgSend_uncached的操作。

查找__objc_msgSend_uncached实现:

继续查找MethodTableLookup

当查询到_lookUpImpOrForward时,就继续搜索不到了。是因为从底层汇编往上层C/C++的代码进行调用实现,即进行慢速查找过程。

快速查找是在缓存中进行方法的查找;慢速查找是从类信息data()中,及其父类链中进行方法的查找。


lookUpImpOrForward解析

全局搜一下lookUpImpOrForward

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();

    // Optimistic cache lookup
    if (fastpath(behavior & LOOKUP_CACHE)) {
        imp = cache_getImp(cls, sel);
        if (imp) goto done_nolock;
    }

    // runtimeLock is held during isRealized and isInitialized checking
    // to prevent races against concurrent realization.

    // runtimeLock is held during method search to make
    // method-lookup + cache-fill atomic with respect to method addition.
    // Otherwise, a category could be added but ignored indefinitely because
    // the cache was re-filled with the old value after the cache flush on
    // behalf of the category.

    runtimeLock.lock();

    // We don't want people to be able to craft a binary blob that looks like
    // a class but really isn't one and do a CFI attack.
    //
    // To make these harder we want to make sure this is a class that was
    // either built into the binary or legitimately registered through
    // objc_duplicateClass, objc_initializeClassPair or objc_allocateClassPair.
    //
    // TODO: this check is quite costly during process startup.
    checkIsKnownClass(cls);

    if (slowpath(!cls->isRealized())) {
        // 如果当前类没有实现,先要确认类及其父类链和方法属性等
        cls = realizeClassMaybeSwiftAndLeaveLocked(cls, runtimeLock);
        // runtimeLock may have been dropped but is now locked again
    }

    if (slowpath((behavior & LOOKUP_INITIALIZE) && !cls->isInitialized())) {
        cls = initializeAndLeaveLocked(cls, inst, runtimeLock);
        // runtimeLock may have been dropped but is now locked again

        // If sel == initialize, class_initialize will send +initialize and 
        // then the messenger will send +initialize again after this 
        // procedure finishes. Of course, if this is not being called 
        // from the messenger then it won't happen. 2778172
    }

    runtimeLock.assertLocked();
    curClass = cls;

    // The code used to lookpu the class's cache again right after
    // we take the lock but for the vast majority of the cases
    // evidence shows this is a miss most of the time, hence a time loss.
    //
    // The only codepath calling into this without having performed some
    // kind of cache lookup is class_getInstanceMethod().

    for (unsigned attempts = unreasonableClassCount();;) {
        // curClass method list.
        Method meth = getMethodNoSuper_nolock(curClass, sel);
        if (meth) {
            imp = meth->imp;
            goto done;
        }

        if (slowpath((curClass = curClass->superclass) == nil)) {
            // No implementation found, and method resolver didn't help.
            // Use forwarding.
            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.
        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;
        }
    }

    // No implementation found. Try method resolver once.

    if (slowpath(behavior & LOOKUP_RESOLVER)) {
        behavior ^= LOOKUP_RESOLVER;
        return resolveMethod_locked(inst, sel, cls, behavior);
    }

 done:
    log_and_fill_cache(cls, imp, sel, inst, curClass);
    runtimeLock.unlock();
 done_nolock:
    if (slowpath((behavior & LOOKUP_NIL) && imp == forward_imp)) {
        return nil;
    }
    return imp;
}

1.
if (fastpath(behavior & LOOKUP_CACHE)) {
    imp = cache_getImp(cls, sel);
    if (imp) goto done_nolock;
}

该判断,是为了防止在多线程操作时,刚好调用到lookUpImpOrForward时,缓存中insert了要查找的方法,那么可以直接从缓存获取imp,并goto done_nolock,毕竟慢速查找是个耗时的过程,苹果尽量不想太过耗时。


2.
runtimeLock.lock();

加锁,从注释也可以看到,是为了保证操作时的线程安全问题。


3.
checkIsKnownClass(cls);

确保传入的类是合法注册的类,是否已经加载过。


4.
if (slowpath(!cls->isRealized())) {
    cls = realizeClassMaybeSwiftAndLeaveLocked(cls, runtimeLock);
}

如果当前类没有实现,如果没有,则要实现并确认其父类链和方法属性等。

而在realizeClassWithoutSwift方法中,会确认当前类的父类链

...
supercls = realizeClassWithoutSwift(remapClass(cls->superclass), nil);
metacls = realizeClassWithoutSwift(remapClass(cls->ISA()), nil);
...
cls->superclass = supercls;
cls->initClassIsa(metacls);
...
// Connect this class to its superclass's subclass lists
if (supercls) {
    addSubclass(supercls, cls);
} else {
    addRootClass(cls);
}

并且会调用到methodizeClass,关联类信息中的数据。

5.
if (slowpath((behavior & LOOKUP_INITIALIZE) && !cls->isInitialized())) {
    cls = initializeAndLeaveLocked(cls, inst, runtimeLock);
}

内部会有一个递归函数,判断类及其父类是否实现isInitialized,没有则会先进行类的初始化。

以上前几段代码,都是慢速查找的准备工作,此后的for死循环是关键部分。


慢速查找for循环

for (unsigned attempts = unreasonableClassCount();;) 这个for循环没有判断条件等,它是个死循环。

1.
Method meth = getMethodNoSuper_nolock(curClass, sel);

通过getMethodNoSuper_nolock -> search_method_list_inline -> findMethodInSortedMethodList

2.
if (meth) {
    imp = meth->imp;
    goto done;
}
...
done:
log_and_fill_cache(cls, imp, sel, inst, curClass);
runtimeLock.unlock();

当二分查找到方法时,会调用到log_and_fill_cache log_and_fill_cache会调用到cache_fill缓存填充,而在cache_fill中,会调用到cache->insert方法。

类结构cache中详述了方法插入到缓存的过程。


3
if (slowpath((curClass = curClass->superclass) == nil)) {
    // No implementation found, and method resolver didn't help.
    // Use forwarding.
    imp = forward_imp;
    break;
}

当二分查找没有找到时,此时curClass会被赋值它的父类superclass,并判断是否为nil,是的话imp被赋值forward_imp后跳出循环,否则继续向下。

往上看forward_imp

全局搜_objc_msgForward_impcache,在汇编代码objc-msg-arm64.s 再查找到C++代码objc-runtime.mm 看到了常见的unrecognized selector sent to instance方法未找到。

那么该段代码的含义就是,当根据类及其父类直到根类,都没有找到方法时,objc_msgSend消息转发到了objc_defaultForwardHandler


4. 重点中的重点
// Superclass cache.
imp = cache_getImp(curClass, sel);

由第三步,此时的curClass是被赋值了它的父类,并且父类不为nilcache_getImp就会从父类的缓存中进行查找。cache中查找,同样我们需要到汇编中搜索其实现代码。

和快速查找类似,同样会调用到CacheLookup,不过传递的参数为GETIMP而不是快速查找时的NORMALCacheLoopup的过程不再赘述,重点看当在缓存中也找不到时,CheckMissJumpMiss都会跳转到LGetImpMiss,其中只有返回了空。

汇编cache_getImp返回空没有从父类的缓存中找到方法,则会继续for死循环,此时的curClass变成了最初类的父类,会再次调用到Method meth = getMethodNoSuper_nolock(curClass, sel);,从父类的类信息中进行慢速查找,直到找到跳出循环,或者找到根类的父类nil,进行消息转发到unrecognized selector sent to instance


流程图总结: