OC底层原理objc_msgSend之方法的慢速查找

235 阅读11分钟

前言:

前文已经分析了方法的快速查流程,但是当方法没有缓存的时候,会执行哪些函数呢,带着这个问题,开始今天的探索。

通过前文的分析,已经知道,在方法缓存查找失败后,会执行__objc_msgSend_uncached,先回顾一下__objc_msgSend_uncached的汇编源码:

    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 ,参数为imp
    TailCallFunctionPointer x17

    END_ENTRY __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
	mov	x2, x16
	mov	x3, #3
	bl	_lookUpImpOrForward
        
        // 将lookUpImpOrForward的返回值imp 给x17
	// IMP in x0
	mov	x17, x0

	RESTORE_REGS MSGSEND

.endmacro

TailCallFunctionPointer解析

//执行imp
.macro TailCallFunctionPointer
	// $0 = function pointer value
	br	$0
.endmacro

通过源码我们可以知道,在MethodTableLookup会调用lookUpImpOrForward函数并返回imp,全局搜索lookUpImpOrForward,在objc-runtime-new.mm文件找到了实现代码如下:

1.lookUpImpOrForward解析:

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();
    //判断cls是否加载,如果为真,behavior |= LOOKUP_NOCACHE;behavior = 11
    if (slowpath(!cls->isInitialized())) {
        // The first message sent to a class is often +new or +alloc, or +self
        // which goes through objc_opt_* or various optimized entry points.
        //
        // However, the class isn't realized/initialized yet at this point,
        // and the optimized entry points fall down through objc_msgSend,
        // which ends up here.
        //
        // We really want to avoid caching these, as it can cause IMP caches
        // to be made with a single entry forever.
        //
        // Note that this check is racy as several threads might try to
        // message a given class for the first time at the same time,
        // in which case we might cache anyway.
        behavior |= LOOKUP_NOCACHE;
    }

    // runtimeLock is held during isRealized and isInitialized checking
    // to prevent races against concurrent realization.

    // runtimeLock is held during method search to make
    // method-lookup + cache-fill atomic with respect to method addition.
    // Otherwise, a category could be added but ignored indefinitely because
    // the cache was re-filled with the old value after the cache flush on
    // behalf of the category.

    runtimeLock.lock();

    // We don't want people to be able to craft a binary blob that looks like
    // a class but really isn't one and do a CFI attack.
    //
    // To make these harder we want to make sure this is a class that was
    // either built into the binary or legitimately registered through
    // objc_duplicateClass, objc_initializeClassPair or objc_allocateClassPair.
    //检查类是否已经注册
    checkIsKnownClass(cls);
    //懒加载类,加载类,并递归加载父类,元类,调用initialize方法
    cls = realizeAndInitializeIfNeeded_locked(inst, cls, behavior & LOOKUP_INITIALIZE);
    // runtimeLock may have been dropped but is now locked again
    runtimeLock.assertLocked();
    curClass = cls;

    // The code used to lookup the class's cache again right after
    // we take the lock but for the vast majority of the cases
    // evidence shows this is a miss most of the time, hence a time loss.
    //
    // The only codepath calling into this without having performed some
    // kind of cache lookup is class_getInstanceMethod().
    //unreasonableClassCount 根据函数的注释翻译为: 为类的任何迭代提供上限,在运行时元数据损坏时防止旋转。
    // 死循环,根据,goto,break退出循环
    for (unsigned attempts = unreasonableClassCount();;) {
        //判断是否有共享缓存优化
        if (curClass->cache.isConstantOptimizedCache(/* strict */true)) {
#if CONFIG_USE_PREOPT_CACHES //判断是否支持共享缓存,真机支持
            //查询共享缓存,如果在查找这个方法时,其它线程已经调用并存储了这个方法,共享缓存中就存在了
            //通过sel在curClass的缓存中查找imp
            imp = cache_getImp(curClass, sel);
            //如果查询到imp,跳转到 done_unlock流程
            if (imp) goto done_unlock;
            curClass = curClass->cache.preoptFallbackClass();
#endif
        } else {
            //在curClass方法列表中,根据sel查找Method
            // curClass method list.
            Method meth = getMethodNoSuper_nolock(curClass, sel);
            //如果meth存在取出imp,并跳转到 done
            if (meth) {
                imp = meth->imp(false);
                goto done;
            }
            //在本类的方法列表未查找到,将curClass赋值为父类并判断是否是否存在
            if (slowpath((curClass = curClass->getSuperclass()) == nil)) {
                // No implementation found, and method resolver didn't help.
                // Use forwarding.
                // 按照继承链(cls->supercls->nil)一直查找到nil都没有查找到sel对应的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.
        //根据sel在父类的缓存中查找imp
        imp = cache_getImp(curClass, sel);
        //判断从父类查找到的imp是否为forward_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;
        }
        //如果imp存在并不为forward_imp,跳转到done
        if (fastpath(imp)) {
            // Found the method in a superclass. Cache it in this class.
            goto done;
        }
    }

    // No implementation found. Try method resolver once.
    // 执行方法动态决议,behavior传值为3,如果!cls->isInitialized()未YES,behavior = 11,LOOKUP_RESOLVER = 2
    // 3 & 2 = 1,8 & 2 = 1,第一次进入时为YES
    if (slowpath(behavior & LOOKUP_RESOLVER)) {
        //3 ^ 2 = 1,11 ^ 2 = 9, 1,9 再次 & 2 都为0,所以此方法只会执行一次
        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
        // 查找到imp,将sel,imp存储到cls的cache,即调用cache_t::insert函数,之前在类的结构中已分析过此函数
        log_and_fill_cache(cls, imp, sel, inst, curClass);
    }
 done_unlock:
    runtimeLock.unlock();
    //imp为找到,并且imp == forward_imp 返回nil
    if (slowpath((behavior & LOOKUP_NIL) && imp == forward_imp)) {
        return nil;
    }
    return imp;
}

慢速查找流程解析:

  • 指定消息转发的imp forward_imp_objc_msgForward_impcache
  • !cls->isInitialized()判断类是否已加载。
  • checkIsKnownClass(cls);检查类是否已经注册。
  • realizeAndInitializeIfNeeded_locked懒加载类,加载类,并递归加载父类,元类,调用initialize方法。 循环查找
  • curClass->cache.isConstantOptimizedCache判断是否有共享缓存优化
  • imp = cache_getImp(curClass, sel)通过sel在curClass的缓存中查找imp,如果查询到imp,跳转到 done_unlock流程。
  • Method meth = getMethodNoSuper_nolock(curClass, sel);使用二分查找法在curClass方法列表中,根据sel查找Method,如果meth存在取出imp,并跳转到 done。
  • curClass = curClass->getSuperclass()) == nil 在本类的方法列表未查找到,将curClass赋值未父类并判断是否是否存在,如果判断父类为nil依然未找到 imp,并且动态方法决议依然未找到 imp,开始方法转发。
  • imp = cache_getImp(curClass, sel);根据sel在父类的缓存中查找imp,如果查找到的 impforward_imp则跳出循环,如果不是,跳转到done
  • if (slowpath(behavior & LOOKUP_RESOLVER))判断是否执行动态方法决议,此函数只会执行一次
  • log_and_fill_cache查找到imp,将sel,imp存储到cls的cache,即调用cache_t::insert函数,OC底层原理之类的结构分析已经分析过此函数。

1.1realizeAndInitializeIfNeeded_locked函数解析:

static Class
realizeAndInitializeIfNeeded_locked(id inst, Class cls, bool initialize)
{
    runtimeLock.assertLocked();
    //首次给懒加载类发消息时调用
    if (slowpath(!cls->isRealized())) {
        cls = realizeClassMaybeSwiftAndLeaveLocked(cls, runtimeLock);
        // runtimeLock may have been dropped but is now locked again
    }
    
    if (slowpath(initialize && !cls->isInitialized())) {
    //判断是否需要调用initialize方法
        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 加载类并递归加载元类,父类
  • initializeAndLeaveLocked调用initialize

1.2cache_getImp函数解析:

//参数:p0 = curClass 
	STATIC_ENTRY _cache_getImp
        // 参数 src = curClass needs_auth = 0,此时将curClass给到p16
	GetClassFromIsa_p16 p0, 0
        // 重新进入CacheLookup查找imp
	CacheLookup GETIMP, _cache_getImp, LGetImpMissDynamic, LGetImpMissConstant
// 查询imp失败执行
// 置空p0并返回
LGetImpMissDynamic:
	mov	p0, #0
	ret

LGetImpMissConstant:
	mov	p0, p2
	ret

	END_ENTRY _cache_getImp

CacheHit:解析:


// CacheHit: x17 = cached IMP, x10 = address of buckets, x1 = SEL, x16 = isa
// 此时$0 = GETIMP
.macro CacheHit
.if $0 == NORMAL
	TailCallCachedImp x17, x10, x1, x16	// authenticate and call imp
.elseif $0 == GETIMP
        // 将p17(imp)赋值给p0,p0寄存器返回值
	mov	p0, p17
        // p0和0进行比较,为0则跳转 9 直接返回p0
	cbz	p0, 9f			// don't ptrauth a nil imp
        // 将查询到的imp进行解码
	AuthAndResignAsIMP x0, x10, x1, x16	// authenticate imp and re-sign as IMP
9:	ret				// return IMP
.elseif $0 == LOOKUP
	// No nil check for ptrauth: the caller would crash anyway when they
	// jump to a nil IMP. We don't care if that jump also fails ptrauth.
	AuthAndResignAsIMP x17, x10, x1, x16	// authenticate imp and re-sign as IMP
	cmp	x16, x15
	cinc	x16, x16, ne			// x16 += 1 when x15 != x16 (for instrumentation ; fallback to the parent class)
	ret				// return imp via x17
.else
.abort oops
.endif
.endmacro
  • 此时$0 = GETIMP,执行.elseif $0 == GETIMP分支,mov p0, p17将p17(imp)赋值给p0,p0寄存器返回值
  • cbz p0, 9f判断 p0是否为空,为空则直接返回
  • AuthAndResignAsIMP 将查询到的imp进行解码

AuthAndResignAsIMP解析:

.macro AuthAndResignAsIMP
	// $0 = cached imp, $1 = address of cached imp, $2 = SEL , $3 = cls
        // imp = imp ^ cls 和bucket_t 的 encodeImp函数(uintptr_t)newImp ^ (uintptr_t)cls;对应
	eor	$0, $0, $3
.endmacro
  • imp = imp ^ clsbucket_tencodeImp函数(uintptr_t)newImp ^ (uintptr_t)cls;对应

getMethodNoSuper_nolock解析:

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();
    // beginLists为最后添加到方法列表的指针
    for (auto mlists = methods.beginLists(),
              end = methods.endLists();
         mlists != end;
         ++mlists)
    {
        // <rdar://problem/46904873> 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.
        //根据sel查询方法
        method_t *m = search_method_list_inline(*mlists, sel);
        if (m) return m;
    }

    return nil;
}
  • 遍历二维指针 methods,后添加的先查询

search_method_list_inline函数解析:

ALWAYS_INLINE static method_t *
search_method_list_inline(const method_list_t *mlist, SEL sel)
{
    int methodListIsFixedUp = mlist->isFixedUp();
    int methodListHasExpectedSize = mlist->isExpectedSize();
    // method列表是否修复过(进行排序过,这部分是在类的加载的时候做的事情)
    if (fastpath(methodListIsFixedUp && methodListHasExpectedSize)) {
        return findMethodInSortedMethodList(sel, mlist);
    } else {// 未修复的不做分析
        // Linear search of unsorted method list
        if (auto *m = findMethodInUnsortedMethodList(sel, mlist))
            return m;
    }
// DEBUG 不做分析
#if DEBUG
    // sanity-check negative results
    if (mlist->isFixedUp()) {
        for (auto& meth : *mlist) {
            if (meth.name() == sel) {
                _objc_fatal("linear search worked when binary search did not");
            }
        }
    }
#endif

    return nil;
}

findMethodInSortedMethodList(SEL key, const method_list_t *list)函数解析:

ALWAYS_INLINE static method_t *
findMethodInSortedMethodList(SEL key, const method_list_t *list)
{
    if (list->isSmallList()) {// 根据架构不同判断是否small
        if (CONFIG_SHARED_CACHE_RELATIVE_DIRECT_SELECTORS && objc::inSharedCache((uintptr_t)list)) {
            return findMethodInSortedMethodList(key, list, [](method_t &m) { return m.getSmallNameAsSEL(); });
        } else {
            return findMethodInSortedMethodList(key, list, [](method_t &m) { return m.getSmallNameAsSELRef(); });
        }
    } else {
        return findMethodInSortedMethodList(key, list, [](method_t &m) { return m.big().name; });
    }
}

findMethodInSortedMethodList(SEL key, const method_list_t *list, const getNameFunc &getName)函数解析:

//二分查找法
template<class getNameFunc>
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;
    // probe 推断为 first的类型
    decltype(first) probe;
    // 将sel转换成uintptr_t,此时已排序
    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))) {
                //如果前一个的名字sel也一样,并且不是起始位置,由于方法以及按照名称排序,前一个名称一样, 则表示前一个方法在主类和分类或多个分类中都有实现,说明了如果分类重写了主类的方法,调用该方法的时候会调用分类的方法:多个分类有相同的方法,则按照加载顺序执行最后加载的方法
                probe--;
            }
            return &*probe;
        }
        
        if (keyValue > probeValue) {
            base = probe + 1;
            count--;
        }
    }
    
    return nil;
}

二分查找法举例说明:假设需要查询的方法在第5位,方法 count =8,第一次查询 probe.index = 4,不满足,进入if (keyValue > probeValue)判断,base为第5位,count-- 为7,再次进入循环,count >> 1 = 3,probe = base + (count >> 1) probeindex = 6,不满足,再次进入循环,count >> 1 = 0 probe = baseindex = 5, 满足条件

图解:

二分查找法.gif

二分查找法测试:

findMethodInSortedMethodList函数做如下修改:

image.png

如下定义一个 XQPerson

@interface XQPerson : NSObject
-(void)playGame;
-(void)eatFood;
-(void)drink;
-(void)watchMovie;
-(void)walk;
-(void)run;
-(void)study;
-(void)playBaskeyBall;
@end

@implementation XQPerson

-(void)playGame{
    NSLog(@"%s",__func__);
}
-(void)eatFood{
    NSLog(@"%s",__func__);
}
-(void)drink{
    NSLog(@"%s",__func__);
}
-(void)watchMovie{
    NSLog(@"%s",__func__);
}
-(void)walk{
    NSLog(@"%s",__func__);
}
-(void)run{
    NSLog(@"%s",__func__);
}
-(void)study{
    NSLog(@"%s",__func__);
}
-(void)playBaskeyBall{
    NSLog(@"%s",__func__);
}
@end



一次调用其中的方法如下:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        XQPerson* person = [XQPerson alloc];
        [person eatFood];
        printf("\n");
        [person walk];
        printf("\n");
        [person run];
        printf("\n");
        [person watchMovie];
        printf("\n");
        [person playGame];
        printf("\n");
        [person playBaskeyBall];
        printf("\n");
        [person study];
        printf("\n");
        [person drink];
        printf("\n");
    }
    return 0;
}

********************************调试结果****************************************

name:walk------------------probeValue:4294982885 ----------------index:4
name:drink------------------probeValue:4294982868 ----------------index:2
name:eatFood------------------probeValue:4294982860 ----------------index:1
2022-02-16 16:34:27.805291+0800 KCObjcBuild[37811:636981] -[XQPerson eatFood]

2022-02-16 16:34:27.806164+0800 KCObjcBuild[37811:636981] -[XQPerson walk]

name:walk------------------probeValue:4294982885 ----------------index:4
name:playBaskeyBall------------------probeValue:4294982900 ----------------index:6
name:run------------------probeValue:140735269552228 ----------------index:7
2022-02-16 16:34:27.806353+0800 KCObjcBuild[37811:636981] -[XQPerson run]

name:walk------------------probeValue:4294982885 ----------------index:4
name:drink------------------probeValue:4294982868 ----------------index:2
name:watchMovie------------------probeValue:4294982874 ----------------index:3
2022-02-16 16:34:27.806486+0800 KCObjcBuild[37811:636981] -[XQPerson watchMovie]

name:walk------------------probeValue:4294982885 ----------------index:4
name:drink------------------probeValue:4294982868 ----------------index:2
name:eatFood------------------probeValue:4294982860 ----------------index:1
name:playGame------------------probeValue:4294982851 ----------------index:0
2022-02-16 16:34:27.812755+0800 KCObjcBuild[37811:636981] -[XQPerson playGame]

name:walk------------------probeValue:4294982885 ----------------index:4
name:playBaskeyBall------------------probeValue:4294982900 ----------------index:6
2022-02-16 16:34:27.813065+0800 KCObjcBuild[37811:636981] -[XQPerson playBaskeyBall]

name:walk------------------probeValue:4294982885 ----------------index:4
name:playBaskeyBall------------------probeValue:4294982900 ----------------index:6
name:study------------------probeValue:4294982894 ----------------index:5
2022-02-16 16:34:27.813203+0800 KCObjcBuild[37811:636981] -[XQPerson study]

name:walk------------------probeValue:4294982885 ----------------index:4
name:drink------------------probeValue:4294982868 ----------------index:2
2022-02-16 16:34:27.813313+0800 KCObjcBuild[37811:636981] -[XQPerson drink]

log_and_fill_cache函数解析:

static void
log_and_fill_cache(Class cls, IMP imp, SEL sel, id receiver, Class implementer)
{
#if SUPPORT_MESSAGE_LOGGING
    if (slowpath(objcMsgLogEnabled && implementer)) {
        bool cacheIt = logMessageSend(implementer->isMetaClass(), 
                                      cls->nameForLogging(),
                                      implementer->nameForLogging(), 
                                      sel);
        if (!cacheIt) return;
    }
#endif
    // 插入缓存
    cls->cache.insert(sel, imp, receiver);
}
  • cls->cache.insert,将查找到的 impsel插入 cls 的cache

1.3方法慢速查找流程图:

方法慢速查找.drawio.png

总结:objc_msgSend在快速查找失败后会进入方法慢速查找流程,即lookUpImpOrForward,大致流程为加载类信息(方法列表,属性列表,协议列表等),确认当前类的继承链,使用二分查找法在本类的方法列表中查找方法,如找到则插入缓存并返回,未找到则依次遍历父类,先在父类的缓存列表中查找如找到则插入本类缓存,如未找到则使用二分查找法查找父类的方法列表如找到 imp 后将 imp 添加到缓存后返回,未找到则进入动态方法决议流程。