objc_msgSend慢速查找流程分析

在之前写过一篇关于objc_msgSend的流程分析,而它就是方法的快速查找;那么本文将介绍一下方法的慢速查找流程。

objc_msgSend的汇编代码中,当查找isa完毕之后,他会跳转到CacheLookup的汇编代码中执行,当系统在缓存中没有找到相应的方法,他就会继续跳转到CheckMiss的汇编代码中。我们来看一下CheckMiss的源码:

.macro CheckMiss
    // miss if bucket->sel == 0
.if $0 == GETIMP
    cbz p9, LGetImpMiss
.elseif $0 == NORMAL
    cbz p9, __objc_msgSend_uncached
.elseif $0 == LOOKUP
    cbz p9, __objc_msgLookup_uncached
.else
.abort oops
.endif
.endmacro

当系统走到这个过程中,会继续执行NORMAL中的cbz p9, __objc_msgSend_uncached这一行代码,也就是说,我们就可以以这个方法为入口去探索慢速查找的过程。

我们进行全局搜索__objc_msgSend_uncached这个,找到了MethodTableLookup,这个是一个方法列表,继续搜索这个方法,我们就只能查找到一个跳转的bl _lookUpImpOrForward,之后我们就无法从找到这个方法的内容。

那么按照我们往常的套路,我们把下划线去掉,经过搜索lookUpImpOrForward这个方法,找到了很多。

那么我们有没有别的途径去找到这个方法呢?

我们创建一个工程,在工程中创建一个类,在类中创建一个方法,在调用方法前打上断点:

16007838857526.png

把这个模式打开,可可以看到汇编中我们打上断点的方法处:


16007840902468.png

在方法下面的objc_msgSend处打上断点:

16007841565462.png

按住control+
16007842149173.png

就进入了objc_msgSend方法中去了:

16007842605844.png

继续往下滑,就会看到下面图的样子,在_objc_msgSend_uncached处打上断点,继续按住control+

16007842149173.png

16007845553851.png

接着就会跳转到_objc_msgSend_uncached方法中去,就能看到lookUpImpOrForward at objc-runtime-new.mm:6099这个信息。

16007849452311.png

下面继续描述一下方法的一些基本常识:
在一个类中,实例方法存储在中,类方法存储在元类中;
那么当我们用调用类方法的方式去调用实例方法,它也会执行成功;那为什么用类方法的方式去调用实例方法,它也会成功呢?

答案在一张图中:

isa流程图.png

isa的走位中,根元类最后也会走到NSObject,因此,我们可以用调用类方法的方式去调用实例方法。

下面继续探索慢速查找流程:
由于在上面我们找到了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();
        if (fastpath(behavior & LOOKUP_CACHE)) {
        imp = cache_getImp(cls, sel);
        if (imp) goto done_nolock;
    }
    runtimeLock.lock();
    checkIsKnownClass(cls);
    if (slowpath(!cls->isRealized())) {
        cls = realizeClassMaybeSwiftAndLeaveLocked(cls, runtimeLock);
    }
    if (slowpath((behavior & LOOKUP_INITIALIZE) && !cls->isInitialized())) {
        cls = initializeAndLeaveLocked(cls, inst, runtimeLock);
    }
    runtimeLock.assertLocked();
    curClass = cls;
        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)) {
            imp = forward_imp;
            break;
        }
        if (slowpath(--attempts == 0)) {
            _objc_fatal("Memory corruption in class list.");
        }

        // Superclass cache.
        imp = cache_getImp(curClass, sel);
        if (slowpath(imp == forward_imp)) {
            break;
        }
        if (fastpath(imp)) {
            goto done;
        }
    }
    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;
}

在main函数中调用一个方法,在方法调用处打上断点,在执行程序之后,在if (fastpath(behavior & LOOKUP_CACHE)) {处打上断点,我们点击下一步,程序就会跳转到这一步上去;

16008270884210.png

我们发现,快速查找会查找缓存中的方法,而慢速查找,它还要查找cache_getImp,那这是为什么呢?
答案是因为要避免多线程的影响,当你程序在调用时,线程可能偷偷摸摸的缓存了一次。

我们继续往下看源码,在下面有两个if slowpath判断,我们从上面的if (slowpath(!cls->isRealized()))的判断内去看它的实现:

cls = realizeClassMaybeSwiftAndLeaveLocked(cls, runtimeLock);
它里面存在一个方法realizeClassMaybeSwiftAndLeaveLocked,点击方法进去,它返回的也是一个方法:realizeClassMaybeSwiftMaybeRelock,继续点击,在realizeClassMaybeSwiftMaybeRelock方法中,有一个关键的方法:realizeClassWithoutSwift,点击进去,里面的代码很多,贴出一部分源码:

runtimeLock.assertLocked();

    class_rw_t *rw;
    Class supercls;
    Class metacls;

    if (!cls) return nil;
    if (cls->isRealized()) return cls;
    ASSERT(cls == remapClass(cls));

    // fixme verify class is not in an un-dlopened part of the shared cache?

    auto ro = (const class_ro_t *)cls->data();
    auto isMeta = ro->flags & RO_META;
    if (ro->flags & RO_FUTURE) {
        // This was a future class. rw data is already allocated.
        rw = cls->data();
        ro = cls->data()->ro();
        ASSERT(!isMeta);
        cls->changeInfo(RW_REALIZED|RW_REALIZING, RW_FUTURE);
    } else {
        // Normal class. Allocate writeable class data.
        rw = objc::zalloc();
        rw->set_ro(ro);
        rw->flags = RW_REALIZED|RW_REALIZING|isMeta;
        cls->setData(rw);
    }

在这个方法中,首先读clsdata数据,对rw,ro进行赋值,然后就有了数据;在有了数据之后,我们继续读下一块代码:

 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)" : "");
    }



    supercls = realizeClassWithoutSwift(remapClass(cls->superclass), nil);
    metacls = realizeClassWithoutSwift(remapClass(cls->ISA()), nil);

里面有一个superclsmetacls两个递归实现,利用realizeClassWithoutSwift,把整个继承链递归下来,这符合那副经典的走位图。继续看下一块代码:

// Update superclass and metaclass in case of remapping
    cls->superclass = supercls;
    cls->initClassIsa(metacls);

    // Reconcile instance variable offsets / layout.
    // This may reallocate class_ro_t, updating our ro variable.
    if (supercls  &&  !isMeta) reconcileInstanceVariables(cls, supercls, ro);

    // Set fastInstanceSize if it wasn't set already.
    cls->setInstanceSize(ro->instanceSize);

    // Copy some flags from ro to rw
    if (ro->flags & RO_HAS_CXX_STRUCTORS) {
        cls->setHasCxxDtor();
        if (! (ro->flags & RO_HAS_CXX_DTOR_ONLY)) {
            cls->setHasCxxCtor();
        }
    }
    
    // Propagate the associated objects forbidden flag from ro or from
    // the superclass.
    if ((ro->flags & RO_FORBIDS_ASSOCIATED_OBJECTS) ||
        (supercls && supercls->forbidsAssociatedObjects()))
    {
        rw->flags |= RW_FORBIDS_ASSOCIATED_OBJECTS;
    }

    // Connect this class to its superclass's subclass lists
    if (supercls) {
        addSubclass(supercls, cls);
    } else {
        addRootClass(cls);
    }

    // Attach categories
    methodizeClass(cls, previously);

    return cls;

在这一块中,有对cls->superclasscls->initClassIsa进行赋值,然后在下面会对supercls进行判断,分析这个if判断,它是一个双向链表结构,结下来的methodizeClass是对所有列表和属性都会添加到rwe当中去。

看完realizeClassMaybeSwiftAndLeaveLocked的大部分内容之后,我们继续往下看另一个slowpath中的initializeAndLeaveLocked方法;继续点击方法进去,最终走到initializeAndMaybeRelock方法,在这个方法中最重要的一行代码是调用的initializeNonMetaClass这个方法,这个方法,贴出一块代码:

Class supercls;
    bool reallyInitialize = NO;
initialize cls.

    supercls = cls->superclass;
    if (supercls  &&  !supercls->isInitialized()) {
        initializeNonMetaClass(supercls);
    }
    SmallVector<_objc_willInitializeClassCallback, 1> localWillInitializeFuncs;
    {
        monitor_locker_t lock(classInitLock);
        if (!cls->isInitialized() && !cls->isInitializing()) {
            cls->setInitializing();
            reallyInitialize = YES;
            localWillInitializeFuncs.initFrom(willInitializeFuncs);
        }
    }
    if (reallyInitialize) {

        _setThisThreadIsInitializingClass(cls);

        if (MultithreadedForkChild) {
            // LOL JK we don't really call +initialize methods after fork().
            performForkChildInitialize(cls, supercls);
            return;
        }
        
        for (auto callback : localWillInitializeFuncs)
            callback.f(callback.context, cls);
        if (PrintInitializing) {
            _objc_inform("INITIALIZE: thread %p: calling +[%s initialize]",
            objc_thread_self(), cls->nameForLogging());
        }

上面的一块代码中,作用很明显,层层递归初始化所有方法的;

知道这个作用之后,其他代码也就不那么重要了,回到lookUpImpOrForward方法,继续往下看源码,它的返回值是imp,既然是返回imp,如果它找不到imp怎么办呢?
那么我们就去看看imp赋值的地方在哪里,他的重点在for (unsigned attempts = unreasonableClassCount();;)for循环中,代码在上面已经贴出来了。我们去研究这一段代码:
在代码中有一个方法getMethodNoSuper_nolock获取cls中的所有方法。
我们来看一下它的源码:

static method_t *
getMethodNoSuper_nolock(Class cls, SEL sel)
{
    runtimeLock.assertLocked();

    ASSERT(cls->isRealized());
    // fixme nil cls? 
    // fixme nil sel?

    auto const methods = cls->data()->methods();
    for (auto mlists = methods.beginLists(),
              end = methods.endLists();
         mlists != end;
         ++mlists)
    {
        //  getMethodNoSuper_nolock is the hottest
        // caller of search_method_list, inlining it turns
        // getMethodNoSuper_nolock into a frame-less function and eliminates
        // any store from this codepath.
        method_t *m = search_method_list_inline(*mlists, sel);
        if (m) return m;
    }

    return nil;
}

可以看到cls->data()->methods(),这个data()就是之前加载过来的ro,rw这些东西。

探索到这里,就涉及到一个算法,所有的方法都在method_list中,那么系统如何去查找方法呢?
答案:二分查找,二分查以中间点为界限,将精确的值确定在某个范围内,经过不断缩小范围,查找正确的内容;

那么是如何知道系统是通过二分查找去找到对应的方法呢?
getMethodNoSuper_nolock方法中有一个对method_t类型的*m赋值的search_method_list_inline函数,我们在这个函数中有一个findMethodInSortedMethodList方法,下面附上这个方法的源码:

ALWAYS_INLINE static method_t *
findMethodInSortedMethodList(SEL key, const method_list_t *list)
{
    ASSERT(list);

    const method_t * const first = &list->first;
    const method_t *base = first;
    const method_t *probe;
    uintptr_t keyValue = (uintptr_t)key;
    uint32_t count;
    
    for (count = list->count; count != 0; count >>= 1) {
        probe = base + (count >> 1);
        
        uintptr_t probeValue = (uintptr_t)probe->name;
        
        if (keyValue == probeValue) {
            // `probe` is a match.
            // Rewind looking for the *first* occurrence of this value.
            // This is required for correct category overrides.
        while (probe > first && keyValue == (uintptr_t)probe[-1].name) {
                probe--;
            }
            return (method_t *)probe;
        }
        
        if (keyValue > probeValue) {
            base = probe + 1;
            count--;
        }
    }
    
    return nil;
}

在for循环遍历所有方法中,probe的赋值中有一个count >> 1位移操作,其作用是减少二分之一;
例如:8的二进制为1000; 8>>1 100 = 4;

通过上面的代码,我们可以了解到系统查找方法是使用二分查找来寻找内容。

我们可以在控制台中打印list中的内容,就可以打印该类中的所有方法。

那么在上面的代码中,probe--又是怎么理解呢?
它其实是来判断分类重名的作用;

我们来探索一下:
首先创建一个Person,里面创建一个方法,在对Person创建一个分类。分类中创建一个相同的方法,在main函数中调用,得到的结果是打印的是分类方法。这边就不贴代码了,实现起来比较简单。

当系统没有找到对应的方法之后,它就会执行goto done;代码:

done:
    log_and_fill_cache(cls, imp, sel, inst, curClass);
    runtimeLock.unlock();
    

static void
log_and_fill_cache(Class cls, IMP imp, SEL sel, id receiver, Class implementer)
{
#if SUPPORT_MESSAGE_LOGGING
    if (slowpath(objcMsgLogEnabled && implementer)) {
        bool cacheIt = logMessageSend(implementer->isMetaClass(), 
                                      cls->nameForLogging(),
                                      implementer->nameForLogging(), 
                                      sel);
        if (!cacheIt) return;
    }
#endif
    cache_fill(cls, sel, imp, receiver);
}

那么在这个代码中,它会对方法进行缓存,以方便下次快速查找。

那么整个流程就是:objc_msgSend -> 二分查找自己 -> cache_fill ->objc_msgSend形成一个闭环。
如果在自己中没有找到方法,那么它就会去父类去找,在父类中快速查找没找到,他就会执行cache_getImp方法,在经过搜索这个方法,没有找到,那么可以肯定的是,它存在汇编代码中(只要与缓存相关的,一般都在汇编代码中):

STATIC_ENTRY _cache_getImp

GetClassFromIsa_p16 p0
CacheLookup GETIMP, _cache_getImp

那么它又会执行CacheLookup方法,接着又会跑到lookUpImpOrForward方法中查找。

注意事项:当在父类中快速查找中没有找到方法,那么它不会继续递归进行慢速查找,而是继续找父类的父类(NSObject)的缓存中进行快速查找,当在NSObject的缓存中没有找到,那么继续找NSObject的父类nil的缓存,进入一个死循环过程。

那为什么在父类中没有慢速查找过程呢?
由于imp的获取在cache_getImp中,通过全局搜索,它存在汇编当中,附上代码:

STATIC_ENTRY _cache_getImp

    GetClassFromIsa_p16 p0
    CacheLookup GETIMP, _cache_getImp

可以看到它的CacheLookupGETIMP类型;因此,在CacheLookup的汇编代码最后会在JumpMiss中,附上他的代码:

.macro JumpMiss
.if $0 == GETIMP
    b   LGetImpMiss
.elseif $0 == NORMAL
    b   __objc_msgSend_uncached
.elseif $0 == LOOKUP
    b   __objc_msgLookup_uncached
.else
.abort oops
.endif
.endmacro

可以看到,只有当CacheLookupNORMAL时,才会进行慢速查找。
LGetImpMiss的代码中,它最后只是进行return返回。

那么如果在父类中还是没有找到,那么在之前就会对(curClass = curClass->superclass) == nil)会判断,将imp赋值为forward_imp,之后会执行这一句判断imp == forward_imp,相等的话,就会递归出去,跳出循环。

那么这个forward_imp又是什么东西呢?
在上面的代码中,forward_imp是通过_objc_msgForward_impcache方法进行赋值的,我们去看看这个方法,经过全局搜索,它存在汇编中,代码我就不贴出来的,直接写出关键方法__objc_forward_handler,搜索_objc_forward_handler,就得到一个很有意思的东西:

__attribute__((noreturn, cold)) void
objc_defaultForwardHandler(id self, SEL sel)
{
    _objc_fatal("%c[%s %s]: unrecognized selector sent to instance %p "
                "(no message forward handler is installed)", 
                class_isMetaClass(object_getClass(self)) ? '+' : '-', 
                object_getClassName(self), sel_getName(sel), self);
}

上面的代码,相信很多iOS开发者都见过。当方法找不到时,就会显示这个错误。

那么在报错之前,它还会走到下面这部分代码中:

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

那么我们就去看看这部分代码中的内容,首先点击resolveMethod_locked,他是一个动态方法决议;动态方法决议其实是系统在没有找到方法时,它给你一个处理过程,处理完之后,它再继续去查找,只要能找到方法所需要的内容,就不会报错,如果没有,则报出错误。
下面去看看它的源码:

static NEVER_INLINE IMP
resolveMethod_locked(id inst, SEL sel, Class cls, int behavior)
{
    runtimeLock.assertLocked();
    ASSERT(cls->isRealized());

    runtimeLock.unlock();

    if (! cls->isMetaClass()) {
        // try [cls resolveInstanceMethod:sel]
        resolveInstanceMethod(inst, sel, cls);
    } 
    else {
        // try [nonMetaClass resolveClassMethod:sel]
        // and [cls resolveInstanceMethod:sel]
        resolveClassMethod(inst, sel, cls);
        if (!lookUpImpOrNil(inst, sel, cls)) {
            resolveInstanceMethod(inst, sel, cls);
        }
    }
    // chances are that calling the resolver have populated the cache
    // so attempt using it
    return lookUpImpOrForward(inst, sel, cls, behavior | LOOKUP_CACHE);
}

在上面一段代码中,重要的就在那个if判断中,我们去看看resolveInstanceMethod方法,请看源码:

static void resolveInstanceMethod(id inst, SEL sel, Class cls)
{
    runtimeLock.assertUnlocked();
    ASSERT(cls->isRealized());
    SEL resolve_sel = @selector(resolveInstanceMethod:);

    if (!lookUpImpOrNil(cls, resolve_sel, cls->ISA())) {
        // Resolver not implemented.
        return;
    }

    BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
    bool resolved = msg(cls, resolve_sel, sel);

    // Cache the result (good or bad) so the resolver doesn't fire next time.
    // +resolveInstanceMethod adds to self a.k.a. cls
    IMP imp = lookUpImpOrNil(inst, sel, cls);

    if (resolved  &&  PrintResolving) {
        if (imp) {
            _objc_inform("RESOLVE: method %c[%s %s] "
                         "dynamically resolved to %p", 
                         cls->isMetaClass() ? '+' : '-', 
                         cls->nameForLogging(), sel_getName(sel), imp);
        }
        else {
            // Method resolver didn't add anything?
            _objc_inform("RESOLVE: +[%s resolveInstanceMethod:%s] returned YES"
                         ", but no new implementation of %c[%s %s] was found",
                         cls->nameForLogging(), sel_getName(sel), 
                         cls->isMetaClass() ? '+' : '-', 
                         cls->nameForLogging(), sel_getName(sel));
        }
    }
}

这段源码作用就是只要能找到这个方法的实现,那么它就会去执行这个方法,动态方法协议其实是苹果给我们的一个机会,在快速和慢速都没找到相应的方法,那么它就给你一个去实现这个方法的机会,只要系统找到了这个方法,那么他就能执行成功。

下面我们用代码来试一下:
在声明文件中创建一个方法,在实现文件中不去写这个方法内容,那么我们在main函数中执行,它会报错,那么我们用另一种方式去写,就不会报错了:

+ (BOOL)resolveInstanceMethod:(SEL)sel{
    
    if (sel == @selector(say666)) {
        NSLog(@"%@ 来了",NSStringFromSelector(sel));
        
        IMP imp           = class_getMethodImplementation(self, @selector(sayMaster));
        Method sayMMethod = class_getInstanceMethod(self, @selector(sayMaster));
        const char *type  = method_getTypeEncoding(sayMMethod);
        return class_addMethod(self, sel, imp, type);
    }
    
    return [super resolveInstanceMethod:sel];
}

上面的代码是实例方法的动态决议,那么类方法的动态决议该如何实现呢?
类方法存在元类中,那么我们在添加对象时,需要将类对象替换成元类对象,而元类一层一层往上走到最后,它最后走到的也是NSObject中,因此,我们可以在实例方法中利用if去进行判断,将类方法的动态决议也写在实例方法中,请看代码:

+ (BOOL)resolveInstanceMethod:(SEL)sel{
    

    NSLog(@"%@ 来了",NSStringFromSelector(sel));
    if (sel == @selector(say666)) {
        NSLog(@"%@ 来了",NSStringFromSelector(sel));

        IMP imp           = class_getMethodImplementation(self, @selector(sayMaster));
        Method sayMMethod = class_getInstanceMethod(self, @selector(sayMaster));
        const char *type  = method_getTypeEncoding(sayMMethod);
        return class_addMethod(self, sel, imp, type);
    }
    else if (sel == @selector(sayNB)) {
        
        IMP imp           = class_getMethodImplementation(objc_getMetaClass("Person"), @selector(lgClassMethod));
        Method sayMMethod = class_getInstanceMethod(objc_getMetaClass("Person"), @selector(lgClassMethod));
        const char *type  = method_getTypeEncoding(sayMMethod);
        return class_addMethod(objc_getMetaClass("LGPerson"), sel, imp, type);
    }
    return NO;
}

在上面的代码中,say666是实例方法,sayNB是类方法,在代码中,区别很明显,类方法的对象是是要获得该类的元类。

上面的代码就是我们对中间层的处理,当系统没有在类中找到这个方法,那么就会启动动态决议,给我们一个处理的过程,处理完之后,再重新去执行lookUpImpOrForward,这样就不会报错了。

重点:lookUpImpOrForward这个方法会执行两次,第一次是动态方法解析,第二次是慢速转发过程。

你可能感兴趣的:(objc_msgSend慢速查找流程分析)