Runtime消息慢速查找流程分析

446 阅读5分钟

前言:我们前面通过方法的本质,了解到了objc_msgSend函数的一个快速缓存查找过程,如果在进行快速查找,寻找不到IMP的情况下,查找过程并不会结束,而是会开始另外一个过程————慢速查找过程。那么接下来就让我们通过一系列的分析,来了解objc_msgSend的慢速查找过程!

runtime消息发送系列文章友情链接

【一】Runtime 消息快速查找流程分析

【二】Runtime消息慢速查找流程分析

【三】Runtime之动态方法决议和消息转发

一、objc_msgSend过程回顾

我们在进行消息发送的时候,会调用objc_msgSend(receiver,_cmd)方法进行消息的发送,其中receiver是消息的接受者,_cmd是消息的内容。如下:

Person *p = [Person alloc];
[p test];

//转化为cpp
Person *p = ((Person *objc_msgSend(objc_getClass("Person"), sel_registerName("alloc")); 
objc_msgSend(p, sel_registerName("test"));    

1、objc_msgSend快速查找回顾

1、通过类找到isa

2、拿到isa通过偏移量找到cache_t

3、然后拿到存储方法的buckets,尝试通过哈希数组拿到bucket中的方法IMP

4、如果获取到就返回,如果找不到就继续遍历其他的bucket

5、最后进入慢速查找过程。

2、objc_msgSend慢速查找

这里,我们要从一个函数说起 ———— lookUpImpOrForward,快速查找的汇编分析中,我们跟踪到这个函数后,就开始了消息查找的另一段旅程 ———— 慢速查找过程!

二、objc_msgSend慢速查找解析

1、lookUpImpOrForward方法

  • 以下程序是runtime中lookUpImpOrForward方法的实现,具体流程请参照注释:

#pragma mark - 消息查找流程,核心内容

NEVER_INLINE
IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior)
{
#pragma mark - 从_objc_msgForward_impcache中寻找IMP
    const IMP forward_imp = (IMP)_objc_msgForward_impcache;
    IMP imp = nil;
    Class curClass;

    runtimeLock.assertUnlocked();

#pragma mark - 在第一次初始化,做一下类还未Initialized的标记
    if (slowpath(!cls->isInitialized())) {
        behavior |= LOOKUP_NOCACHE;
    }

    runtimeLock.lock();

#pragma mark - 检查类是否进行过Initialized绑定操作
    checkIsKnownClass(cls);
    cls = realizeAndInitializeIfNeeded_locked(inst, cls, behavior & LOOKUP_INITIALIZE);
    runtimeLock.assertLocked();
    curClass = cls;


#pragma mark - 通过for循环,找出当前类的方法
    for (unsigned attempts = unreasonableClassCount();;) {
        if (curClass->cache.isConstantOptimizedCache(/* strict */true)) {
#if CONFIG_USE_PREOPT_CACHES


#pragma mark - ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
#pragma mark - 开始寻找缓存和方法列表中的方法

            imp = cache_getImp(curClass, sel);
            if (imp) goto done_unlock;
            curClass = curClass->cache.preoptFallbackClass();
#endif
        } else {

#pragma mark - 寻找当前类中的方法列表中的方法(二分查找法寻找方法)
            Method meth = getMethodNoSuper_nolock(curClass, sel);
            if (meth) {
                imp = meth->imp(false);
                goto done;
            }

#pragma mark - 未找到方法并且未进行后续处理,跳出循环
            if (slowpath((curClass = curClass->getSuperclass()) == nil)) {
                imp = forward_imp;
                break;
            }
        }

#pragma mark - 父类的二分查找尝试次数结束,发送错误通知
        if (slowpath(--attempts == 0)) {
            _objc_fatal("Memory corruption in class list.");
        }

#pragma mark - ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
#pragma mark - 本类没有找到方法,开始寻找父类的缓存和方法列表中的方法
        imp = cache_getImp(curClass, sel);
        if (slowpath(imp == forward_imp)) {
#pragma mark - 发现缓存中存在imp,跳出当前循环,去处理imp
            break;
        }
        
        if (fastpath(imp)) {
#pragma mark - 发现imp在父类中,将其缓存到自己的类信息中,然后处理imp
            goto done;
        }
    }

#pragma mark - 如果imp没有实现,则进入消息动态解析过程,如果实现,就不处理,继续进行后续
    if (slowpath(behavior & LOOKUP_RESOLVER)) {
        behavior ^= LOOKUP_RESOLVER;
        return resolveMethod_locked(inst, sel, cls, behavior);
    }

#pragma mark - ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
#pragma mark - ************** 核心事务处理过程 ************
#pragma mark - ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓

/*
* done内做的工作:
* 1、如果cache中存在IMP,就从cache中取出IMP返回
* 2、打印并且插入方法到当前的类信息中
* 3、如果没找到缓存或者未找到imp相同的方法,则返回nil
*/

 done:
    if (fastpath((behavior & LOOKUP_NOCACHE) == 0)) {
#if CONFIG_USE_PREOPT_CACHES
        while (cls->cache.isConstantOptimizedCache(/* strict */true)) {
            cls = cls->cache.preoptFallbackClass();
        }
#endif
        log_and_fill_cache(cls, imp, sel, inst, curClass);
    }
 done_unlock:
    runtimeLock.unlock();
    if (slowpath((behavior & LOOKUP_NIL) && imp == forward_imp)) {
        return nil;
    }
    return imp;
}

2、消息慢速查找流程图

慢速查找流程.png

3、查找过程的关键点

快速查找过程我们分析了汇编相关的实现,那么慢速查找中也是有相关的难点存在的。比如查找方法的过程中,是根据什么进行上层查找的,查找的算法实现等。那么我们接下来就根据这些关键的代码实现进行分析!

三、查找过程中的关键点分析

3.1、在类中怎么寻找缓存中的方法?

        meth = _cache_getMethod(curClass, sel, _objc_msgForward_impcache);
  • 从查找过程的函数分析,关键过程在_cache_getMethod的实现,在runtime的c++代码中查找不到实现,于是,我们搜索汇编的实现:
#pragma mark 中间会经历isa->class->cache_t->buckets->bucket->SEL IMP的运算过程
#pragma mark 中间会经历isa->class->cache_t->buckets->bucket->SEL IMP的运算过程
#pragma mark 中间会经历isa->class->cache_t->buckets->bucket->SEL IMP的运算过程

1: cmp    p0, #0

2: GetClassFromIsa_p16 p13, 1, x0    // p16 = class

3: CacheLookup NORMAL, _objc_msgSend, __objc_msgSend_uncached

3.1 : LLookupStart -> CACHE_MASK_STORAGE_HIGH_16

// 获取cache
ldr    p11, [x16, #CACHE]            // p11 = mask|buckets

CONFIG_USE_PREOPT_CACHES == 1

// p11 的 0 号位置是否为0  不为0 -> LLookupPreopt
tbnz    p11, #0, LLookupPreopt\Function
and    p10, p11, #0x0000ffffffffffff    // p10 = buckets

// p1 sel >> 7 == value ^= value >> 7;
eor    p12, p1, p1, LSR #7
// 哈希 index
and    p12, p12, p11, LSR #48        // x12 = (_cmd ^ (_cmd >> 7)) & mask

// index << 4
// 2 * 16
// buckets + 32
// 对应下标 : p13 第一个要查bucket(sel imp)
add    p13, p10, p12, LSL #(1+PTRSHIFT)
                    // p13 = buckets + ((_cmd & mask) << (1+PTRSHIFT))
// sel -> imp

3.2、在类中怎么寻找类的方法列表中的方法的?

  • 查找过程中,调用了以下方法进行了方法查找。

#pragma mark - 寻找当前类中的方法列表中的方法(二分查找法寻找方法)

 Method meth = getMethodNoSuper_nolock(curClass, sel);


  • getMethodNoSuper_nolock内部实现了cls->data()->methods()的过程。

static method_t *
getMethodNoSuper_nolock(Class cls, SEL sel)
{
  .......
    auto const methods = cls->data()->methods();
    for (auto mlists = methods.beginLists(),
              end = methods.endLists();
         mlists != end;
         ++mlists)
    {
        method_t *m = search_method_list_inline(*mlists, sel);
        if (m) return m;
    }
    return nil;
}

四、整体思路总结:

1、先判断类是否存在,不存在,返回空,否则继续,

2、查找类自己的缓存(class's cache),找到IMP就返回,否则就继续,

3、查找类自己的方法列表(class's method lists),找到就返回IMP,否则继续,

4、查找父类的缓存(superclass)和方法列表(method lists),找到返回IMP,否则继续

5、如果找到IMP,且IMP不是本类的IMP,就将父类的IMP缓存拷贝到自己的cache下一份使用,

6、如果没有IMP,则执行动态方法决议和消息转发流程。

以上过程是对整个方法慢速查找过程的总结。