OC底层原理09-消息慢速查找

472 阅读7分钟

前言

上一篇文章我们说到一只找不到就走__objc_msgSend_uncached 我们探索一下

objc_msgSend_uncached

在当前类中,缓存查找流程中如果没有到查找目标方法,跳转 MissLabelDynamic 流程 MissLabelDynamic = __objc_msgSend_uncached ,搜索 __objc_msgSend_uncached 并找到入口,真机汇编代码如下

Xnip2021-07-04_12-48-04.jpg

简单的几行汇编代码,最重要的是 MethodTableLookup TailCallFunctionPointer x17,现在不知道 x17 是什么,那么看 TailCallFunctionPointer x17 做了什么,全局搜索 TailCallFunctionPointer 代码如下

Xnip2021-07-04_12-49-35.jpg TailCallFunctionPointer 就一行汇编代码 br $0. 因为 0 = p17 br $0 的意思读取 p17 寄存器中的地址并且跳转到该地址,因为我们是在查询方法就是根据找 sel imp ,在根据 TailCallFunctionPointer 提示。猜测 p17 寄存器应该存的是 imp

p17 寄存器在 __objc_msgSend_uncached 没有赋值的地方,那么只能在MethodTableLookup赋值。代码如下

Xnip2021-07-04_12-53-11.jpg 通过 _lookUpImpOrForward 查询到 imp ,将 imp 赋值给 x17 寄存器,全局搜索 _lookUpImpOrForward ,发现汇编里面没有 _lookUpImpOrForward 的定义或者实现,全局搜索 lookUpImpOrForward

Xnip2021-07-04_12-56-44.jpg

lookUpImpOrForward 的返回值是 imp ,疑问:为什么 lookUpImpOrForward 实现使用汇编呢?

汇编更接近机器语言,查询速度更快。缓存查找流程是快速在缓存中找到方法,而慢速查找流程是不断的遍历 methodlist 过程,这个过程很慢 方法中有的参数是不确定的,但是在C语言中参数必须是确定的,而汇编可以让你感觉更加的动态化

总结

  • 通过 MethodTableLookup 查询将查询到 imp 作为返回值存在 `x0寄存器,将x0寄存器的值赋值给x17寄存器
  • 通过 TailCallFunctionPointer x17 直接调用 x17 寄存器中的 imp
  • __objc_msgSend_uncached--> MethodTableLookup --> _lookUpImpOrForward --> TailCallFunctionPointer

lookUpImpOrForward

lookUpImpOrForward 根据 sel 查询 imp ,具体是怎么查询 imp 下面探究下

NEVER_INLINE
IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior)
{
    //定义消息转发forward_imp  //behavior传入的是 3  = LOOKUP_INITIALIZE|LOOKUP_RESOLVER
    const IMP forward_imp = (IMP)_objc_msgForward_impcache;
    IMP imp = nil;
    Class curClass;
    runtimeLock.assertUnlocked();
    /*
      //判断类是否初始化 没有初始化 behavior = LOOKUP_NOCACHE|LOOKUP_INITIALIZE|LOOKUP_RESOLVER
      //发送给类的第一条消息通常是+new或+alloc或+self会初始化类
      */

    if (slowpath(!cls->isInitialized())) {
        behavior |= LOOKUP_NOCACHE;
    }
    //加锁防止多线程访问出现错乱
    runtimeLock.lock();
    checkIsKnownClass(cls);//是否注册类 是否被dyld加载的类
    //实现类包括实现isa走位中的父类和元类 //初始化类和父类
    cls = realizeAndInitializeIfNeeded_locked(inst, cls, behavior & LOOKUP_INITIALIZE);
    // runtimeLock may have been dropped but is now locked again
    runtimeLock.assertLocked();
    curClass = cls;
    //获取锁后,代码再次查找类的缓存,但绝大多数情况下,证据表明大部分时间都未命中,因此浪费时间。
    //唯一没有执行某种缓存查找的代码路径就是class_getInstanceMethod()。
    // unreasonableClassCount 类迭代上限 根据翻译来的
    for (unsigned attempts = unreasonableClassCount();;) {
        //判断是否有共享缓存缓存优化,一般是系统的方法比如NSLog,一般的方法不会走
        if (curClass->cache.isConstantOptimizedCache(/* strict */true)) {
#if CONFIG_USE_PREOPT_CACHES
            /*
               再一次查询共享缓存,目的可能在你查询过程中
               别的线程可能调用了这个方法共享缓存中有了直接去查询
            */
            imp = cache_getImp(curClass, sel);//缓存中根据sel查询imp
            //如果imp存在即缓存中有 跳转到done_unlock流程
            if (imp) goto done_unlock;
            //具体干啥不知道我感觉是获取父类的里面是进行的偏移
            curClass = curClass->cache.preoptFallbackClass();
#endif
        } else {
            // curClass method list.
            // 在curClass类中采用二分查找算法查找methodlist
            Method meth = getMethodNoSuper_nolock(curClass, sel);
            if (meth) {  // 如果找到了sel对应的方法
                imp = meth->imp(false);//获取对应的imp
                goto done;  //跳转到 done 流程
            }
            // curClass = curClass->getSuperclass() 直到为nil走if里面的流程,不为nil走下面流程
            if (slowpath((curClass = curClass->getSuperclass()) == nil)) {
                // No implementation found, and method resolver didn't help.
                // Use forwarding.
                //就是在循环里面没有找到对应的sel的方法,把定义息转发forward_imp赋值给imp
                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
        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.
            //如果父类返回的是forward_imp 停止查找,那么就跳出循环
            break;
        }
        if (fastpath(imp)) {
            // Found the method in a superclass. Cache it in this class.
            //如果缓存中有就跳转done流程
            goto done;
        }
    }

    
    // No implementation found. Try method resolver once.
    // 如果查询方法的没有实现,系统会尝试一次方法解析
    // behavior = 3 LOOKUP_RESOLVER = 2
    // behavior & LOOKUP_RESOLVER = 3 & 2 = 2 所以成立进入条件
    // 再次进入behavior = 1 & 2 = 0 不会在进入条件里面只执行一次动态方法决议
    if (slowpath(behavior & LOOKUP_RESOLVER)) {
        //behavior = behavior ^ LOOKUP_RESOLVER = 3 ^ 2 = 1
        behavior ^= LOOKUP_RESOLVER;
        //动态方法决议
        return resolveMethod_locked(inst, sel, cls, behavior);
    }

 done:
    //(behavior & LOOKUP_NOCACHE) == 0 成立  behavior = 3
    //LOOKUP_NOCACHE = 8 所以(behavior & LOOKUP_NOCACHE) = 0
    if (fastpath((behavior & LOOKUP_NOCACHE) == 0)) {
#if CONFIG_USE_PREOPT_CACHES
        while (cls->cache.isConstantOptimizedCache(/* strict */true)) {
            cls = cls->cache.preoptFallbackClass();
        }
#endif
        //将查询到的sel和imp插入到缓存 注意:插入的是当前类的缓存
        log_and_fill_cache(cls, imp, sel, inst, curClass);
    }
 done_unlock:
    //解锁
    runtimeLock.unlock();
    /*
      如果 (behavior & LOOKUP_NIL)成立则 behavior != LOOKUP_NIL
      且imp == forward_imp 没有查询到直接返回 nil
      */
    if (slowpath((behavior & LOOKUP_NIL) && imp == forward_imp)) {
        return nil;
    }
    return imp;
}

慢速查找流程

  • 是否注册类,如果没有直接报错
  • 是否实现 cls ,如果没有实现,则先去实现类以及相关的 isa走位链 isa继承链 中类的实现,目的是方法查找时到父类中去查询
  • 类是否初始化,如果没有则初始化,这一步我觉着是创建类对象比如调用类方法时,就是类对象调用实例方法

cls 开始遍历查询

  • 判断是否有共享缓存,目的是有可能在查过过程中这个方法被调用缓存了,如果有的话直接从缓存中取,没有共享缓存则开始到本类中查询
  • 在类中采用 二分查找算法 查找methodlist中的方法,如果找到插入缓存中,循环结束

父类缓存中查询

  • 如果父类中存在循环则终止查询,跳出循环
  • 此时 curClass = superclass ,到父类的缓存中找,如果找到则插入到本类的缓存中。如果父类中返回的是 forward_imp 则跳出遍历,执行消息转发
  • 如果本类中没有找到此时的 curClass = superclass 进入和 cls 类相同的查找流程进行遍历循环,直到 curClass = nil imp = forward_imp 进行消息转发

动态方法决议

  • 如果 cls 以及父类都没有查询到,此时系统会给你一次机会,判断是否执行过动态方法决议,如果没有则走动态方法决议
  • 如果动态决议方法执行过, imp = forward_imp 会走 done 流程插入缓存,会走 done_unlock 流程 return imp 进入消息转发阶段

实现和实例化类

Xnip2021-07-04_13-06-44.jpg

  • realizeClassMaybeSwiftAndLeaveLocked 方法中的 realizeClassWithoutSwift 就是去实现类的 isa走位链 继承链 中相关的类
  • initializeAndMaybeRelock initializeNonMetaClass 就是初始化类和父类的

二分法查找算法

Xnip2021-07-04_15-53-15.jpg Xnip2021-07-04_15-53-54.jpg Xnip2021-07-04_15-54-19.jpg Xnip2021-07-04_13-10-24.jpg 方法列表中的方法是经过修复的,意思就是按照 sel 大小进行过排序的

  • 二分法查找算法其实就是每次找到范围内的中间位置和 keyValue 比较,如果相等直接返回查找到的方法(当然如果有分类方法就返回分类方法)
  • 如果不相等则继续二分法查询,不断缩小查询的范围,如果最后还是没有查询到则返回nil

cache_getImp

方法快速查找流程是汇编源码实现的

Xnip2021-07-04_13-12-34.jpg

GetClassFromIsa_p16 宏定义和我们开始在本类中查询缓存方法一样,但是参数不一样 needs_auth = 0

Xnip2021-07-04_13-14-06.jpg

直接走 needs_auth==0 p0=curClass 。把 p0 寄存器的值赋值给 p16 寄存器,p16= curClass CacheLookup 方法前面已经探究过了在这不细说了 CacheLookup GETIMP , _cache_getImp , LGetImpMissDynamic , LGetImpMissConstant

如果缓存没有命中走 LGetImpMissDynamic 流程 如果缓存命中 Mode = GETIMP

Xnip2021-07-04_13-14-53.jpg

LGetImpMissDynamic 流程 p0 = 0,就是没有查到缓存就是返回 imp = nil

Xnip2021-07-04_13-16-02.jpg

p17 = imp,把p17寄存器的值赋值给p0寄存器,x0 = p0 = imp 如果imp=0直接跳转9流程 return 0 AuthAndResignAsIMP也是一个宏,对imp进行解码,拿到解码后的imp返回

Xnip2021-07-04_13-17-47.jpg

实例演示

1.实例方法测试 创建一个 LGPerson

Xnip2021-07-04_16-25-02.jpg

创建一个LGStudent类 继承 LGPerson

Xnip2021-07-04_16-25-47.jpg

main函数里面调用方法 崩溃

Xnip2021-07-04_16-28-23.jpg

unrecognized经典的崩溃信息,[student sayHello]也走了快速查找流程,慢速查找流程,动态方法决议,最后消息转发,最后还是没找到报unrecognized,全局搜索doesNotRecognizeSelector或者unrecognized selector sent to instance,在源码中搜索

Xnip2021-07-04_16-32-28.jpg

Xnip2021-07-04_16-32-41.jpg

下面 创建有个NSObject+LGCate分类里面添加一个对象方法sayEasy,并且只在分类中实现

Xnip2021-07-04_16-38-07.jpgmain函数里面调用方法 Xnip2021-07-04_16-37-37.jpg

类调用对象方法调用成功了原因是什么,在oc底层没有所谓的实例方法和类方法,获取一个类方法实际上就是获取元类的实例方法,没找到找到根源类,根源类也没有,最后找到NSObject所以可以找到sayEasy方法