iOS-14.方法查找流程之慢速查找流程分析

485 阅读6分钟

ios底层文章汇总

1 引入

iOS方法调用,底层首先会查找cache进行快速查找imp

查找cache进行快速查找imp的具体流程逻辑请参考:iOS-13.方法查找流程之快速查找流程

如果快速查找cache没有找到匹配的imp,则会跳转到__objc_msgSend_uncached,进入慢速的方法列表查询 MethodTableLookup

STATIC_ENTRY __objc_msgSend_uncached
UNWIND __objc_msgSend_uncached, FrameWithNoSaves

MethodTableLookup // 查询方法列表
TailCallFunctionPointer x17

END_ENTRY __objc_msgSend_uncached

MethodTableLookup是个宏,具体定义如下:

.macro MethodTableLookup
	
	// push frame
	SignLR
	stp	fp, lr, [sp, #-16]!
	mov	fp, sp

	// save parameter registers: x0..x8, q0..q7
	sub	sp, sp, #(10*8 + 8*16)
	stp	q0, q1, [sp, #(0*16)]
	stp	q2, q3, [sp, #(2*16)]
	stp	q4, q5, [sp, #(4*16)]
	stp	q6, q7, [sp, #(6*16)]
	stp	x0, x1, [sp, #(8*16+0*8)]
	stp	x2, x3, [sp, #(8*16+2*8)]
	stp	x4, x5, [sp, #(8*16+4*8)]
	stp	x6, x7, [sp, #(8*16+6*8)]
	str	x8,     [sp, #(8*16+8*8)]

	// lookUpImpOrForward(obj, sel, cls, LOOKUP_INITIALIZE | LOOKUP_RESOLVER)
	// receiver and selector already in x0 and x1
	mov	x2, x16
	mov	x3, #3
	bl	_lookUpImpOrForward

	// IMP in x0
	mov	x17, x0
	
	// restore registers and return
	ldp	q0, q1, [sp, #(0*16)]
	ldp	q2, q3, [sp, #(2*16)]
	ldp	q4, q5, [sp, #(4*16)]
	ldp	q6, q7, [sp, #(6*16)]
	ldp	x0, x1, [sp, #(8*16+0*8)]
	ldp	x2, x3, [sp, #(8*16+2*8)]
	ldp	x4, x5, [sp, #(8*16+4*8)]
	ldp	x6, x7, [sp, #(8*16+6*8)]
	ldr	x8,     [sp, #(8*16+8*8)]

	mov	sp, fp
	ldp	fp, lr, [sp], #16
	AuthenticateLR

.endmacro

MethodTableLookup的宏定义可知,主要是进行了一系列的寄存器操作,然后跳转到_lookUpImpOrForward ,但全局搜索_lookUpImpOrForward未找到其定义,猜想底层从汇编跳转到C源码部分

注:从汇编宏跳到C函数,函数名少一个下划线前缀

从C函数调用汇编宏,名称会多一个下划线前缀

全局搜索lookUpImpOrForward 果然找到了函数的定义

2 验证程序跳转流程

  • main.m设置断点,打开Debug->Debug Workflow->Always Show Disassemble
  • 运行程序后,执行到断点处,按住control键+点击step into,多点击step into直到进入_objc_msgSend_uncached,可见一堆的寄存器处理后,进行了callq
  • 程序确实进入lookUpImpOrForward at objc-runtime-new.mm:6099

让编译忽略错误 image.png

3 lookUpImpOrForward慢速查找逻辑

3.1 lookUpImpOrForward源码

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();

    // 多线程可能更新了cache 所以进入慢速查找前会先进入快速查找
    if (fastpath(behavior & LOOKUP_CACHE)) {
        /*
         cache_getImp将调用汇编:
         ```
         STATIC_ENTRY _cache_getImp

         GetClassFromIsa_p16 p0
         CacheLookup GETIMP, _cache_getImp

         LGetImpMiss:
         mov    p0, #0
         ret

         END_ENTRY _cache_getImp
         ```
         注意CacheLookup GETIMP查找失败后,跳转到LGetImpMiss,将寄存器清零直接返回,不会再次进入慢速查找
         */
        imp = cache_getImp(cls, sel);
        if (imp) goto done_nolock;
  
    }
    
    //加锁,目的是保证读取的线程安全
    runtimeLock.lock();
    
    //判断是否是一个已知的类:判断当前类是否是已经被认可的类,即已经加载的类
    checkIsKnownClass(cls); 
    
    //判断类是否实现,如果没有,需要先实现,此时的目的是为了确定父类链,方法后续的循环
    if (slowpath(!cls->isRealized())) { 
        cls = realizeClassMaybeSwiftAndLeaveLocked(cls, runtimeLock);
    }

    //判断类是否初始化,如果没有,需要先初始化
    if (slowpath((behavior & LOOKUP_INITIALIZE) && !cls->isInitialized())) { 
        cls = initializeAndLeaveLocked(cls, inst, runtimeLock);
    }

    runtimeLock.assertLocked();
    curClass = cls;

    //查找类的缓存
    
    // unreasonableClassCount 最大循环次数
    for (unsigned attempts = unreasonableClassCount();;) { 
        //当前类方法列表(采用二分查找算法),如果找到,则返回,将方法缓存到cache中
        Method meth = getMethodNoSuper_nolock(curClass, sel);
        if (meth) {
            imp = meth->imp;
            goto done;
        }
        //赋值当前类 = 当前类的父类,并判断父类是否为nil
        if (slowpath((curClass = curClass->superclass) == nil)) {
            //未找到方法实现,方法解析器也不行,使用转发
            imp = forward_imp;
            break;
        }

        // 如果attempts==0,则停止
        if (slowpath(--attempts == 0)) {
            _objc_fatal("Memory corruption in class list.");
        }

        // 父类缓存 此时的curClass已在前面代码赋值为当前类的父类--cache_getImp进入汇编查找,如果没查找到,直接返回,不会进入lookUpImpOrForward
        imp = cache_getImp(curClass, sel);
        if (slowpath(imp == forward_imp)) { 
            // 到继承链找到nil后未找到,则将imp = forward_imp 跳出循环
            break;
        }
        if (fastpath(imp)) {
            //如果在父类中,找到了此方法,将其存储到cache中
            goto done;
        }
    }

    //没有找到方法实现,尝试一次动态方法解析
    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;
}

3.2 lookUpImpOrForward流程逻辑

image.png

  • 1)定义的消息转发forward_imp
const IMP forward_imp = (IMP)_objc_msgForward_impcache;
  • 2)进入慢速查找后,多线程可能更新了cache 所以进入慢速查找前会先进入快速查找

  • 3)cache_getImp将调用汇编 执行 CacheLookup GETIMP, _cache_getImp

注意CacheLookup GETIMP查找失败后,跳转到LGetImpMiss,将寄存器清零直接返回,不会再次进入慢速查找

  • 4)进入循环查找流程是

    • 当前类方法列表
    • 将当前类 赋值为当前类的父类进行迭代(判断父类是否为nil,为nil就 imp = forward_imp消息转发,退出循环)
    • 如果--attempts==0,则停止循环
    • 查找快速父类缓存,在继承链的父类中如果找到了此方法,跳出循环,将方法存储到cache中
  • 5)没有找到方法实现,尝试一次动态方法解析,并返回动态方法解析的结果

  • 6)找到了方法实现,则将方法写入到缓存log_and_fill_cache --> cache_fill

3.3 在当前类方法列表中查找imp(采用二分查找算法): findMethodInSortedMethodList

getMethodNoSuper_nolock --> 循环methods调用search_method_list_inline(*mlists, sel) --> findMethodInSortedMethodList

ALWAYS_INLINE static method_t *
findMethodInSortedMethodList(SEL key, const method_list_t *list)
{
   ASSERT(list);

   const method_t * const first = &list->first;
   const method_t *base = first;
   const method_t *probe;
   uintptr_t keyValue = (uintptr_t)key;
   uint32_t count;
   
   /**
    二分查找
    list 递增存储的
    */
   
   for (count = list->count; count != 0; count >>= 1) {
       probe = base + (count >> 1);
       
       uintptr_t probeValue = (uintptr_t)probe->name;
       
       if (keyValue == probeValue) {
           // list中可能存在同名方法,方法的存储顺序是: 分类 - 类
           while (probe > first && keyValue == (uintptr_t)probe[-1].name) {
          
            //排除分类重名方法(方法的存储是先存储类方法,在存储分类---按照先进后出的原则,分类方法最先出,而我们要取的类方法,所以需要先排除分类方法)
            //如果是两个分类,就看谁先进行加载
               probe--;
           }
           return (method_t *)probe;
       }
       
       if (keyValue > probeValue) {
           base = probe + 1;
           count--;
       }
   }
   
   return nil;
}

2251862-3e3b988e3e02b757.png

3.4 总结

1)对象方法查找测试

  • 自己有,就找到后返回;
  • 自己没有,找父类,父类找到后返回;
  • 自己没有,父类没有,找父类的父类,找到后返回;
  • 沿着继承链找父类,直到NSObject,找到就返回;
  • 如果继承链中都没有找到,崩溃

2)类方法查找测试

  • 自己有,就找到后返回;
  • 自己没有,找父类,父类找到后返回;
  • 自己没有,父类没有,找父类的父类,找到后返回;
  • 沿着继承链找父类,直到NSObject,找到就返回;
  • 沿着继承链找父类,直到NSObject都没有找到,找NSObject的对象方法,找到后返回;
  • 沿着继承链找父类,直到NSObject都没有找到,找NSObject的对象方法,也没有找到,则崩溃

4 cache_getImp解析

快速查找失败,未找到imp后,为了规避多线程的影响,重新刷新了类的cache,在进入C代码的 lookUpImpOrForward中后还进行了一次快速查找cache中的imp:imp = cache_getImp(cls, sel);

cache_getImp将进入汇编代码,_cache_getImp,源码如下,从源码可知核心为CacheLookup GETIMP, _cache_getImp进入CacheLookup宏(进入CacheLookup宏参考:iOS-13.方法查找流程之快速查找流程)

	STATIC_ENTRY _cache_getImp

	GetClassFromIsa_p16 p0
	CacheLookup GETIMP, _cache_getImp

LGetImpMiss:
	mov	p0, #0
	ret

	END_ENTRY _cache_getImp

CacheLookup GETIMP,找到imp则直接返回imp,如果查找失败后,跳转LGetImpMiss,将寄存器清零直接返回,不会再次进入慢速查找

5 _objc_msgForward_impcache

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_msgForward_impcache--> __objc_msgForward-->_objc_forward_handler进入C语言函数
  • void *_objc_forward_handler = (void*)objc_defaultForwardHandler
  • objc_defaultForwardHandler函数实现
// Default forward handler halts the process.
__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);
}

动态方法决议,请关注后续更新:iOS-15.objc_msgSend动态方法决议和消息转发