在上一篇文章 底层原理(07)中,剖析了办法的快速查找流程即缓存查找,假如缓存中没有找到就会。。。下面就会进入办法慢速查找流程。。。

一. __objc_msgSend_uncached

在当时类中,缓存查找流程中假如没有到方针办法,跳转 MissLabelDynamic 流程, MissLabelDynamic = __objc_msgSend_uncached,查找__objc_msgSend_uncached 并找到进口,真机汇编 代码如下:

.endmacro
    STATIC_ENTRY __objc_msgSend_uncached
    UNWIND __objc_msgSend_uncached, FrameWithNoSaves
    // THIS IS NOT A CALLABLE C FUNCTION
    // Out-of-band p15 is the class to search
    MethodTableLookup
    TailCallFunctionPointer x17
    END_ENTRY __objc_msgSend_uncached

这儿 TailCallFunctionPointer 接收个参数 x17TailCallFunctionPointer 里做了什么? 找到它:

.macro TailCallFunctionPointer
    // $0 = 函数指针的值
    // $0 = function pointer value
    br $0
.endmacro

TailCallFunctionPointer就一行汇编代码 br $0.由于 $0 = p17br $0 的意思读取 p17 寄存器中的地址并且跳转到该地址,由于咱们是在查询办法便是依据 selimp,在依据TailCallFunctionPointer 提示。猜想 p17寄存器应该存的是imp

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

.macro MethodTableLookup
    SAVE_REGS MSGSEND
    // lookUpImpOrForward(obj, sel, cls, LOOKUP_INITIALIZE | LOOKUP_RESOLVER)
    // receiver and selector already in x0 and x1
    // x16(class) 赋值给 x2
    mov x2, x16
    // x3 = 3
    mov x3, #3
    // bl:b:跳转 l:链接寄存器 
    // 在跳转到_lookUpImpOrForward之前,
    // 将下一条指令的地址保存到 lr寄存器中,既将(mov x17, x0)的指令地址保存在lr中 
    // 当_lookUpImpOrForwar履行完以后,履行lr寄存器中的地址,即:mov x17, x0
    bl _lookUpImpOrForward
    // IMP in x0
    // x0 中保存了 _lookUpImpOrForward 的回来值,即:找到的imp
    mov x17, x0
    RESTORE_REGS MSGSEND
.endmacro

经过_lookUpImpOrForward 查询到imp,将imp赋值给x17寄存器。

到这儿现已对 __objc_msgSend_uncached 流程有了大概的了解,即:

  • __objc_msgSend_uncached –>
  • MethodTableLookup –>
  • _lookUpImpOrForward(回来imp) –>
  • TailCallFunctionPointer x17 (x17 = imp) –>
  • br $0 调用imp

二. lookUpImpOrForward

2.1 lookUpImpOrForward 源码剖析

上面知道,_lookUpImpOrForward 回来了 imp。大局查找_lookUpImpOrForward,发现汇编里面没有_lookUpImpOrForward的定义或者完成,大局查找 lookUpImpOrForward 找到了 c++ 代码:

// behavior = 3
IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior)
{
    // 音讯转发 imp
  const IMP forward_imp = (IMP)_objc_msgForward_impcache;
  IMP imp = nil;
  Class curClass;
  runtimeLock.assertUnlocked();
    // 判别类是否已初始化 
    // 发送给类的第一条音讯通常是+new或+alloc或+self会初始化类 
  if (slowpath(!cls->isInitialized())) {
        // LOOKUP_NOCACHE = 8
        // 没有初始化 behavior = 3|8 = 11
    behavior |= LOOKUP_NOCACHE;
  }
    // 加锁防止多线程访问出现错乱
  runtimeLock.lock();
    // 是否注册类 是否被dyld加载的类
  checkIsKnownClass(cls);
    // 完成类包括完成isa走位中的父类和元类 
    // 初始化类和父类
  cls = realizeAndInitializeIfNeeded_locked(inst, cls, behavior & LOOKUP_INITIALIZE);
  runtimeLock.assertLocked();
  curClass = cls;
    // 获取锁后,代码再次查找类的缓存,但绝大多数情况下,证据表明大部分时间都未射中,因此浪费时间。
    // 唯一没有履行某种缓存查找的代码路径便是class_getInstanceMethod()。
  for (unsigned attempts = unreasonableClassCount();;) {
        // 判别是否有同享缓存缓存优化,一般是系统的办法比方NSLog,一般的办法不会走
    if (curClass->cache.isConstantOptimizedCache(/* strict */true)) {
#if CONFIG_USE_PREOPT_CACHES
            // 再一次查询同享缓存,由于可能在你查询过程中
            // 别的线程调用了这个办法,同享缓存中有了直接去查询
      imp = cache_getImp(curClass, sel);
            // 假如imp存在即缓存中有 跳转到done_unlock流程
      if (imp) goto done_unlock;
      curClass = curClass->cache.preoptFallbackClass();
#endif
    } else {
      // curClass method list.
            // 在curClass类中选用二分查找算法查找methodlist
      method_t *meth = getMethodNoSuper_nolock(curClass, sel);
            // 假如找到了sel对应的办法
      if (meth) {
                // 获取对应的imp
        imp = meth->imp(false);
                // 跳转到 done 流程
        goto done;
      }
            // 直到 superCls 为nil,走if里面的流程:imp = forward_imp;
      if (slowpath((curClass = curClass->getSuperclass()) == nil)) {
                // 没找到办法完成,使用 forward_imp
        imp = forward_imp;
        break;
      }
    }
        // 假如父类中存在循环则中止
    if (slowpath(--attempts == 0)) {
      _objc_fatal("Memory corruption in class list.");
    }
    // Superclass cache.
        // 去父类的缓存中查找imp
    imp = cache_getImp(curClass, sel);
        // 假如父类回来的是forward_imp 中止查找,那么就跳出循环
    if (slowpath(imp == forward_imp)) {
      break;
    }
        // 假如缓存中有就跳转done流程
    if (fastpath(imp)) {
      goto done;
    }
  }
    // 假如查询办法的没有完成,系统会尝试一次办法解析
    // behavior = 3,LOOKUP_RESOLVER = 2; 
    // 3 & 2 = 2,条件建立,进入 if {}
  if (slowpath(behavior & LOOKUP_RESOLVER)) {
        // behavior = behavior ^ LOOKUP_RESOLVER = 3 ^ 2 = 1
    behavior ^= LOOKUP_RESOLVER;
        // 动态办法抉择
    return resolveMethod_locked(inst, sel, cls, behavior);
  }
done:
    // behavior = 3,LOOKUP_NOCACHE = 8;0011 & 1000 = 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;
}

2.2 慢速查找流程总结

  • 是否注册类,假如没有直接报错
  • 是否完成cls,假如没有完成,则先去完成以及相关的isa走位链isa承继链中类的完成,意图是办法查找时到父类中去查询
  • 类是否初始化,假如没有则初始化,这一步我觉着是创立类方针比方调用类办法时,便是类方针调用实例办法

cls开端遍历查询

  • 判别是否有同享缓存,意图是有可能在查过过程中这个办法被调用缓存了,假如有的话直接从缓存中取,没有同享缓存则开端到本类中查询
  • 在类中选用二分查找算法查找methodlist中的办法,假如找到刺进缓存中,循环结束

父类缓存中查询

  • 假如父类中存在循环则终止查询,跳出循环
  • 此时curClass = superclass,到父类的缓存中找,假如找到则刺进到本类的缓存中。假如父类中回来的是forward_imp则跳出遍历,履行音讯转发
  • 假如本类中没有找到此时的curClass = superclass进入和cls类相同的查找流程进行遍历循环,直到 curClass = nilimp = forward_imp进行音讯转发

动态办法抉择

  • 假如cls以及父类都没有查询到,此时系统会给你一次机会,判别是否履行过动态办法抉择,假如没有则走动态办法抉择
  • 假如动态抉择办法履行过,imp = forward_imp会走done流程刺进缓存,会走done_unlock流程 return imp进入音讯转发阶段

lookUpImpOrForward流程图

OC底层原理(08)imp慢速查找

2.3 realizeAndInitializeIfNeeded_locked 完成和实例化

static Class
realizeAndInitializeIfNeeded_locked(id inst, Class cls, bool initialize) 
{
    runtimeLock.assertLocked(); 
    // !cls->isRealized()小概率产生 cls->isRealized()大概率是YES 
    // 判别类是否完成 意图是完成isa走位图中的isa走位链和父类链 
    if (slowpath(!cls->isRealized())) { 
        cls = realizeClassMaybeSwiftAndLeaveLocked(cls, runtimeLock); 
        // runtimeLock may have been dropped but is now locked again 
    } 
    // 类是否初始化 没有先去初始化 
    if (slowpath(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 
    }
    return cls; 
}
  • realizeClassMaybeSwiftAndLeaveLocked办法中的realizeClassWithoutSwift便是去完成类的isa走位链和承继链中相关的类

  • initializeAndMaybeRelockinitializeNonMetaClass便是初始化类和父类的

2.4 分法查找算法

ALWAYS_INLINE static method_t *
findMethodInSortedMethodList(SEL key, const method_list_t *list, const getNameFunc &getName)
{
    ASSERT(list);
    auto first = list->begin();  //第一个method的方位
    auto base = first;
    decltype(first) probe;       //相当于mid
    //把key直接转换成uintptr_t 由于修正过后的method_list_t中的元素是排过序的
    uintptr_t keyValue = (uintptr_t)key;
    uint32_t count;
    //count = 数组的个数 (count >>= 1 = count = count >> 1)
    //count >> 1 就相当于(count/2) 取整
    //1.假如 count = list->count = 8   //2 count =  7 >> 1 = 3
    for (count = list->count; count != 0; count >>= 1) {
        /*
          1. 首地址 + (下标) //地址偏移 中心值 probe = base + 4
          2. 中心值  probe = base(首地址)+ 6
         */
        probe = base + (count >> 1);
        //获取中心的sel的值也是强转后的值
        uintptr_t probeValue = (uintptr_t)getName(probe);
        if (keyValue == probeValue) {// 假如 方针key ==  中心方位的key 匹配成功
            //分类覆盖,分类中有相同姓名的办法,假如有分类的办法咱们就获取分类的办法,多个分类看编译的次序
            while (probe > first && keyValue == (uintptr_t)getName((probe - 1))) {
                probe--;
            }
            //回来办法的地址
            return &*probe;
        }
        //假如 keyValue > 中心的方位的值
        if (keyValue > probeValue) {
            /*
               1.base = probe + 1 =  4 + 1 = base(首地址) + 5 向上移 一位
               2.base = probe + 1 ;向上移 一位
             */
            base = probe + 1;
            // 8 -1 = 7 由于比过一次没中 然后循环
            count--;
        }
    }
    return nil;//查询完没找到回来nil
}

办法列表中的办法是经过修正的,意思便是按照sel大小进行过排序的

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

2.5 cache_getImp

快速查找流程是汇编完成的

STATIC_ENTRY _cache_getImp
// 获取 isa 放到 p16,这儿 needs_auth = 0
GetClassFromIsa_p16 p0, 0
// 查找,留意这儿 Mode = GETIMP,
CacheLookup GETIMP, _cache_getImp, LGetImpMissDynamic, LGetImpMissConstant 
// 缓存未射中,回来0
LGetImpMissDynamic: 
mov p0, #0 
ret
END_ENTRY _cache_getImp
.macro GetClassFromIsa_p16 src, needs_auth, auth_address
/* note: auth_address is not required if !needs_auth */
#if SUPPORT_INDEXED_ISA  // armv7k or arm64_32
	...//省略
1:
#elif __LP64__
.if \needs_auth == 0 // _cache_getImp takes an authed class already
	mov	p16, \src
.else //needs_auth = 1 所以走下面的流程
	// 64-bit packed isa
        //把 \src 和 \auth_address 传进ExtractISA 得到的结果赋值给p16寄存器
	ExtractISA p16, \src, \auth_address  
.endif
#else
	// 32-bit raw isa
	mov	p16, \src
#endif

直接走needs_auth==0p0=curClass。把p0寄存器的值赋值给p16寄存器,p16= curClass

CacheLookup办法前面现已探究过了在这不细说了CacheLookup GETIMP, _cache_getImp, LGetImpMissDynamic, LGetImpMissConstant

  • 假如缓存没有射中走 LGetImpMissDynamic流程
  • 假如缓存射中 Mode = GETIMP
.macro CacheHit
.if $0 == NORMAL 
    TailCallCachedImp x17, x10, x1, x16 // authenticate and call imp
.elseif $0 == GETIMP 
    mov p0, p17
    cbz p0, 9f //假如imp = 0直接跳转9流程 return 0
    AuthAndResignAsIMP x0, x10, x1, x16 // authenticate imp and re-sign as IMP 9: 
    ret
  • p17=imp,把p17寄存器的值赋值给p0寄存器,x0=p0=imp
  • 假如imp=0直接跳转9流程return 0
  • AuthAndResignAsIMP也是一个,对imp进行解码,拿到解码后的imp回来
.macro AuthAndResignAsIMP
    // $0 = cache imp , $1 = buckets的地址, $2 = SEL $3 = class 
    // $0 = $0 ^ $3 = imp ^ class = 解码后的imp
    eor $0, $0, $3 
.endmacro

缓存中获取imp是编码过的,此时imp^class= 解码后的imp

三 实例查找

创立一个 YJPerson 类,声明个实例办法 say1,不完成 say1,然后调用:

OC底层原理(08)imp慢速查找

奔溃:

OC底层原理(08)imp慢速查找

unrecognized经典的溃散信息,[perosn say1]也走了快速查找流程,慢速查找流程,动态办法抉择,最终音讯转发,最终还是没找到报unrecognized,大局查找doesNotRecognizeSelector或者unrecognized selector sent to instance,在源码中查找:

// Replaced by CF (throws an NSException)
+ (void)doesNotRecognizeSelector:(SEL)sel {
  _objc_fatal("+[%s %s]: unrecognized selector sent to instance %p",
        class_getName(self), sel_getName(sel), self);
}
// Replaced by CF (throws an NSException)
- (void)doesNotRecognizeSelector:(SEL)sel {
  _objc_fatal("-[%s %s]: unrecognized selector sent to instance %p",
        object_getClassName(self), sel_getName(sel), self);
}

最终怎么调用doesNotRecognizeSelector 后面在 音讯转发华章 会做详细的说明

分类测验 创立有个 NSObject+YJ 分类里面添加一个方针办法 testMethod,并在分类中完成

OC底层原理(08)imp慢速查找

YJPerson 类调用方针办法调用成功了,为什么?由于在 OC 底层没有所谓的方针办法和类办法之分,对于底层来说都是方针办法;获取一个类办法实际上便是获取元类的方针办法:

Method class_getClassMethod(Class cls, SEL sel)
{
  if (!cls|| !sel) return nil;
    // 回来元类的实例办法
  return class_getInstanceMethod(cls->getMeta(), sel);
}

这儿查找流程:

  • YJPerson类 没有 –>
  • YJPerson元类 没有 –>
  • NSObject根元类 没有 –>
  • NSObject根类 –> 找到了