Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

objc_msgSend 流程之慢速流程(8) #12

Open
lincf0912 opened this issue Jun 26, 2021 · 0 comments
Open

objc_msgSend 流程之慢速流程(8) #12

lincf0912 opened this issue Jun 26, 2021 · 0 comments

Comments

@lincf0912
Copy link
Owner

objc_msgSend 慢速流程

经过上一篇 objc_msgSend 快速流程分析,通过汇编查询 cache,如果 缓存命中 就直接进行发送消息。但是如果没有命中缓存接下来就需要走慢速流程了,也就是来到上一篇结尾处所说的 lookUpImpOrForward 函数里面,接下来本文将对慢速流程进行探索。

在快速查找流程中,如果没有找到方法实现,最终会来到 _objc_msgSend_uncached 函数中。

  • __objc_msgSend_uncached --> MethodTableLookup --> _lookUpImpOrForward

通过源码可以看到缓存中找不到会进入 _lookUpImpOrForward 的查找过程,汇编中搜索并未找到此方法:

1、全局搜索‘lookUpImpOrForward’,可以发现此方法是在objc-runtime-new.mm文件中C++实现的;

2、同样也可以通过 show 反汇编方式找到:Debug --> Debug Workflow --> Always show disassembly 勾上

842658-20200925093806876-479293389

  • control + stepinto 进入 objc_msgSend:

842658-20200925094650859-625298241

  • control + stepinto 进入 _objc_msgSend_uncached:

842658-20200925094853652-630459038

  • 最终我们可以看到它会走到 lookUpImpOrForward 函数,所以从这里我们也可以看出,他在汇编快速查找流程没有找到方法实现的时候,会来到慢速查找流程 lookUpImpOrForward 处。

慢速查找方法流程分析

因为 lookUpImpOrForward 函数是支持多线程的,所以内部有很多锁操作,然后通过 runtimeLock 控制读写锁。其内部有很多逻辑代码。

通过类对象的 isRealized 函数,判断当前类是是否被实现,如果没有被实现,则通过 realizeClassMaybeSwiftAndLeaveLocked 函数实现该类。在 realizeClassMaybeSwiftAndLeaveLocked 函数中,会设置 rwrosuperclsmetacls等一些信息。

lookUpImpOrForward 分析

IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior)
{// cache 中没有找到方法,开始走 lookUpImpOrForward 查找流程
    // 汇编有调 --> objc_msgForward --> 找不到方法报错信息的处理
    const IMP forward_imp = (IMP)_objc_msgForward_impcache;
    IMP imp = nil;
    Class curClass;

    runtimeLock.assertUnlocked();

    // Optimistic cache lookup
    if (fastpath(behavior & LOOKUP_CACHE)) {
        // 找缓存 - 是为了出现在此过程中方法又被人调用加进缓存了,有缓存了就不必继续慢速找了
        imp = cache_getImp(cls, sel);
        if (imp) goto done_nolock;// 找到了,去 done_nolock
    }

    // 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.
    //
    // TODO: this check is quite costly during process startup.
    checkIsKnownClass(cls);

    if (slowpath(!cls->isRealized())) {// cls 是否已实现,否则去将类信息进行处理 类元类方法全部要处理好的 --> 为了后面的方法查找
        cls = realizeClassMaybeSwiftAndLeaveLocked(cls, runtimeLock);
        // runtimeLock may have been dropped but is now locked again
    }

    // initialize 初始化
    if (slowpath((behavior & LOOKUP_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
    }

    runtimeLock.assertLocked();
    curClass = cls;

    // The code used to lookpu 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().

    for (unsigned attempts = unreasonableClassCount();;) {// 死循环,没有出口条件,跳出逻辑在循环内部
        // curClass method list.
        Method meth = getMethodNoSuper_nolock(curClass, sel);// 查找方法
        if (meth) {// 找着了
            imp = meth->imp;
            goto done;
        }
        // 自己没找着
        // curClass = superClass
        // 是nil 则直接 没找着方法把nil的forward_imp赋给imp,并跳出循环
        if (slowpath((curClass = curClass->superclass) == nil)) {
            // No implementation found, and method resolver didn't help.
            // Use forwarding.
            imp = forward_imp;
            break;
        }
// superclass 不是 nil 继续向下走

        // Halt if there is a cycle in the superclass chain.
        // 如果超类链中存在循环,则停止
        if (slowpath(--attempts == 0)) {
            _objc_fatal("Memory corruption in class list.");// 类列表的内存污染了
        }

        // Superclass cache.
        // 找父类的缓存
        /**
         CacheLookup  GETIMP, _cache_getImp
         */
        imp = cache_getImp(curClass, sel);
        /*
         STATIC_ENTRY _cache_getImp

         GetClassFromIsa_p16 p0
         CacheLookup GETIMP, _cache_getImp // GETIMP,cache查找的参数是GETIMP,checkMiss

    LGetImpMiss:// cache 中没找到直接返回0
         mov    p0, #0
         ret

         END_ENTRY _cache_getImp
         */


        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;
        }
        if (fastpath(imp)) {// 父类中找到了 goto done --> 对此方法进行缓存
            // Found the method in a superclass. Cache it in this class.
            goto done;
        }
    }

    // No implementation found. Try method resolver once.
  // 上面找完了没找着方法 动态方法解析 resolver 一次 --> 此方法只会走一次once
    if (slowpath(behavior & LOOKUP_RESOLVER)) {/*                                &:3 & 2 = 0011 & 0010 = 0010                                第二次(方法动态处理中会再回来查一遍)再来条件就为false了: 0001 & 0010 = 0000                                */
        behavior ^= LOOKUP_RESOLVER;// 异或操作 behavior = 0011^0010 = 0001
        return resolveMethod_locked(inst, sel, cls, behavior);
    }

 done:
	 // 查找到了对应的 Method,那么就填充到缓存
    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;
}

getMethodNoSuper_nolock 二分查找法的分析

在方法不是第一次调用时,可以通过 cache_getImp 函数查找到缓存的 IMP。但如果是第一次调用,就查找不到缓存的 IMP,那么就会进入到 getMethodNoSuper_nolock 函数中执行。下面是 getMethodNoSuper_nolock 函数的实现代码。

static method_t *
getMethodNoSuper_nolock(Class cls, SEL sel)
{
    auto const methods = cls->data()->methods();
    // 二分查找
    // 在 objc_object 的 class_rw_t *data() 的 methods 。
    // beginLists : 第一个方法的指针地址。
    // endLists : 最后一个方法的指针地址。
    // 每次遍历后向后移动一位地址。
    for (auto mlists = methods.beginLists(),
              end = methods.endLists();
         mlists != end;
         ++mlists)
    {
        // 对 `sel` 参数和 `method_t` 做匹配,如果匹配上则返回。
        method_t *m = search_method_list_inline(*mlists, sel);
        if (m) return m;
    }

    return nil;
}

search_method_list 分析

当调用一个对象的方法时,查找对象的方法,本质上就是遍历对象 isa 所指向类的方法列表,并用调用方法的 SEL 和遍历的 method_t 结构体的 name 字段做对比,如果相等则将 IMP 函数指针返回。

// 根据传入的 SEL,查找对应的 method_t 结构体
ALWAYS_INLINE static method_t *
search_method_list_inline(const method_list_t *mlist, SEL sel)
{
    int methodListIsFixedUp = mlist->isFixedUp();
    int methodListHasExpectedSize = mlist->entsize() == sizeof(method_t);
    
    if (fastpath(methodListIsFixedUp && methodListHasExpectedSize)) {
        return findMethodInSortedMethodList(sel, mlist);
    } else {
        for (auto& meth : *mlist) {
            // SEL 本质上就是字符串,查找的过程就是进行字符串对比
            if (meth.name == sel) return &meth;
        }
    }

    return nil;
}

findMethodInSortedMethodList 分析

二分查找关键点和注意点:

  • 排序方法 fixupMethodList 中使用 std::stable_sort 进行文档排序,确保分类的 method 在前。
  • 二分查找找到 SEL 相同的 method 之后,会继续向前查找是否还有 SEL 相同的 method,找到之后,那个才是最终要找的 method。这样就确保了分类的 method 被优先调用。
/***********************************************************************
 * search_method_list_inline
 **********************************************************************/
ALWAYS_INLINE static method_t *
findMethodInSortedMethodList(SEL key, const method_list_t *list)
{
    ASSERT(list);
    // list: 方法list是递增排序(类加载时完成的排序)的 即:name 转 unsigned long 类型 对应的数值 是递增的,例:0 1 2 3 4 5 ......
    const method_t * const first = &list->first;
    const method_t *base = first;// 方法list中第一个方法
    const method_t *probe;
    uintptr_t keyValue = (uintptr_t)key;// 要查找的方法  sel强转uintptr_t
    uint32_t count;// 方法数
    // 二分查找
    /**
     找 03   -    01 02 03 04 05 06 07 08
     probe = 1 +  8>>1 = 1+ 4 = 5
     判断第5个方法是否是要找的方法
        不是:对比 要找的方法 和 当前方法 位置谁大 03>05?false

     开始回第一步
     probe = 1+ count>>1 = 1 +  4>>1 = 1+2 = 3
     继续判断...
     02 == 03 false
     02 > 03 false

     继续
     probe = 1+ 2>>1 = 1+1 = 2
     02 == 02 ture
        while 判断
            判断找有重名的分类方法
     */
    for (count = list->count; count != 0; count >>= 1) {// count = 8>>1  4>>1  2>>1
        probe = base + (count >> 1);

        uintptr_t probeValue = (uintptr_t)probe->name;

        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)probe[-1].name) {// 判断分类方法 是否有重名的方法,有则往前找取分类方法 --> 分类排在前面
                probe--;
            }
            return (method_t *)probe;
        }

        if (keyValue > probeValue) {
            base = probe + 1;
            count--;
        }
    }

    return nil;
}

findMethodInSortedMethodList 执行逻辑

  • count: 假设初始值为方法列表的个数为 48

  • 如果 count != 0; 循环条件每次右移一位,也就是说除以 2;

  • 第一次进入从一半 24 开始找起,如果 keyValue > probeValue 那么在右边,否则在左边;

  • 第二次是从 12 开始找起,也不满足 keyValue > probeValue 的条件;

  • 第三次从 6 开始找起,满足条件 keyValue > probeValue,将初始值移动到当前 6 的后一位,也就是从 7 开始查找,然后 count--,可以看到当前 count = 5 ,然后在对 > 6 且 < 12 进行查找,也就是 7 - 11 ,count >> 1 为 2, 7+2 = 9,刚好是 7 - 11 的中心。

  • 这就是 二分查找法,但是前提必须是有序数组。

找不到实现方法, Xcode 崩溃

如果没有实现 动态方法决议和消息转发 就进入 _objc_msgForward_impcache汇编

STATIC_ENTRY __objc_msgForward_impcache

// No stret specialization.
b	__objc_msgForward

END_ENTRY __objc_msgForward_impcache

ENTRY __objc_msgForward

上述代码发现调用了 _objc_forward_handler 函数,继续搜索。

_objc_forward_handler 函数

__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);
}

void *_objc_forward_handler = (void*)objc_defaultForwardHandler;

看到这里应该都明白了,这就是经常在 Xcode 控制台看到的找不到方法的崩溃信息。所以也证明了一件事,在底层是 没有对象方法和类方法之分 的。都是函数而已,实例方法是类的实例方法,类方法也是元类的实例方法。

慢速查找流程图

image

总结

消息调用总结

  • 消息的查找有快速流程通过 objc_msgSend 通过 cache 查找、慢速流程 lookUpImpOrForward 进行查找。

  • 从快速查找流程进入慢速查找流程一开始是不会进行 cache 查找的,而是直接从方法列表中进行查找。

  • 从方法的缓存列表中查找,通过 cache_getImp 函数进行查找,如果找打缓存则直接返回 IMP

  • 首先会查找当前类的 method list,查找是否有对应的 SEL,如果有则获取到 Method 对象,并从 Method 对象中获取 IMP,并返回 IMP(这一步查找的结果是 Method 对象)。

  • 如果在当前类没有找到 SEL,则进行死循环去父类的缓存列表和方法列表中查找。

  • 如果在类的继承体系中,一直都没有查找到对应的 SEL,则进去动态方法决议。可以在 + resolveInstanceMethod+ resolveClassMethod 两个方法中动态添加实现。

  • 如果动态方法决议阶段没有做出任何响应,则进入动态消息转发阶段。此时可以在动态消息转发阶段做一下处理,如果还不进行处理,就会引发 Crash

总体可以被分为以下三个部分

  1. 刚调用 objc_msgSend 函数后,内部会做一些处理逻辑。
  2. 复杂的查找 IMP 的过程,会涉及到缓存列表和方法列表等等信息。
  3. 进入动态方法决议和消息转发阶段。
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant