方法查找和动态决议

我们在objc_msgSend汇编流程分析了缓存的查找流程,缓存找不到时,会执行到C++的lookupImpOrForward方法进行方法查找,现在来分析方法查找流程。

第一次调用这个方法传进来的4个参数:inst = receiversel = selcls = classbehavior = 3,这些参数在上一篇文章的汇编代码已经分析过了。为什么说第一次?因为后面还会进来。

const IMP forward_imp = (IMP)_objc_msgForward_impcache;首先先声明一个变量forward_imp,全局搜索_objc_msgForward_impcache发现他的实现在汇编代码中,看arm64的即可:

STATIC_ENTRY __objc_msgForward_impcache
// No stret specialization.
b   __objc_msgForward
END_ENTRY __objc_msgForward_impcache

ENTRY __objc_msgForward
adrp    x17, __objc_forward_handler@PAGE
ldr p17, [x17, __objc_forward_handler@PAGEOFF]
TailCallFunctionPointer x17
END_ENTRY __objc_msgForward

上面汇编代码可以看出最后跳转__objc_forward_handler方法,再全局搜,没有实现,去掉一个下划线搜,实现在C++代码中。

__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);
}
void *_objc_forward_handler = (void*)objc_defaultForwardHandler;

if (slowpath(!cls->isInitialized())) {
  behavior |= LOOKUP_NOCACHE;
}

这里判断cls是否已经初始化完成,如果没有,behavior记录,在下面代码判断使用,类没有初始化完成的话下面存缓存的方法就不会走,就不会存缓存。


checkIsKnownClass(cls);

这个方法检查类是否已经注册在类表里面,具体实现:

ALWAYS_INLINE
static bool
isKnownClass(Class cls)
{
    if (fastpath(objc::dataSegmentsRanges.contains(cls->data()->witness, (uintptr_t)cls))) {
        return true;
    }
    auto &set = objc::allocatedClasses.get();
    return set.find(cls) != set.end() || dataSegmentsContain(cls);
}

allocatedClasses即为已经创建的class的表。


cls = realizeAndInitializeIfNeeded_locked(inst, cls, behavior & LOOKUP_INITIALIZE);

这个方法的意义就是确保这个类已经初始化,对rwro做处理,处理完supermeta又走这个方法把父类、元类都要初始化完,方便后面找方法。这里暂时先不展开讨论,有兴趣可以研究下。


for (unsigned attempts = unreasonableClassCount();;) {
}

一个死循环,目的就是无限的遍历,找父类,再父类直到nil。找到方法会跳出循环。


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
        }

这个判断就是判断缓存的第0位是否为1,是1进入if从共享缓存查,这里有个疑问,就是为什么汇编流程已经查找一遍共享缓存,在这为什么又找一遍呢?因为你在这个方法调用之前,别人有可能插入方法,所以又查找一遍。不是主要流程,我们看else


Method meth = getMethodNoSuper_nolock(curClass, sel);
if (meth) {
    imp = meth->imp(false);
    goto done;
}

从当前的curClass对象里面遍历方法,查找是否有和sel相同的方法,如果查找到,imp赋值,跳转到done位置。

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;
}

methods是从rw取出来所有的方法的二维数组,这里面for循环取出每一个list,然后再查找。

if (fastpath(methodListIsFixedUp && methodListHasExpectedSize)) {
    return findMethodInSortedMethodList(sel, mlist);
} else {
    // Linear search of unsorted method list
    if (auto *m = findMethodInUnsortedMethodList(sel, mlist))
        return m;
}

这里判断如果方法是排好序的,通过二分查找,时间复杂度O(lgn),如果没排序,则遍历查找,时间复杂度O(n)。

template
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;
    
    for (count = list->count; count != 0; count >>= 1) {
        probe = base + (count >> 1);
        
        uintptr_t probeValue = (uintptr_t)getName(probe);
        
        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)getName((probe - 1))) {
                probe--;
            }
            return &*probe;
        }
        
        if (keyValue > probeValue) {
            base = probe + 1;
            count--;
        }
    }
    
    return nil;
}

上面就是二分查找的核心代码。
思路:

  1. 利用一个base来记录每次一查找段落的起始位置,如果key小于当前一半,base不变,下次寻找起始位置不变,如果key大于当前一半,base变为一半位置+1,下次从右半边为起始位置。
  2. 偏移量是每次循环都会除以2,加上起始位置和key做比较。
  3. 判断结果相等还有个循环,是处理比如分类和类有同样的方法,会执行分类方法,如果查到的是类的方法,需要减1拿到分类方法。

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

这里就是把curClass设置成自己的父类,并且判断他的父类是nil的时候,imp = forward_imp;跳出循环。也就是说去父类找方法,直到NSObject都没找到的话,imp = forward_imp


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;
}

cache_getImp又走到汇编里面了,这时curClass已经是父类了,汇编会从父类缓存再开始找:

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

objc_msgSend一开始一样执行CacheLookup从缓存找方法,找到缓存时,执行CacheHit,这时$0 = GETIMP

.elseif $0 == GETIMP
    mov p0, p17
    cbz p0, 9f          // don't ptrauth a nil imp
    AuthAndResignAsIMP x0, x10, x1, x16 // authenticate imp and re-sign as IMP
9:  ret             // return IMP

p17就是查找到的imp,给p0。判断p0结果为0,跳到9,p0不为0,执行下一句,根据imp编码找到imp,返回x0 = imp

缓存找不到时,执行LGetImpMissDynamicp0 = 0返回nil

这时如果找到,imp编码后返回。

找完一圈,如果imp等于forward_imp,说明没找到对应方法,跳出循环;如果imp有值,那么说明找到对应方法,直接跳到done位置。


// No implementation found. Try method resolver once.
if (slowpath(behavior & LOOKUP_RESOLVER)) {
    behavior ^= LOOKUP_RESOLVER;
    return resolveMethod_locked(inst, sel, cls, behavior);
}

正如翻译所写,方法没有找到的时候,尝试一次方法解析器,也就是方法动态决议流程。

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 (!lookUpImpOrNilTryCache(inst, sel, cls)) {
            resolveInstanceMethod(inst, sel, cls);
        }
    }

    // chances are that calling the resolver have populated the cache
    // so attempt using it
    return lookUpImpOrForwardTryCache(inst, sel, cls, behavior);
}

这个方法我们能看出一个细节,就是! cls->isMetaClass()判断是否是元类,如果不是元类,会执行resolveInstanceMethod,如果是元类,执行resolveClassMethod,后面有个判断,判断里面又走了一遍resolveInstanceMethod,这个细节可以看出类方法最后也会走到对象方法的动态决议里面。继续分析:

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

    if (!lookUpImpOrNilTryCache(cls, resolve_sel, cls->ISA(/*authenticated*/true))) {
        // Resolver not implemented.
        return;
    }

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

首先看一个细节,这里resolveInstanceMethod返回值resolved并没有做逻辑判断,只是后面日志用作判断,所以这个方法我们返回YES或者NO都不影响这个程序的逻辑。
lookUpImpOrNilTryCache是查找方法的流程,第一次执行时候必然还是找不到,所以会走下面的resolveInstanceMethod给一次机会去动态添加方法。

IMP lookUpImpOrNilTryCache(id inst, SEL sel, Class cls, int behavior)
{
    return _lookUpImpTryCache(inst, sel, cls, behavior | LOOKUP_NIL);
}

这里需要注意下,behavior的值包含了LOOKUP_NIL

static IMP _lookUpImpTryCache(id inst, SEL sel, Class cls, int behavior)
{
    runtimeLock.assertUnlocked();

    if (slowpath(!cls->isInitialized())) {
        // see comment in lookUpImpOrForward
        return lookUpImpOrForward(inst, sel, cls, behavior);
    }

    IMP imp = cache_getImp(cls, sel);
    if (imp != NULL) goto done;
#if CONFIG_USE_PREOPT_CACHES
    if (fastpath(cls->cache.isConstantOptimizedCache(/* strict */true))) {
        imp = cache_getImp(cls->cache.preoptFallbackClass(), sel);
    }
#endif
    if (slowpath(imp == NULL)) {
        return lookUpImpOrForward(inst, sel, cls, behavior);
    }

done:
    if ((behavior & LOOKUP_NIL) && imp == (IMP)_objc_msgForward_impcache) {
        return nil;
    }
    return imp;
}

这里先判断如果cls没有初始化,走一遍慢速查找流程,不过这时behavior & LOOKUP_NIL != 1,就算找不到也不会走方法动态决议了。类已经初始化完毕,从缓存找一遍,如果找到,判断behavior & LOOKUP_NIL并且方法不是_objc_msgForward_impcache直接返回该方法,如果没找到,走慢速查找流程去找方法。

如果是元类,执行resolveClassMethod,主要流程和resolveInstanceMethod没什么区别,只是实现的动态决议的方法不同,这里不再赘述。

static void resolveClassMethod(id inst, SEL sel, Class cls)
{
    runtimeLock.assertUnlocked();
    ASSERT(cls->isRealized());
    ASSERT(cls->isMetaClass());

    if (!lookUpImpOrNilTryCache(inst, @selector(resolveClassMethod:), cls)) {
        // Resolver not implemented.
        return;
    }

    BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
    bool resolved = msg(nonmeta, @selector(resolveClassMethod:), sel);
}

这里判断类方法找不到会再走一遍resolveInstanceMethod,为什么呢,因为底层是不区分类方法和实例方法的,类方法执行从元类开始找实现,最后会找到NSObject类中,所以与之对应还要走一遍resolveInstanceMethod。也就是说,苹果为了圆类方法、实例方法的谎,做了一遍又一遍的处理。

这里可以看出,如果我们在NSObject分类里面实现resolveInstanceMethod,那不管类方法还是实例方法,都可以在这一个方法中处理。

IMP lookUpImpOrForwardTryCache(id inst, SEL sel, Class cls, int behavior)
{
    return _lookUpImpTryCache(inst, sel, cls, behavior);
}

这里稍微有点乱,先总结一下方法动态决议:

  1. 方法查找流程找不到resolveInstanceMethod
  2. 这时lookUpImpOrNilTryCache判断肯定还是找不到方法,执行第一次resolveInstanceMethod可以动态处理
  3. 这里动态决议无论成功与否,返回值是什么,都会走lookUpImpOrForwardTryCache,这时的behavior已经不包含LOOKUP_RESOLVER
  4. _lookUpImpTryCache查找方法,这时找到的话,是什么就返回什么,没找到就会重新走lookUpImpOrForward找一遍
  5. 这次慢速查找就算找不到,也不会再执行resolveMethod_locked,会直接返回结果

简单来说,就是方法找不到,会先执行resolveInstanceMethod看是否有动态处理这个方法,之后再重新回来从缓存开始再找一遍方法。
这里还有个问题就是resolveInstanceMethod会走两次,我们分析只会走一次,为什么执行两次在下一篇文章中说明。


 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;

while循环是看cache是否有共享缓存的标记,真机测试没有,所以暂时不考虑。log_and_fill_cache这个方法就是向缓存中插入方法,因为这里面已经是慢速查找流程,所以自己缓存肯定没有这个方法,当然插入缓存的时候也有校验,缓存存在就不插入了。

如果behavior的值包含LOOKUP_NIL并且forward_imp,返回空。

总结:当objc_msgSend从缓存快速查找找不到方法时,会走慢速查找流程。流程图如下:

方法查找流程.jpg

你可能感兴趣的:(方法查找和动态决议)