OC方法查找之慢速查找流程分析

1,196 阅读5分钟

前面我们已经在runtime之objc_msgSend分析文章中已经对方法快速查找流程进行了分析,并且知道,当在cache中无法找到方法的IMP时,会调用_objc_msgForward函数进入慢速查找流程,这遍文章我们来分析一下慢速查找过程中,OC底层的处理逻辑

方法的查找

创建一个LGPersonLGStudent和一个NSObject的分类NSObject+LGCate,其中LGPerson继承自NSObject,LGStudent继承自LGPerson,分别实现如下方法;

LGPerson声明并实现了sayNBsayHappay

LGStudent声明并实现了sayHellosayObjc

NSObject+LGCate声明并实现了sayEasy

我们在main函数中实现如下:

  • 创建一个LGStudent的实例对象,并调用了sayHello和sayNB方法;
  • LGStudent调用了类方法sayObjc和sayHappay;
  • LGStudent调用类方法sayEasy.

下面我们执行代码,看结果:

解释:

  • 实例对象可以调用自己的对象方法,也可以调用父类的对象方法;
  • 类对象可以调用自己的类方法,也可以调用父类的类方法;
  • LGStudent调用的是类方法sayEasy,为什么实际调用的是分类的对象方法sayEasy???

下面我们看一下isa走位图

在文章OC类方法归属分析我们已经知道,类方法归属于元类,通过如上的isa走位图可以发现,元类最终会继承于根元类,而根元类继承自NSObject,所以才有了LGStudent调用的是类方法sayEasy,实际调用的却是分类的对象方法sayEasy

总结:

实例对象或类对象在调用方法时,会先在本类中查找,查不到时,再沿着继承链到父类中查找,最后找到NSObject中。如果都没找到就会报错unrecognized selector sent to instance 0x1006059d0'

通过源码分析慢速查找流程

我们在objc4-781源码中全局搜索_objc_msgForward,如果如下:

查询到这么多结果,那么我们到底应该研究哪个呢?

执行如下代码,并断点

按住control键,然后鼠标左键点击如下图标:

多点几次,就会来到如下的位置:

因为say666并没有实现,所以往下走,找到_objc_msgSend_uncached,然后断点在这里:

按住control键,然后鼠标左键点击如下图标:

找到lookUpImpOrForward我们发现,其所在的文件名为objc-runtime-new.mm,在6099行代码.

这样我们成功的找到了我们要研究的源码对象objc-runtime-new.mm

objc-runtime-new.mm搜索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); // 有问题???? cache_getImp - lookup - lookUpImpOrForward
        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;
}

下面我们分段分析

  • const IMP forward_imp = (IMP)_objc_msgForward_impcache;查找到objc_msgForwardimp,当最终没有找到目标方法的imp时,回调objc_msgForwardimp进行消息转发流程;
  • imp = cache_getImp(cls, sel);再次走快速查找的流程是因为可能有多线程调用方法将目标方法的Imp进行了缓存,再查一次岂不更快。

  • 先确定好继承关系isa指向,可以在后续快速的沿着继承链查找方法的Imp
  • 确定当前的类,可以到类的结构中查找方法的Imp.

  • 先去类结构的methodList中查找,找到就返回;
  • 没找到就沿着继承链找到父类,此时同样要先去cache中查找,找到返回,没找到又会来到lookUpImpOrForward的慢速查找,从而形成递归查找;
  • 找到NSObject都没有找到,imp指向objc_msgForwardimp,进入消息转发的流程.

进入消息转发

以上流程解释:找到了imp,将imp插入cache缓存,有关cache缓存流程,请看文章OC类结构之cache分析

总结

方法查找的慢速查找流程: 拿到objc_msgForwardimp -> 再次查找cache -> 确定继承关系 -> 去类的methodList中找 -> 去父类cache中查找 -> 去父类的methodList中查找 -> 沿着继承链查找 —> 没找到进入消息转发 —> 找到了返回并写入cache