OC消息转发(二)— 消息的查找流程探索

前言

上篇文章我们对objc_msgSend进行了探索,主要是快速查找流程的汇编语言探索,最终我们探索到了__class_lookupMethodAndLoadCache3方法,然后我们猜测该方法的实现是C语言实现的。本篇文章我们就接着进行探索,首先对上篇文章的猜想进行验证一下。

验证猜想

要想验证上篇文章的猜想,那么我们办法就是跟着汇编语言走一下,找到方法__class_lookupMethodAndLoadCache3,看他是在哪里实现的。和上篇文章一样还是打断点,菜单里寻找Debug -> Debug Workflow -> Always Show Disassembly,然后按着control+进入objc_msgSend详情一步步找下去。找到如下图:

_objc_msgSend_uncached

进入到_objc_msgSend_uncached方法,接着往下寻找,如下图:

__class_lookupMethodAndLoadCache3

最终找到了方法是在objc-runtime-new.mm文件中实现的,验证了猜想。接下来我们沿着__class_lookupMethodAndLoadCache3往下探索下去,探索出objc_msgSend寻找方法的慢速流程。

方法查找流程

查找流程

寻找到方法__class_lookupMethodAndLoadCache3如下:

IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls)
{
    //cache = NO 是因为快速查找流程没有找到
    return lookUpImpOrForward(cls, sel, obj, 
                              YES/*initialize*/, NO/*cache*/, YES/*resolver*/);
}

调用方法lookUpImpOrForward ()慢速查找imp或者消息转发,lookUpImpOrForward ()如下:

IMP lookUpImpOrForward(Class cls, SEL sel, id inst, 
                       bool initialize, bool cache, bool resolver)
{
    IMP imp = nil;
    bool triedResolver = NO;
    //打开线程锁,防止多个同时调用
    runtimeLock.assertUnlocked();
    // Optimistic cache lookup
    //cache == YES  的情况,直接获取和返回imp
    if (cache) {
        imp = cache_getImp(cls, sel);
        if (imp) return imp;
    }

    // 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();
    //检测是否存在class
    checkIsKnownClass(cls);

    if (!cls->isRealized()) {
        //如果class未实现,那就调用realizeClass()实现
        realizeClass(cls);
    }

    if (initialize  &&  !cls->isInitialized()) {
        //class未初始化,但是传入的initialize为YES,需要重新初始化
        runtimeLock.unlock();
        _class_initialize (_class_getNonMetaClass(cls, inst));
        runtimeLock.lock();
        // 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.
    }

    
 retry:    
    runtimeLock.assertLocked();

    // Try this class's cache.
    //从本类中获取到imp,直接返回imp
    imp = cache_getImp(cls, sel);
    if (imp) goto done;

    // Try this class's method lists.
    {//本类方法列表中寻找方法
        Method meth = getMethodNoSuper_nolock(cls, sel);
        if (meth) {
            //查找到方法缓存一份
            log_and_fill_cache(cls, meth->imp, sel, inst, cls);
            imp = meth->imp;
            goto done;
        }
    }

    // Try superclass caches and method lists.
    {
        unsigned attempts = unreasonableClassCount();
        //本类中未获取到imp,遍历superclass链,找到imp就停止
        for (Class curClass = cls->superclass;
             curClass != nil;
             curClass = curClass->superclass)
        {
            // Halt if there is a cycle in the superclass chain.
            //当superclass链循环完成时停止
            if (--attempts == 0) {
                _objc_fatal("Memory corruption in class list.");
            }
            
            // Superclass cache.
            //首先在superclass的缓存中寻找imp
            imp = cache_getImp(curClass, sel);
            if (imp) {
                if (imp != (IMP)_objc_msgForward_impcache) {
                    // Found the method in a superclass. Cache it in this class.
                    //寻找到方法,加入到缓存里
                    log_and_fill_cache(cls, imp, sel, inst, curClass);
                    goto done;
                }
                else {
                    // Found a forward:: entry in a superclass.
                    // Stop searching, but don't cache yet; call method 
                    // resolver for this class first.
                    break;
                }
            }
            
            // Superclass method list.
            //superclass的缓存中没有找到imp,就到他的方法列表里寻找,如果找到就缓存
            Method meth = getMethodNoSuper_nolock(curClass, sel);
            if (meth) {
                log_and_fill_cache(cls, meth->imp, sel, inst, curClass);
                imp = meth->imp;
                goto done;
            }
        }
    }

    // No implementation found. Try method resolver once.
   //如果遍历的superclass都没有找到,那就进入消息转发流程,转发完成重新进入retry寻找一遍
    if (resolver  &&  !triedResolver) {
        runtimeLock.unlock();
       //消息转发流程入口(下篇文章讲)
        _class_resolveMethod(cls, sel, inst);
        runtimeLock.lock();
        // Don't cache the result; we don't hold the lock so it may have 
        // changed already. Re-do the search from scratch instead.
        triedResolver = YES;
        goto retry;
    }

    // No implementation found, and method resolver didn't help. 
    // Use forwarding.
    //最后都没找到方法,进入
    imp = (IMP)_objc_msgForward_impcache;
    cache_fill(cls, sel, imp, inst);

 done:
    runtimeLock.unlock();

    return imp;
}

可以看出调用方法lookUpImpOrForward()的时候,首先会做一些异常判断,如果能够获取到imp就直接返回,如果获取不到就进入寻找的流程。接下来流程方法分析,记得看我注释。

  • cache_getImp ()是汇编方法,GetClassFromIsa_p16CacheLookup请参考objc_msgSend探索cache_getImp ()详情如下:
STATIC_ENTRY _cache_getImp

    GetClassFromIsa_p16 p0
    CacheLookup GETIMP

LGetImpMiss:
    mov p0, #0
    ret

    END_ENTRY _cache_getImp
  • 下面是getMethodNoSuper_nolock()的详细流程方法(看注释)。
static method_t *
getMethodNoSuper_nolock(Class cls, SEL sel)
{
    runtimeLock.assertLocked();

    assert(cls->isRealized());
    // fixme nil cls? 
    // fixme nil sel?
    //遍历
    for (auto mlists = cls->data()->methods.beginLists(), 
              end = cls->data()->methods.endLists(); 
         mlists != end;
         ++mlists)
    {
        //寻找方法
        method_t *m = search_method_list(*mlists, sel);
        if (m) return m;
    }

    return nil;
}

static method_t *search_method_list(const method_list_t *mlist, SEL sel)
{
    int methodListIsFixedUp = mlist->isFixedUp();
    int methodListHasExpectedSize = mlist->entsize() == sizeof(method_t);
    
    if (__builtin_expect(methodListIsFixedUp && methodListHasExpectedSize, 1)) {
        //这里用到的是二分法查找已排序的方法列表
        return findMethodInSortedMethodList(sel, mlist);
    } else {
        // Linear search of unsorted method list
        //未排序方法列表
        for (auto& meth : *mlist) {
            if (meth.name == sel) return &meth;
        }
    }
    //此处省略部分代码
     ... 
    return nil;
}

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;

    //二进制数字右移一位相当于除以2,这里用到的是二分法查找
    for (count = list->count; count != 0; count >>= 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;
}
  • 下面是log_and_fill_cache()方法实现,会调用cache_fill(),具体实现请看对象的方法缓存
static void
log_and_fill_cache(Class cls, IMP imp, SEL sel, id receiver, Class implementer)
{
#if SUPPORT_MESSAGE_LOGGING
    if (objcMsgLogEnabled) {
        //
        bool cacheIt = logMessageSend(implementer->isMetaClass(), 
                                      cls->nameForLogging(),
                                      implementer->nameForLogging(), 
                                      sel);
        if (!cacheIt) return;
    }
#endif
//方法缓存
    cache_fill (cls, sel, imp, receiver);
}

void cache_fill(Class cls, SEL sel, IMP imp, id receiver)
{
#if !DEBUG_TASK_THREADS
    mutex_locker_t lock(cacheUpdateLock);
//方法缓存流程
    cache_fill_nolock(cls, sel, imp, receiver);
#else
    _collecting_in_critical();
    return;
#endif
}
  • _objc_msgForward_impcache也是汇编方法,通过全局查找找到arm64下详情,如下:
    STATIC_ENTRY __objc_msgForward_impcache

    // No stret specialization.
    b   __objc_msgForward

    END_ENTRY __objc_msgForward_impcache

//__objc_msgForward方法-------------------
ENTRY __objc_msgForward

    adrp    x17, __objc_forward_handler@PAGE
    ldr p17, [x17, __objc_forward_handler@PAGEOFF]
    TailCallFunctionPointer x17
    
    END_ENTRY __objc_msgForward
  • __objc_forward_handler方法是C语言方法,如下:
void *_objc_forward_handler = (void*)objc_defaultForwardHandler;

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

在这里我们看到了unrecognized selector sent to instance崩溃信息的文本,可以说已经找完了未找到方法的崩溃流程。

判断是不是汇编方法的方法
1、在C语言代码里的方法全局搜索不到,方法名前边加ENTRY搜索,能搜到就是汇编方法了。C语言的方法跳汇编方法名前边一般带ENTRY或者STATIC_ENTRY
2、在汇编语言里的方法要跳转到C语言方法一般方法名前带双下划线,带有b或者bl跳转指令,双下划线搜不到的时候可以去掉一个下划线搜一下。

总结

  • 1、先调用cache_getImp()class本类缓存中寻找此方法,找到的话返回方法的imp
  • 2、会调用getMethodNoSuper_nolock()class本类中寻找此方法,找到的话调用log_and_fill_cache()加入缓存并返回方法的imp
  • 3、如果第1步没找到,遍历superclass链,调用第1步的方法先在superclass的缓存中直接取,取到就返回,如果缓存中没有就调用第2步的方法在superclass的方法列表里寻找,找到返回并加入到缓存;直到所有的superclass都遍历完成。
  • 4、如果前三步还没有找到会调用_class_resolveMethod()进入消息转发流程(下篇文章详细讲解),转发完成会goto retry;重复第1步、第2步寻找imp
  • 5、如果前四步都没有找到会进入_objc_msgForward_impcache获取到imp,获取不到会崩溃unrecognized selector sent to instance 0xxxxxxx
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,482评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,377评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,762评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,273评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,289评论 5 373
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,046评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,351评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,988评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,476评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,948评论 2 324
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,064评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,712评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,261评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,264评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,486评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,511评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,802评论 2 345