iOS底层之Runtime探索(二)

9,432 阅读7分钟

iOS 全网最新objc4 可调式/编译源码
编译好的源码的下载地址

序言

在前一篇iOS底层之Runtime探索(一)中,已经知道了在selimp的整个缓存查找过程,这个过程是用汇编实现,是一个快速方法查找流程,今天就来探究一下缓存没有查找到后面的流程。

__objc_msgSend_uncached 方法探究

前面流程是对缓存进行查找imp,如果找不到就走方法__objc_msgSend_uncached方法 image.png__objc_msgSend_uncached中,共两条语句MethodTableLookupTailCallFunctionPointer x17

MethodTableLookup方法解析

image.png MethodTableLookup中的代码逻辑

  • x0receiverx1sel
  • mov x2, x16: 将x16也就是clsx2
  • mov x3, #3: 将常量值3x3
  • 调用_lookUpImpOrForward: x0~x3是传的参数;
  • mov x17, x0: 将_lookUpImpOrForward的返回值x0x17

通过注释也可以大致看出,调用方法lookUpImpOrForward获取imp,并返回。

TailCallFunctionPointer定义

image.png 这里仅仅是对传入的$0直接调用。

我们可以总结,__objc_msgSend_uncached方法流程是通过MethodTableLookup获取imp,然后传值到TailCallFunctionPointer定义调用imp

这里的核心就是MethodTableLookup中的lookUpImpOrForward是怎么获取的imp

lookUpImpOrForward方法探究

lookUpImpOrForward通过方法命名,可以看出一些端倪,就是查找imp找不到就forward操作。

实例分析

  • 对上面定义的LGPerson添加分类,并在分类中同样实现sayHello方法;
  • NSObject添加分类,并自定义方法sayNB
  • LGTeacher定义方法sayByeBye,但不添加实现;

image.png 结果

  • sayHello调用的是LGPerson的分类方法,why?
  • 用类对象LGTeacher调用NSObjectsayNB方法成功,why?
  • 未实现方法sayByeBye报错unrecognized selector sent to instance 0x600000008000,why?

lookUpImpOrForward源码分析

源代码添加了中文注释

NEVER_INLINE
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())) {
        /*
         enum {
             LOOKUP_INITIALIZE = 1,
             LOOKUP_RESOLVER = 2,
             LOOKUP_NIL = 4,
             LOOKUP_NOCACHE = 8,
         };
         */

        // 如果没有初始化 behavior = LOOKUP_INITIALIZE | LOOKUP_RESOLVER | LOOKUP_NOCACHE
        behavior |= LOOKUP_NOCACHE;
    }

    // 加锁防止多线程访问出现错乱
    runtimeLock.lock();

    // 检查当前类是否被dyld加载的类
    checkIsKnownClass(cls);

    // 如果尚未实现给定的类,则实现该类;如果尚未初始化,则初始化该类。
    cls = realizeAndInitializeIfNeeded_locked(inst, cls, behavior & LOOKUP_INITIALIZE);

    runtimeLock.assertLocked();
    curClass = cls;


     //在我们获取锁后,用于再次查找类缓存的代码,但对于大多数情况,证据表明这在大多数情况下都是失败的,因此会造成时间损失。
     //在没有执行某种缓存查找的情况下,唯一调用此函数的代码路径是class_getInstanceMethod()。
    for (unsigned attempts = unreasonableClassCount();;) {// 开启循环查找imp
        // 继续检查共享缓存是否有方法
        if (curClass->cache.isConstantOptimizedCache(/* strict */true)) {
#if CONFIG_USE_PREOPT_CACHES
            imp = cache_getImp(curClass, sel); // cache_getImp流程获取imp
            if (imp) goto done_unlock; // 找到imp走”done_unlock“流程
            curClass = curClass->cache.preoptFallbackClass();
#endif
        } else {
            // 从curClass的method list中查找method.
            method_t *meth = getMethodNoSuper_nolock(curClass, sel);
            if (meth) {
                imp = meth->imp(false);
                goto done; // 查找到imp,跳转done流程
            }

            // 未从curClass的method list中查找到对应的method,继续往父类查找,直到父类为nil
            if (slowpath((curClass = curClass->getSuperclass()) == nil)) {
                // No implementation found, and method resolver didn't help.
                // Use forwarding.
                imp = forward_imp; // 查找不到跳出for循环
                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); // 这里curClass已指向父类,查找父类的缓存中的imp
        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)) { // 从父类缓存中查找到imp,跳转到done
            // Found the method in a superclass. Cache it in this class.
            goto done;
        }
    }
    // No implementation found. Try method resolver once.
    // 为找到imp,开始resolveMethod_locked流程,动态方法决议
    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); // 将获取的imp插入缓存
    }

 done_unlock:
    runtimeLock.unlock(); //runtimeLock 解锁
    if (slowpath((behavior & LOOKUP_NIL) && imp == forward_imp)) {
        return nil;
    }
    return imp;
}

逻辑流程分析

  1. 判断当前类是否初始化,若未初始化执行behavior |= LOOKUP_NOCACHE
  2. checkIsKnownClass(cls)检查当前类是否被加载到dyld,类的加载属于懒加载,未加载这里会做加载处理;
  3. realizeAndInitializeIfNeeded_locked如果尚未实现给定的类,则实现该类;如果尚未初始化,则初始化该类;
  4. 进入for循环开始一层层遍历查找imp;
  5. curClass->cache.isConstantOptimizedCache检查共享缓存,这个时候,可能别的地方进行缓存了,如果有则直接跳转done_unlock返回imp
  6. 上面没有缓存,getMethodNoSuper_nolock从当前类的methodlist中查找method
  7. 如果找到跳转done,将找到的imp进行缓存插入,再返回imp
  8. 如果未找到,if (slowpath((curClass = curClass->getSuperclass()) == nil))这里将curClass指向父类,并判断如果父类为nil,将imp指向forward_imp,break跳出for循环;
  9. 如果父类存在,通过cache_getImp检查父类缓存是否有imp,如果impforward_imp则跳出循环,然后再检查imp,如果imp有效则跳转done流程;
  10. 查找for循环结束,没有找到imp,按LOOKUP_RESOLVER判断走动态方法决议流程resolveMethod_locked

归总分析就是,消息查找会沿着继承链一层一层往上查找,直到找到nil,如果有找到则插入缓存并返回imp,如果找不到则imp指向forward_imp也就是_objc_msgForward_impcache

上面LGTeacher调用NSObject的分类方法sayNB的过程,就是LGTeacher沿着元类的继承链找到了NSObject,并调用sayNB方法。

lookUpImpOrForward流程图

消息慢速查找流程-导出(1).png

getMethodNoSuper_nolock方法查找解析

getMethodNoSuper_nolock的方法中,查找method的算法就是findMethodInSortedMethodList方法中的二分查找算法实现的。

template<class getNameFunc>
ALWAYS_INLINE static method_t *
findMethodInSortedMethodList(SEL key, const method_list_t *list, const getNameFunc &getName)
{
    ASSERT(list);
    // method_list_t 数组是通过将sel转为uintptr_t类型的value值进行排序的
    auto first = list->begin(); // 起始位置 0
    auto base = first;
    decltype(first) probe;
    
    uintptr_t keyValue = (uintptr_t)key; // 目标sel
    uint32_t count; // 数组个数

    for (count = list->count; count != 0; count >>= 1) {
        probe = base + (count >> 1); // probe = base + floor(count / 2)

        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) { // 目标value大于,二分法的中间数
            base = probe + 1;
            count--;
        }
    }
    return nil;
}

这里的二分算法设计还是挺巧妙的,可以慢慢品玩;其中在keyValue == probeValue的时候,会进入while循环,让probe--,直到找到同名方法中最前面的方法,同名方法就是我们分类中重写的方法,分类中方法会放在前面,所以调用是会优先调用分类中的方法实现。

cache_getImp方法解析

image.png 根据之前对CacheLookup的分析,cache_getImpMissLabelDynamic传的是LGetImpMissDynamic,因此如果CacheLookup中找不到imp就会进入LGetImpMissDynamic,这里仅仅是返回了0值,所以cache_getImp流程在这里也就断了返回的是0

forward_imp解析

在取父类(curClass = curClass->getSuperclass()) == nil)的判断中,如果往上没有父类了,即条件成立,那imp会指向_objc_msgForward_impcache类型的forward_imp,并返回执行imp

image.png

  • _objc_msgForward_impcache中只是对__objc_msgForward进行调用;
  • __objc_msgForward是对__objc_forward_handler相关的方法操作返回值到x17,并通过TailCallFunctionPointer调用x17

image.png __objc_forward_handler就是函数objc_defaultForwardHandler,这里面就是直接抛出错误unrecognized selector sent to instance

这里的错误信息字符串中,对+-的处理是通过元类判断的,底层中没有+号方法或-号方法,都是OC的语法设计。

总结

  • 在缓存cache中没有找到imp,就会通过lookUpImpOrForward开启从当前类NSObject方法列表进行层层遍历,这个过程为“慢速查找”过程;
  • 如果找到了就会返回imp,并执行;
  • 如果找不到就会让imp指向forward_imp并返回,执行forward_imp系统会直接抛出方法未找到的错误;
  • 在返回forward_imp之前,还有一步resolveMethod_locked操作,这就是动态方法决议的流程,在下一篇幅展开细讲。

以上是Runtime中对objc_msgSned方法的慢速查找流程解析,如有疑问或错误之处,请在评论区留言或私信我,感谢支持~❤