[OC学习笔记]objc_msgSend:方法慢速查找

Posted Billy Miracle

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了[OC学习笔记]objc_msgSend:方法慢速查找相关的知识,希望对你有一定的参考价值。

一、执行慢速查找的时机

当快速查找流程找不到方法时候,走goto Miss 方法

(一)__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 x17// 返回函数指针

END_ENTRY __objc_msgSend_uncached

接下来先看一下TailCallFunctionPointer方法:

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

可看出,这里实际上是调用函数指针方法,说明如果走到这里,函数的imp已经找到。那么 MethodTableLookup 这个方法应该就是找到imp方法,看下源码:

(二)MethodTableLookup

//Locate the implementation for a selector in a class's method lists.
//在类的方法列表中找到选择器的实现。
//参数:
//	$0 = NORMAL, STRET
//	r0 or r1 (STRET) = receiver
//	r1 or r2 (STRET) = selector
//	r9 = class to search in
//结束:
//  IMP in r12, eq/ne set for forwarding
.macro MethodTableLookup
	
	SAVE_REGS

	// lookUpImpOrForward(obj, sel, cls, LOOKUP_INITIALIZE | LOOKUP_RESOLVER)
	// 我们可以在objc-runtime-new.s找到它的实现
.if $0 == NORMAL
	// receiver already in r0
	// selector already in r1
.else
	mov 	r0, r1			// receiver
	mov 	r1, r2			// selector
.endif
	mov	r2, r9			// class to search
	mov	r3, #3			// LOOKUP_INITIALIZE | LOOKUP_RESOLVER
	blx	_lookUpImpOrForward   //⚠️跳转,cache里找不到,跳转到方法列表里查找(blx:一种汇编的跳转指令![请添加图片描述](https://img-blog.csdnimg.cn/a49ef1601b34432686e5425721f1a2a8.png)
)
	mov	r12, r0			// r12 = IMP
	
.if $0 == NORMAL
	cmp	r12, r12		// set eq for nonstret forwarding
.else
	tst	r12, r12		// set ne for stret forwarding
.endif

	RESTORE_REGS

.endmacro

汇编流程:

可看出关键代码是_lookUpImpOrForward做了些操作,查到了imp,由于是一个下划线_,那么我们直接在底层代码查找即可。

(三)有关下划线

  1. C/C++调用汇编: 查找汇编时,C/C++调用的方法需要多加一个下划线( ___ )
  2. 汇编 调用 C/C++方法: 查找C/C++方法,需要将汇编调用的方法去掉一个下划线( ___ )

(四)代码调试

博主是个大菜b,一大堆源码和根本看不懂的汇编真是把我整晕了😵‍💫😵‍💫😵‍💫,跟着别人的博客调试一遍吧,眼见为实。经典打上断点,使用Debug->Debug Workflow->Always shows Disassembly查看汇编。
代码就写成这样:

紧接着就会看到这样的,说明进入了汇编语言部分,我们使用control + Step into的方式一步步调试。

我们按下去,发现会进入objc_msgSend
在控制台读取x1:

接下来正式进入objc_msgSend,br要跳转前,可以看到,参数x0就是第一个参数:类信息,x1就是第二个参数:方法信息。

接下来正式进入objc_msgSend流程:

一步步点,循环了几次,最后会进入_objc_msgSend_uncached

接下来,会进入lookUpImpOrForward

二、lookUpImpOrForward

接下来我们看下慢速查找lookUpImpOrForward方法:

IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior)

    const IMP forward_imp = (IMP)_objc_msgForward_impcache;
    IMP imp = nil;
    Class curClass;

    ...
    
    checkIsKnownClass(cls);

    cls = realizeAndInitializeIfNeeded_locked(inst, cls, behavior & LOOKUP_INITIALIZE);
    // runtimeLock may have been dropped but is now locked again
    runtimeLock.assertLocked();
    curClass = cls;

    for (unsigned attempts = unreasonableClassCount();;) 
        if (curClass->cache.isConstantOptimizedCache(/* strict */true)) 
#if CONFIG_USE_PREOPT_CACHES
            imp = cache_getImp(curClass, sel);
            if (imp) goto done_unlock;
            curClass = curClass->cache.preoptFallbackClass();
#endif
         else 
            // curClass method list.
            method_t *meth = getMethodNoSuper_nolock(curClass, sel);
            if (meth) 
                imp = meth->imp(false);
                goto done;
            

            if (slowpath((curClass = curClass->getSuperclass()) == nil)) 
                // No implementation found, and method resolver didn't help.
                // Use forwarding.
                imp = forward_imp;
                break;
            
        

        ...
        
    
    ...
 done:
    ...
 done_unlock:
    ...
    return imp;

这里内容比较多,废话不多说,直接开始看。

(一)checkIsKnownClass

lookUpImpOrForward里面先调用了这个方法,先看一下调用这个 方法前给出的注释:

// 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.
//
// 我们不希望人们能够制作一个看起来像类但实际上不是一个类的二进制 blob 并进行CFI攻击。
// 为了使这些更难,我们希望确保这是一个内置于二进制文件中或通过objc_duplicateClass,
// objc_initializeClassPair或objc_allocateClassPair合法注册的类。
/***********************************************************************
* checkIsKnownClass
* Checks the given class against the list of all known classes. Dies
* with a fatal error if the class is not known.
* Locking: runtimeLock must be held by the caller.
**********************************************************************/
ALWAYS_INLINE
static void
checkIsKnownClass(Class cls)

    if (slowpath(!isKnownClass(cls))) 
        _objc_fatal("Attempt to use unknown class %p.", cls);
    

这个方法是判断当前的类是否已经注册到缓存表里面,即它是否是注册类。如果当前类未知, 直接返回错误。

(二)realizeAndInitializeIfNeeded_locked

在调用了checkIsKnownClass后,紧接着调用了realizeAndInitializeIfNeeded_locked方法:

/***********************************************************************
* realizeAndInitializeIfNeeded_locked
* Realize the given class if not already realized, and initialize it if
* not already initialized.
* inst is an instance of cls or a subclass, or nil if none is known.
* cls is the class to initialize and realize.
* initializer is true to initialize the class, false to skip initialization.
**********************************************************************/
/*
如果尚未实现,请实现给定的类,如果尚未初始化,则初始化它。
inst 是 cls 或子类的实例,如果没有已知,则为 nil。
cls 是要初始化和实现的类。
初始值设定项为 true 用于初始化类,false 表示跳过初始化。
 */
static Class
realizeAndInitializeIfNeeded_locked(id inst, Class cls, bool initialize)

    runtimeLock.assertLocked();
    // 快速判断 当前类有没有实现
    if (slowpath(!cls->isRealized())) 
        // 当前类如果没实现, 调用realizeClassMaybeSwiftAndLeaveLocked, 先实现一下
        cls = realizeClassMaybeSwiftAndLeaveLocked(cls, runtimeLock);
        // runtimeLock may have been dropped but is now locked again
    
    // 判断类有没有初始化
    if (slowpath(initialize && !cls->isInitialized())) 
        // 当前类如果没实现, 调用initializeAndLeaveLocked, 先初始化一下
        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

1. realizeClassMaybeSwiftAndLeaveLocked

static Class
realizeClassMaybeSwiftAndLeaveLocked(Class cls, mutex_t& lock)

    return realizeClassMaybeSwiftMaybeRelock(cls, lock, true);

2. realizeClassMaybeSwiftMaybeRelock

/***********************************************************************
* realizeClassMaybeSwift (MaybeRelock / AndUnlock / AndLeaveLocked)
* Realize a class that might be a Swift class.
* Returns the real class structure for the class. 
* Locking: 
*   runtimeLock must be held on entry
*   runtimeLock may be dropped during execution
*   ...AndUnlock function leaves runtimeLock unlocked on exit
*   ...AndLeaveLocked re-acquires runtimeLock if it was dropped
* This complication avoids repeated lock transitions in some cases.
**********************************************************************/
/*
实现一个可能是 Swift 类的类。
返回类的实际类结构。
*/
static Class
realizeClassMaybeSwiftMaybeRelock(Class cls, mutex_t& lock, bool leaveLocked)

    lock.assertLocked();

    if (!cls->isSwiftStable_ButAllowLegacyForNow()) 
        // Non-Swift class. Realize it now with the lock still held.
        // fixme wrong in the future for objc subclasses of swift classes
        realizeClassWithoutSwift(cls, nil);
        if (!leaveLocked) lock.unlock();
     else 
        // Swift class. We need to drop locks and call the Swift
        // runtime to initialize it.
        lock.unlock();
        cls = realizeSwiftClass(cls);
        ASSERT(cls->isRealized());    // callback must have provoked realization
        if (leaveLocked) lock.lock();
    

    return cls;

3. realizeClassWithoutSwift

/***********************************************************************
* realizeClassWithoutSwift
* Performs first-time initialization on class cls, 
* including allocating its read-write data.
* Does not perform any Swift-side initialization.
* Returns the real class structure for the class. 
* Locking: runtimeLock must be write-locked by the caller
**********************************************************************/
/*
对类 cls 执行首次初始化,包括分配其读写数据。
不执行任何 Swift 端初始化。
返回类的实际类结构。
*/
static Class realizeClassWithoutSwift(Class cls, Class previously)

    runtimeLock.assertLocked();

    class_rw_t *rw;
    Class supercls;
    Class metacls;

    if (!cls) return nil;
    if (cls->isRealized()) 
        validateAlreadyRealizedClass(cls);
        return cls;
    
    ASSERT(cls == remapClass(cls));

    auto ro = (const class_ro_t *)cls->data();
    auto isMeta = ro->flags & RO_META;
    if (ro->flags & RO_FUTURE) 
        // This was a future class. rw data is already allocated.
        // 这是一个未来的类。rw 数据已分配。
        rw = cls->data();
        ro = cls->data()->ro();
        ASSERT(!isMeta);
        cls->changeInfo(RW_REALIZED|RW_REALIZING, RW_FUTURE);
     else 
        // Normal class. Allocate writeable class data.
        // 普通类。分配可写类数据。
        rw = objc::zalloc<class_rw_t>();
        rw->set_ro(ro);
        rw->flags = RW_REALIZED|RW_REALIZING|isMeta;
        cls->setData(rw);
    

    ...
    
    supercls = realizeClassWithoutSwift(remapClass(cls->getSuperclass()), nil);
    metacls = realizeClassWithoutSwift(remapClass(cls->ISA()), nil);

	...

    // Update superclass and metaclass in case of remapping
    cls->setSuperclass(supercls);
    cls->initClassIsa(metacls);

    ...

    return cls;

在这个方法里可看到主要是先对rwro的一些数据处理,之后处理了superclsmetacls

  1. supercls = cls->getSuperclass()递归取父类,再调用自身方法realizeClassWithoutSwift,即确定父类信息。
  2. metacls = cls->ISA()递归取元类,再调用自身方法realizeClassWithoutSwift,即确定元类信息。
  3. 之后调用cls->setSuperclass(supercls), cls->initClassIsa(metacls)将当前类的父类链、元类链确认绑定。

有关父类、元类这部分遗忘的知识,可以参考:[OC学习笔记]浅学元类
看了这部分代码,我们大概明白源代码的思路了,知道一个类,就层层嵌套,将父类/元类都初始化,方便之后找方法:

  • 实例方法:当前类没有去父类找,父类没有再去根类找……
  • 类方法:当前元类没有去父元类找,父元类没有再去根元类找……

(三)二分查找

继续往后面走,就会来到for (unsigned attempts = unreasonableClassCount();;),一个死循环,只能通过breakreturn来结束。

    // 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().
    /*
     用于在获取锁后立即再次查找类的缓存的代码,但在大多数情况下。
     有证据表明,这在大多数时候都是失误,因此是时间损失。
     在没有执行某种缓存查找的情况下调用此代码的唯一代码路径是class_getInstanceMethod()。
     */
     
	// 是因为多线程同步问题,还要再在cache里查找一次
    // 判断curClass是否被缓存, 防止因为上面不断调用ro, rw被对应方法被缓存, 如果当前类可以根据 sel找到imp, 即走 if 判断,
    // 不过通常是没有的
    for (unsigned attempts = unreasonableClassCount();;) 
        if (curClass->cache.isConstantOptimizedCache(/* strict */true)) 
#if CONFIG_USE_PREOPT_CACHES    // ios真机为1, 其他为0
//#if defined(__arm64__) && TARGET_OS_IOS && !TARGET_OS_SIMULATOR && !TARGET_OS_MACCATALYST
//#define CONFIG_USE_PREOPT_CACHES 1
//#else
//#define CONFIG_USE_PREOPT_CACHES 0
//#endif
            // 如果缓存能找到对应的imp, 直接跳转 goto done_unlock 方法
            imp = cache_getImp(curClass, sel);
            if (imp) goto done_unlock;
            curClass = curClass->cache.preoptFallbackClass();
#endif
         else 
            // curClass method list.
            // 如果缓存没有找到对应的imp, 走二分查找
            method_t *meth = getMethodNoSuper_nolock(curClass, sel);
            if (meth) 
                imp = meth->imp(false);
                goto done;
            

            if (slowpath((curClass = curClass->getSuperclass()) == nil)) 
                // No implementation found, and method resolver didn't help.
                // Use forwarding.
                imp = forward_imp;
                break;
            
        
        
    	...
    	
    

接下来看一下没有找的到时调用的getMethodNoSuper_nolock

1. getMethodNoSuper_nolock

/***********************************************************************
 * getMethodNoSuper_nolock
 * fixme
 * Locking: runtimeLock must be read- or write-locked by the caller
 **********************************************************************/
static method_t *
getMethodNoSuper_nolock(Class cls, SEL sel)

    runtimeLock.assertLocked();

    ASSERT(cls->isRealized());
    // fixme nil cls? 
    // fixme nil sel?
    
    // 从类中获取方法列表methods
    auto const methods = cls->data()->methods();
    // 循环遍历二位数组
    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.
        /*
         getMethodNoSuper_nolock是search_method_list最热门的调用方,
         内联它将getMethodNoSuper_nolock转换为无帧函数,
         并从此代码路径中消除任何存储。
         */
        
        // 从方法中查找
        method_t *m = search_method_list_inline(*mlists, sel);
        if (m) return m;
    

    return nil;

遍历时调用search_method_list_inline查找

2. search_method_list_inline

ALWAYS_INLINE static method_t *
search_method_list_inline(const method_list_t *mlist, SEL sel)

    // 检查mlist是否是固定顺序
    int methodListIsFixedUp = mlist->isFixedUp();
    // 检查mlist是否有预期大小
    int methodListHasExpectedSize = mlist->isExpectedSize();
    
    // 都满足走有序,不满足走无序
    if (fastpath(methodListIsFixedUp && methodListHasExpectedSize)) 
        // 有序方法
        return findMethodInSortedMethodList(sel, mlist);
     else 
        // Linear search of unsorted method list
        // 无序方法方法
        if (auto *m = findMethodInUnsortedMethodList(sel, mlist))
            return m;
    

#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;

可以看出,他既然分成两种查找,一种在有序里查找,一种在无序里查找,肯定要对有序进行了优化,想必就是二分。我们迫切想看到它是怎么实现的,会发现它有两个实现:

点进第一个看看:

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

    if (list->isSmallList()) 
        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; );
    

发现这个方法确有两个参数,最后都会调用它有三个参数的“兄弟”方法。那么我们直接去看三个参数的方法实现吧!

3. findMethodInSortedMethodList(二分查找)

/***********************************************************************
 * search_method_list_inline
 **********************************************************************/
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();// 初始化first为list首位
    auto base = first;// 定义 base为 first
    decltype(first) probe;// 定义循环查的位置 probe

    uintptr_t keyValue = (uintptr_t)key;// 定义keyValue为我们查找的sel
    uint32_t count;//定义count
    
    // 循环
    // count 为 list 元素个数
    // 循环条件: count != 0
    // 每次count >>= 1, 除2操作, 二分方法关键循环不断 / 2
    for (count = list->count; count != 0; count >>= 1) 
        //  probe 为 base + count除以2
        probe = base + (count >> 1);
        //  定义probeValue 为查询sel
        uintptr_t probeValue = (uintptr_t)getName(probe);
        //  如果当前查询sel == 循环sel
        if (keyValue == probeValue) 
            // `probe` is a match.
            // Rewind looking for the *first* occurrence of this value.
            // This is required for correct category overrides.
            // “probe”是匹配项。
            // 倒回去查找此值的 第一次 出现。
            // 这是正确的类别覆盖所必需的。
            // 做一步分类重名排查处理
            while (probe > first && keyValue == (uintptr_t)getName((probe - 1))) 
                probe--;
            以上是关于[OC学习笔记]objc_msgSend:方法慢速查找的主要内容,如果未能解决你的问题,请参考以下文章

[OC学习笔记]objc_msgSend:动态方法决议和消息转发

objc_msgSend消息传递学习笔记 – 消息转发

objc_msgSend消息传递学习笔记 – 对象方法消息传递流程

iOS底层探索之Runtime: lookUpImpOrForward慢速查找分析

runtime - 消息发送(objc_msgSend)

OC学习笔记