Runtime原理探究(四)—— 刨根問(wèn)底消息機(jī)制

上一篇


Runtime系列文章

Runtime原理探究(一)—— isa的深入體會(huì)(蘋(píng)果對(duì)isa的優(yōu)化)
Runtime原理探究(二)—— Class結(jié)構(gòu)的深入分析
Runtime原理探究(三)—— OC Class的方法緩存cache_t
Runtime原理探究(四)—— 刨根問(wèn)底消息機(jī)制
Runtime原理探究(五)—— super的本質(zhì)
[Runtime原理探究(六)—— Runtime的應(yīng)用...待續(xù)]-()
[Runtime原理探究(七)—— Runtime的API...待續(xù)]-()
Runtime原理探究(八)—— 面試題中的Runtime


 
上一篇里面渣磷,我們從Classcache_t作為切入點(diǎn)圈盔,完善了我們對(duì)于OC類對(duì)象的認(rèn)識(shí),而且還詳細(xì)了解了Runtime的消息發(fā)送流程和方法緩存策略付魔,不過(guò)對(duì)于消息機(jī)制這個(gè)話題只是熱身而已逢渔。接下來(lái)肋坚,本文就從源頭開(kāi)始,完整地來(lái)研究Runtime的消息機(jī)制肃廓。

消息機(jī)制流程框架

[obj message] ?? 消息發(fā)送 ?? 動(dòng)態(tài)方法解析 ?? 消息轉(zhuǎn)發(fā)

(一)消息發(fā)送

消息發(fā)送流程上一篇文章已經(jīng)分析過(guò)智厌,這里再?gòu)?code>[obj message]為出發(fā)點(diǎn),從objc源碼里進(jìn)行一次正向梳理盲赊。

首先铣鹏,要查看[obj message]的底層表示,可以通過(guò)xcode調(diào)試工具調(diào)出其匯編代碼進(jìn)行分析哀蘑,但是這個(gè)方法需要你至少有熟練的匯編代碼閱讀能力诚卸,有不少難度葵第。如果把要求降低一點(diǎn),可以在命令行工具里面通過(guò)

xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc xxx.m -o yyy.cpp

生成一個(gè)中間代碼合溺,這個(gè)中間代碼基本上都是C或C++代碼羹幸,閱讀起來(lái)相對(duì)容易。但是需要說(shuō)明一下辫愉,這個(gè)中間代碼僅作為參考,因?yàn)槟壳皒code編譯器已經(jīng)不使用用這種格式的中間代碼的将硝,取而代之的是另一種語(yǔ)法格式的中間代碼恭朗,但是雖然語(yǔ)法不同,但是實(shí)現(xiàn)思路和邏輯大致是相同的依疼,因此老的中間代碼還是能夠借來(lái)參考一下的痰腮。

通過(guò)上面的命令行操作,[obj message]編譯之后的底層表示是

((void (*)(id, SEL))(void *)objc_msgSend)((id)obj, sel_registerName("message"));

去掉類型轉(zhuǎn)換后律罢,簡(jiǎn)化一下可以表示成

objc_msgSend(obj, sel_registerName("message"));

其中第一個(gè)參數(shù)obj就是消息接受者膀值,后面的sel_registerName,可以在objc源碼中搜到它的函數(shù)聲明

SEL _Nonnull sel_registerName(const char * _Nonnull str)

很明顯這里是根據(jù)一個(gè)C字符串返回一個(gè)SEL误辑,其實(shí)就等同于OC里面的@selector()沧踏。這兩個(gè)參數(shù)都沒(méi)什么太大疑問(wèn),然后我們來(lái)從源碼里看一看能否找到objc_msgSend的實(shí)現(xiàn)巾钉。但最終翘狱,你無(wú)法在源碼里面找到對(duì)應(yīng)的C函數(shù)實(shí)現(xiàn),因?yàn)樵趏bjc源碼里面砰苍,是通過(guò)匯編來(lái)實(shí)現(xiàn)的潦匈,并且對(duì)應(yīng)不同架構(gòu)有不同的版本,這里我們就關(guān)注arm64版本的實(shí)現(xiàn)赚导。

objc源碼中的匯編文件
其實(shí)objc源碼總共也沒(méi)多少文件茬缩,如上圖所示,除了msg吼旧,還有涉及到block相關(guān)的一些內(nèi)容也是用匯編實(shí)現(xiàn)的凰锡,還有一個(gè)objc-sel-table.s,受限本人知識(shí)儲(chǔ)備黍少,暫時(shí)還解讀不了寡夹,不過(guò)沒(méi)關(guān)系,它跟我們現(xiàn)在討論的話題不相關(guān)厂置。

你或許會(huì)疑惑菩掏,蘋(píng)果為什么要用匯編來(lái)實(shí)現(xiàn)某些函數(shù)呢?主要原因是因?yàn)閷?duì)于一些調(diào)用頻率太高的函數(shù)或操作昵济,使用匯編來(lái)實(shí)現(xiàn)能夠提高效率智绸。在匯編源碼里面野揪,可以按照下面的方法來(lái)定位函數(shù)實(shí)現(xiàn)


接下來(lái)我們開(kāi)始閱讀匯編
_objc_msgSend

然后查找一下CacheLookup,看看緩存怎么查詢的瞧栗,注意斯稳,這里的NORMAL是參數(shù)。
CacheLookup流程

如果是命中緩存迹恐,找到了方法挣惰,那就簡(jiǎn)單了,直接返回并調(diào)用就好了殴边,如果沒(méi)找憎茂,就會(huì)進(jìn)入上圖中的__objc_msgSend_uncached


__objc_msgSend_uncached中調(diào)用了MethodTableLookup
image.png
我們發(fā)現(xiàn)在MethodTableLookup里面,調(diào)用了__class_lookupMethodAndLoadCache3函數(shù)锤岸,而這個(gè)函數(shù)在當(dāng)前的匯編代碼里面是找不到實(shí)現(xiàn)的竖幔。你去objc源碼進(jìn)行全局搜索,也搜不到是偷,這里經(jīng)過(guò)大佬指點(diǎn)拳氢,如果是一個(gè)C函數(shù),在底層匯編里面如果需要調(diào)用的話蛋铆,蘋(píng)果會(huì)為其加一個(gè)下劃線_馋评,因此上面的的函數(shù)刪去一個(gè)下劃線,_class_lookupMethodAndLoadCache3戒职,你就可以在源碼里面找到它對(duì)應(yīng)的C函數(shù)栗恩,它是objc-runtime-new.mm里面的一個(gè)C函數(shù)


IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls)
{
    return lookUpImpOrForward(cls, sel, obj, 
                              YES/*initialize*/, NO/*cache*/, YES/*resolver*/);
}

而這個(gè)函數(shù)里面最終是調(diào)用了lookUpImpOrForward函數(shù),從這個(gè)函數(shù)開(kāi)始的后面的流程洪燥,我在上一篇文章里面已經(jīng)做過(guò)完整解讀了磕秤,這里不再做詳細(xì)論述,只將之前的結(jié)論貼出來(lái)

  • (1) 當(dāng)一個(gè)對(duì)象接收到消息時(shí)[obj message];捧韵,首先根據(jù)objisa指針進(jìn)入它的類對(duì)象cls里面市咆。
  • (2) 在objcls里面,首先到緩存cache_t里面查詢方法message的函數(shù)實(shí)現(xiàn)再来,如果找到蒙兰,就直接調(diào)用該函數(shù)。
  • (3) 如果上一步?jīng)]有找到對(duì)應(yīng)函數(shù)芒篷,在對(duì)該cls的方法列表進(jìn)行二分/遍歷查找搜变,如果找到了對(duì)應(yīng)函數(shù),首先會(huì)將該方法緩存到obj的類對(duì)象clscache_t里面针炉,然后對(duì)函數(shù)進(jìn)行調(diào)用挠他。
  • (4) 在每次進(jìn)行緩存操作之前,首先需要檢查緩存容量篡帕,如果緩存內(nèi)的方法數(shù)量超過(guò)規(guī)定的臨界值(設(shè)定容量的3/4)殖侵,需要先對(duì)緩存進(jìn)行2倍擴(kuò)容贸呢,原先緩存過(guò)的方法全部丟棄,然后將當(dāng)前方法存入擴(kuò)容后的新緩存內(nèi)拢军。
  • (5) 如果在objcls對(duì)象里面楞陷,發(fā)現(xiàn)緩存和方法列表都找不到mssage方法,則通過(guò)clssuperclass指針進(jìn)入它的父類對(duì)象f_cls里面
  • (6) 進(jìn)入f_cls后茉唉,首先在它的cache_t里面查找mssage固蛾,如果找到了該方法,那么會(huì)首先將方法緩存到消息接受者obj的類對(duì)象clscache_t里面度陆,然后調(diào)用方法對(duì)應(yīng)的函數(shù)魏铅。
  • (7) 如果上一步?jīng)]有找到方法,將會(huì)對(duì)f_cls的方法列表進(jìn)行遍歷二分/遍歷查找坚芜,如果找到了mssage方法,那么同樣斜姥,會(huì)首先將方法緩存到消息接受者obj的類對(duì)象clscache_t里面鸿竖,然后調(diào)用方法對(duì)應(yīng)的函數(shù)。需要注意的是铸敏,這里并不會(huì)將方法緩存到當(dāng)前父類對(duì)象f_cls的cache_t里面缚忧。
  • (8) 如果還沒(méi)找到方法,則會(huì)通過(guò)f_clssuperclass進(jìn)入更上層的父類對(duì)象里面杈笔,按照(6)->(7)->(8)步驟流程重復(fù)闪水。如果此時(shí)已經(jīng)到了基類對(duì)象NSObject,仍沒(méi)有找到mssage蒙具,則進(jìn)入步驟(9)
  • (9) 接下來(lái)將會(huì)轉(zhuǎn)到消息機(jī)制的 動(dòng)態(tài)方法解析 階段
    消息發(fā)送流程

到此球榆,消息發(fā)送機(jī)制的正向解讀就到這里。關(guān)于上面的匯編代碼禁筏,我自己也只是借助相關(guān)注釋說(shuō)明持钉,間接挖掘蘋(píng)果的底層思路,其實(shí)匯編里面還有更多的細(xì)節(jié)篱昔,只有你自己親自讀一遍每强,才會(huì)有更跟深的體會(huì)和領(lǐng)悟。

(二)動(dòng)態(tài)方法解析

接下來(lái)州刽,一起來(lái)認(rèn)識(shí)一下方法的動(dòng)態(tài)解析空执。上面的章節(jié),我們講到了lookUpImpOrForward函數(shù)穗椅,這個(gè)函數(shù)我在之前的文章也具體討論過(guò)了辨绊,但是僅僅是解讀完了消息發(fā)送和方法緩存的內(nèi)容,這里我先貼出該函數(shù)的代碼

/***********************************************************************
* lookUpImpOrForward.
* The standard IMP lookup. ------------->??????標(biāo)準(zhǔn)的IMP查找流程
* initialize==NO tries to avoid +initialize (but sometimes fails)
* cache==NO skips optimistic unlocked lookup (but uses cache elsewhere)
* Most callers should use initialize==YES and cache==YES.
* inst is an instance of cls or a subclass thereof, or nil if none is known. 
*   If cls is an un-initialized metaclass then a non-nil inst is faster.
* May return _objc_msgForward_impcache. IMPs destined for external use 
*   must be converted to _objc_msgForward or _objc_msgForward_stret.
*   If you don't want forwarding at all, use lookUpImpOrNil() instead.
**********************************************************************/
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
    if (cache) {//------------------>??????查詢當(dāng)前Class對(duì)象的緩存房待,如果找到方法邢羔,就返回該方法
        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.read();

    if (!cls->isRealized()) {//--------------->??????當(dāng)前Class如果沒(méi)有被realized驼抹,就進(jìn)行realize操作
        // Drop the read-lock and acquire the write-lock.
        // realizeClass() checks isRealized() again to prevent
        // a race while the lock is down.
        runtimeLock.unlockRead();
        runtimeLock.write();

        realizeClass(cls);

        runtimeLock.unlockWrite();
        runtimeLock.read();
    }

    if (initialize  &&  !cls->isInitialized()) {//--------->??????當(dāng)前Class如果沒(méi)有初始化,就進(jìn)行初始化操作
        runtimeLock.unlockRead();
        _class_initialize (_class_getNonMetaClass(cls, inst));
        runtimeLock.read();
        // 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
    }

    
 retry:    
    runtimeLock.assertReading();

    // Try this class's cache.//------------>??????嘗試從該Class對(duì)象的緩存中查找拜鹤,如果找到框冀,就跳到done處返回該方法

    imp = cache_getImp(cls, sel);
    if (imp) goto done;

    // Try this class's method lists.//---------------->??????嘗試從該Class對(duì)象的方法列表中查找,找到的話敏簿,就緩存到該Class的cache_t里面明也,并跳到done處返回該方法
    {
        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.------>??????進(jìn)入當(dāng)前Class對(duì)象的superclass對(duì)象
    {
        unsigned attempts = unreasonableClassCount();
        for (Class curClass = cls->superclass;//------>??????該for循環(huán)每循環(huán)一次,就會(huì)進(jìn)入上一層的superclass對(duì)象惯裕,進(jìn)行循環(huán)內(nèi)部方法查詢流程
             curClass != nil;
             curClass = curClass->superclass)
        {
            // Halt if there is a cycle in the superclass chain.
            if (--attempts == 0) {
                _objc_fatal("Memory corruption in class list.");
            }
            
            // Superclass cache.------>??????在當(dāng)前superclass對(duì)象的緩存進(jìn)行查找
            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;//------>??????如果在當(dāng)前superclass的緩存里找到了方法温数,就調(diào)用log_and_fill_cache進(jìn)行方法緩存,注意這里傳入的參數(shù)是cls蜻势,也就是將方法緩存到消息接受對(duì)象所對(duì)應(yīng)的Class對(duì)象的cache_t中撑刺,然后跳到done處返回該方法
                }
                else {
                    // Found a forward:: entry in a superclass.
                    // Stop searching, but don't cache yet; call method 
                    // resolver for this class first.
                    break;//---->??????如果緩存里找到的方法是_objc_msgForward_impcache,就跳出該輪循環(huán)握玛,進(jìn)入上一層的superclass够傍,再次進(jìn)行查找
                }
            }
            // Superclass method list.---->??????如過(guò)畫(huà)緩存里面沒(méi)有找到方法,則對(duì)當(dāng)前superclass的方法列表進(jìn)行查找
            Method meth = getMethodNoSuper_nolock(curClass, sel);
            if (meth) {
            //------>??????如果在當(dāng)前superclass的方法列表里找到了方法挠铲,就調(diào)用log_and_fill_cache進(jìn)行方法緩存冕屯,注意這里傳入的參數(shù)是cls,也就是將方法緩存到消息接受對(duì)象所對(duì)應(yīng)的Class對(duì)象的cache_t中拂苹,然后跳到done處返回該方法
                log_and_fill_cache(cls, meth->imp, sel, inst, curClass);
                imp = meth->imp;
                goto done;
            }
        }
    }
//**********************???消息發(fā)送流程結(jié)束???*************************


//??????動(dòng)態(tài)方法解析
    // No implementation found. Try method resolver once.//------>??????如果到基類還沒(méi)有找到方法安聘,就嘗試進(jìn)行方法解析

    if (resolver  &&  !triedResolver) {
        runtimeLock.unlockRead();
        _class_resolveMethod(cls, sel, inst);
        runtimeLock.read();
        // 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;
    }



//??????消息轉(zhuǎn)發(fā)
    // No implementation found, and method resolver didn't help. //------>??????如果方法解析不成功,就進(jìn)行消息轉(zhuǎn)發(fā)
    // Use forwarding.

    imp = (IMP)_objc_msgForward_impcache;
    cache_fill(cls, sel, imp, inst);

 done:
    runtimeLock.unlockRead();

    return imp;
}

根據(jù)上面代碼里面的??????動(dòng)態(tài)方法解析標(biāo)記處瓢棒,我們繼續(xù)解讀消息機(jī)制的 動(dòng)態(tài)方法解析階段浴韭。
首先注意一個(gè)細(xì)節(jié),這里有一個(gè)標(biāo)簽triedResolver用來(lái)判斷是否進(jìn)行該類是否進(jìn)行過(guò)動(dòng)態(tài)方法解析脯宿。如果首次走到這里囱桨,triedResolver = NO,當(dāng)動(dòng)態(tài)方法解析進(jìn)行過(guò)一次之后嗅绰,會(huì)設(shè)置triedResolver = YES舍肠,這樣下次走到這里的時(shí)候,就不會(huì)再次進(jìn)行動(dòng)態(tài)方法解析窘面,因?yàn)檫@個(gè)流程只需要進(jìn)行一次就夠了翠语,并且實(shí)在首次調(diào)用一個(gè)該類沒(méi)有實(shí)現(xiàn)的方法的時(shí)候,才會(huì)進(jìn)行這個(gè)流程财边,仔細(xì)體會(huì)一下

而最后這個(gè)goto retry回到的地方是本函數(shù)的如下位置
這個(gè)就是消息發(fā)送和緩存查詢流程的開(kāi)始步驟肌括,啥意思呢?就是說(shuō)經(jīng)過(guò)動(dòng)態(tài)方法解析流程處理過(guò)之后(在這個(gè)流程我們可以動(dòng)態(tài)給類增加方法【新增的方法會(huì)存放在 消息接受者->ISA() 的rw的方法列表里面去】),會(huì)重新走一遍緩存查找和消息發(fā)送谍夭。

下面再繼續(xù)看一下方法動(dòng)態(tài)解析里面的核心函數(shù)_class_resolveMethod(cls, sel, inst);

***********************************************************************
* _class_resolveMethod
* Call +resolveClassMethod or +resolveInstanceMethod.
* Returns nothing; any result would be potentially out-of-date already.
* Does not check if the method already exists.
**********************************************************************/
void _class_resolveMethod(Class cls, SEL sel, id inst)
{
    if (! cls->isMetaClass()) {
        // try [cls resolveInstanceMethod:sel]
        _class_resolveInstanceMethod(cls, sel, inst);
    } 
    else {
        // try [nonMetaClass resolveClassMethod:sel]
        // and [cls resolveInstanceMethod:sel]
        _class_resolveClassMethod(cls, sel, inst);
        if (!lookUpImpOrNil(cls, sel, inst, 
                            NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) 
        {
            _class_resolveInstanceMethod(cls, sel, inst);
        }
    }
}

很明顯黑滴,if (! cls->isMetaClass())這句代碼實(shí)在判斷當(dāng)前的參數(shù)cls是否是一個(gè)meta-class對(duì)象,也就是說(shuō)紧索,調(diào)用對(duì)象方法(-方法)和調(diào)用類方法(+方法)過(guò)程里面的動(dòng)態(tài)方法解析袁辈,走的都是這個(gè)方法啊,這里我們先關(guān)注對(duì)象方法(-方法)的解析處理邏輯珠漂。也就是_class_resolveInstanceMethod(cls, sel, inst);晚缩,進(jìn)入它的函數(shù)實(shí)現(xiàn)如下

static void _class_resolveInstanceMethod(Class cls, SEL sel, id inst)
{
//---??????查看cls的meta-class對(duì)象的方法列表里面是否有SEL_resolveInstanceMethod函數(shù),
//---??????也就是看是否實(shí)現(xiàn)了+(BOOL)resolveInstanceMethod:(SEL)sel方法
    if (!  lookUpImpOrNil(cls->ISA(), SEL_resolveInstanceMethod, cls, 
                         NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) 
    {

//---??????如果沒(méi)找到媳危,直接返回荞彼,
        // Resolver not implemented.
        return;
    }

//---??????如果找到,則通過(guò)objc_msgSend調(diào)用一下+(BOOL)resolveInstanceMethod:(SEL)sel方法
//---??????完成里面的動(dòng)態(tài)增加方法的步驟


//---??????接下來(lái)是一些對(duì)解析結(jié)果的打印信息
    BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
    bool resolved = msg(cls, SEL_resolveInstanceMethod, sel);

    // Cache the result (good or bad) so the resolver doesn't fire next time.
    // +resolveInstanceMethod adds to self a.k.a. cls
    IMP imp = lookUpImpOrNil(cls, sel, inst, 
                             NO/*initialize*/, YES/*cache*/, NO/*resolver*/);

    if (resolved  &&  PrintResolving) {
        if (imp) {
            _objc_inform("RESOLVE: method %c[%s %s] "
                         "dynamically resolved to %p", 
                         cls->isMetaClass() ? '+' : '-', 
                         cls->nameForLogging(), sel_getName(sel), imp);
        }
        else {
            // Method resolver didn't add anything?
            _objc_inform("RESOLVE: +[%s resolveInstanceMethod:%s] returned YES"
                         ", but no new implementation of %c[%s %s] was found",
                         cls->nameForLogging(), sel_getName(sel), 
                         cls->isMetaClass() ? '+' : '-', 
                         cls->nameForLogging(), sel_getName(sel));
        }
    }
}

動(dòng)態(tài)方法解析的核心步驟完成之后待笑,會(huì)一層一層往上返回到lookUpImpOrForward函數(shù)鸣皂,跳到retry標(biāo)記處,重新查詢方法暮蹂,因?yàn)樵诜椒ń馕鲞@一步签夭,如果對(duì)某個(gè)目標(biāo)方法名xxx有過(guò)處理,為其動(dòng)態(tài)增加了方法實(shí)現(xiàn)椎侠,那么再次查詢?cè)摲椒ǎ瑒t一定可以在消息發(fā)送階段被找到并調(diào)用措拇。 對(duì)于類方法(+方法)的動(dòng)態(tài)解析其實(shí)跟上面的過(guò)程大致相同我纪,只不過(guò)解析的時(shí)候調(diào)用的+(BOOL)resolveClassMethod:(SEL)sel方法,來(lái)完成類方法的動(dòng)態(tài)添加綁定丐吓。

小結(jié)

首先用圖來(lái)總結(jié)一下
動(dòng)態(tài)方法解析

動(dòng)態(tài)方法解析真的有必要嗎浅悉?
其實(shí)這個(gè)我覺(jué)得沒(méi)有固定答案,根據(jù)個(gè)人的理解因人而異券犁,就我個(gè)人的膚淺看法术健,好像除了在面試?yán)锩婵梢栽黾右稽c(diǎn)逼格外,實(shí)際項(xiàng)目中好像沒(méi)太多使用場(chǎng)景粘衬,因?yàn)榕c其在動(dòng)態(tài)解析步驟里面動(dòng)態(tài)增加方法荞估,還不如直接在類里面實(shí)現(xiàn)該方法呢,不知道大家有什么心得體會(huì)稚新,歡迎留言交流勘伺。

好了,動(dòng)態(tài)方法解析流程解讀完畢褂删。

(三)消息轉(zhuǎn)發(fā)

經(jīng)過(guò)前兩個(gè)流程之后飞醉,如果還沒(méi)能找到方法對(duì)應(yīng)的函數(shù),說(shuō)明當(dāng)前類已經(jīng)盡力了屯阀,但是確實(shí)沒(méi)有能力處理目標(biāo)方法缅帘,因子只能把方法拋給別人轴术,也就丟給其他的類去處理,因此最后一個(gè)流程為什么叫消息轉(zhuǎn)發(fā)钦无,顧名思義逗栽。
下面,我們來(lái)搞定消息轉(zhuǎn)發(fā)铃诬,入口如下祭陷,位于lookUpImpOrForward函數(shù)的尾部

從截圖中可以看出,消息轉(zhuǎn)發(fā)這里直接就是返回了一個(gè)(IMP)_objc_msgForward_impcache指針趣席。對(duì)源碼搜索一下兵志,發(fā)現(xiàn)它其實(shí)也是一段匯編實(shí)現(xiàn)

STATIC_ENTRY __objc_msgForward_impcache

    MESSENGER_START
    nop
    MESSENGER_END_SLOW

    // No stret specialization.
    b   __objc_msgForward

    END_ENTRY __objc_msgForward_impcache

//**************************************************************
    
    ENTRY __objc_msgForward

    adrp    x17, __objc_forward_handler@PAGE
    ldr x17, [x17, __objc_forward_handler@PAGEOFF]
    br  x17
    
    END_ENTRY __objc_msgForward

匯編中的調(diào)用順序是這樣
_objc_msgForward_impcache->__objc_msgForward->__objc_forward_handler,我們可以嘗試搜索一下objc_forward_handler,最終宣肚,我們可以在objc-runtime.mm里面可以找到與objc_forward_handler相關(guān)的信息

__attribute__((noreturn)) 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;

發(fā)現(xiàn)_objc_forward_handler其實(shí)是一個(gè)函數(shù)指針想罕,指向objc_defaultForwardHandler,但是這個(gè)函數(shù)只有打印信息霉涨,再往下深入按价,無(wú)法看出消息轉(zhuǎn)發(fā)更底層的執(zhí)行邏輯,蘋(píng)果對(duì)此并沒(méi)有開(kāi)源笙瑟。如果想要繼續(xù)挖掘楼镐,就只通過(guò)匯編碼逆向反推C函數(shù)的實(shí)現(xiàn),逆向是一個(gè)很大的話題往枷,需要很多知識(shí)儲(chǔ)備框产,本文無(wú)法展開(kāi)介紹。
其實(shí)错洁,如果消息機(jī)制的前兩個(gè)流程都沒(méi)命中秉宿,進(jìn)入消息轉(zhuǎn)發(fā)階段,則會(huì)調(diào)用__forwarding__函數(shù)屯碴。這個(gè)可以從xcode的打印信息里面驗(yàn)證描睦,如果調(diào)用一個(gè)沒(méi)有實(shí)現(xiàn)的方法,并且動(dòng)態(tài)解析和消息轉(zhuǎn)發(fā)都沒(méi)有處理导而,最終打印結(jié)果如下


可以看到從底層上來(lái)忱叭,調(diào)用了CF框架的_CF_forwarding_prep_0,然后就調(diào)用了___forwarding___今艺。該函數(shù)就屬于蘋(píng)果未開(kāi)源部分窑多,感謝大神MJ老師的分享,以下貼出他為我提供的一份消息轉(zhuǎn)發(fā)流程的C函數(shù)實(shí)現(xiàn)洼滚,

int __forwarding__(void *frameStackPointer, int isStret) {
    id receiver = *(id *)frameStackPointer;
    SEL sel = *(SEL *)(frameStackPointer + 8);
    const char *selName = sel_getName(sel);
    Class receiverClass = object_getClass(receiver);

    // 調(diào)用 forwardingTargetForSelector:

    if (class_respondsToSelector(receiverClass, @selector(forwardingTargetForSelector:))) {
        id forwardingTarget = [receiver forwardingTargetForSelector:sel];
        if (forwardingTarget && forwardingTarget != receiver) {
            return objc_msgSend(forwardingTarget, sel, ...);
        }
    }

    // 調(diào)用 methodSignatureForSelector 獲取方法簽名后再調(diào)用 forwardInvocation
    if (class_respondsToSelector(receiverClass, @selector(methodSignatureForSelector:))) {
        NSMethodSignature *methodSignature = [receiver methodSignatureForSelector:sel];
        if (methodSignature && class_respondsToSelector(receiverClass, @selector(forwardInvocation:))) {
            NSInvocation *invocation = [NSInvocation _invocationWithMethodSignature:methodSignature frame:frameStackPointer];

            [receiver forwardInvocation:invocation];

            void *returnValue = NULL;
            [invocation getReturnValue:&value];
            return returnValue;
        }
    }

    if (class_respondsToSelector(receiverClass,@selector(doesNotRecognizeSelector:))) {
        [receiver doesNotRecognizeSelector:sel];
    }

    // The point of no return.
    kill(getpid(), 9);
}

__forwarding__函數(shù)邏輯可以簡(jiǎn)單概括成
forwardingTargetForSelector:->methodSignatureForSelector->forwardInvocation埂息。我在功過(guò)一個(gè)流程圖來(lái)解讀一下上面的代碼

消息轉(zhuǎn)發(fā)流程

  • -(id)forwardingTargetForSelector:(SEL)aSelector—— __forwarding__首先會(huì)看類有沒(méi)有實(shí)現(xiàn)這個(gè)方法,這個(gè)方法返回的是一個(gè)id類型的轉(zhuǎn)發(fā)對(duì)象forwardingTarget,如果其不為空千康,則會(huì)通過(guò)objc_msgSend函數(shù)對(duì)其直接發(fā)送消息objc_msgSend(forwardingTarget, sel, ...);享幽,也就是說(shuō)讓轉(zhuǎn)發(fā)對(duì)象forwardingTarget去處理當(dāng)前的方法SEL。如果forwardingTargetnil拾弃,則進(jìn)入下面的方法
  • -(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector——這個(gè)方法是讓我們根據(jù)方法選擇器SEL生成一個(gè)NSMethodSignature方法簽名并返回值桩,這個(gè)方法簽名里面其實(shí)就是封裝了返回值類型,參數(shù)類型的信息豪椿。
    __forwarding__會(huì)利用這個(gè)方法簽名奔坟,生成一個(gè)NSInvocation,將其作為參數(shù)搭盾,調(diào)用- (void)forwardInvocation:(NSInvocation *)anInvocation方法咳秉。如果我們?cè)谶@里沒(méi)有返回方法簽名,系統(tǒng)則認(rèn)為我們徹底不想處理這個(gè)方法了鸯隅,就會(huì)調(diào)用doesNotRecognizeSelector:方法拋出經(jīng)典的報(bào)錯(cuò)報(bào)錯(cuò)unrecognized selector sent to instance 0xXXXXXXXX澜建,結(jié)束消息機(jī)制的全部流程。
  • - (void)forwardInvocation:(NSInvocation *)anInvocation ——如果我們?cè)谏厦嫣峁┝朔椒ê灻?code>__forwarding__則會(huì)最終調(diào)用這個(gè)方法蝌以。在這個(gè)方法里面炕舵,我們會(huì)拿到一個(gè)參數(shù)(NSInvocation *)anInvocation,這個(gè)anInvocation其實(shí)是__forwarding__對(duì)如下三個(gè)信息的封裝:
    1. anInvocation.target -- 方法調(diào)用者
    2. anInvocation.selector -- 方法名
    3. - (void)getArgument:(void *)argumentLocation atIndex:(NSInteger)idx; -- 方法參數(shù)
      因此在此方法里面跟畅,我們可以決定將消息轉(zhuǎn)發(fā)給誰(shuí)(target)咽筋,甚至還可以修改消息的參數(shù),由于anInvocation會(huì)存儲(chǔ)消息selector里面帶來(lái)的參數(shù)徊件,并且可以根據(jù)消息所對(duì)應(yīng)的方法簽名確定消息參數(shù)的個(gè)數(shù)奸攻,所以我們通過(guò)- (void)setArgument:(void *)argumentLocation atIndex:(NSInteger)idx;可以對(duì)參數(shù)進(jìn)行修改。總之你可以按照你的意愿庇忌,配置好anInvocation,然后簡(jiǎn)單一句[anInvocation invoke];即可完成消息的轉(zhuǎn)發(fā)調(diào)用舰褪,也可以不做任何處理皆疹,輕輕地來(lái),輕輕地走占拍,但是不會(huì)導(dǎo)致程序報(bào)錯(cuò)略就。

至此,Runtime的消息機(jī)制就全部梳理完畢~~


Runtime系列文章

Runtime原理探究(一)—— isa的深入體會(huì)(蘋(píng)果對(duì)isa的優(yōu)化)
Runtime原理探究(二)—— Class結(jié)構(gòu)的深入分析
Runtime原理探究(三)—— OC Class的方法緩存cache_t
Runtime原理探究(四)—— 刨根問(wèn)底消息機(jī)制
Runtime原理探究(五)—— super的本質(zhì)
[Runtime原理探究(六)—— Runtime的應(yīng)用...待續(xù)]-()
[Runtime原理探究(七)—— Runtime的API...待續(xù)]-()
Runtime原理探究(八)—— 面試題中的Runtime

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末晃酒,一起剝皮案震驚了整個(gè)濱河市表牢,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌贝次,老刑警劉巖崔兴,帶你破解...
    沈念sama閱讀 222,252評(píng)論 6 516
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡敲茄,警方通過(guò)查閱死者的電腦和手機(jī)位谋,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,886評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)堰燎,“玉大人掏父,你說(shuō)我怎么就攤上這事「鸭簦” “怎么了赊淑?”我有些...
    開(kāi)封第一講書(shū)人閱讀 168,814評(píng)論 0 361
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)仅讽。 經(jīng)常有香客問(wèn)我陶缺,道長(zhǎng),這世上最難降的妖魔是什么何什? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 59,869評(píng)論 1 299
  • 正文 為了忘掉前任组哩,我火速辦了婚禮,結(jié)果婚禮上处渣,老公的妹妹穿的比我還像新娘伶贰。我一直安慰自己,他們只是感情好罐栈,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,888評(píng)論 6 398
  • 文/花漫 我一把揭開(kāi)白布黍衙。 她就那樣靜靜地躺著,像睡著了一般荠诬。 火紅的嫁衣襯著肌膚如雪琅翻。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 52,475評(píng)論 1 312
  • 那天柑贞,我揣著相機(jī)與錄音蕉堰,去河邊找鬼傍睹。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的划乖。 我是一名探鬼主播宇姚,決...
    沈念sama閱讀 41,010評(píng)論 3 422
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼秧秉,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼污桦!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起书幕,我...
    開(kāi)封第一講書(shū)人閱讀 39,924評(píng)論 0 277
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤新荤,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后台汇,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體苛骨,經(jīng)...
    沈念sama閱讀 46,469評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡篱瞎,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,552評(píng)論 3 342
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了智袭。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片奔缠。...
    茶點(diǎn)故事閱讀 40,680評(píng)論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖吼野,靈堂內(nèi)的尸體忽然破棺而出校哎,到底是詐尸還是另有隱情,我是刑警寧澤瞳步,帶...
    沈念sama閱讀 36,362評(píng)論 5 351
  • 正文 年R本政府宣布闷哆,位于F島的核電站,受9級(jí)特大地震影響单起,放射性物質(zhì)發(fā)生泄漏抱怔。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,037評(píng)論 3 335
  • 文/蒙蒙 一嘀倒、第九天 我趴在偏房一處隱蔽的房頂上張望屈留。 院中可真熱鬧,春花似錦测蘑、人聲如沸灌危。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,519評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)勇蝙。三九已至,卻和暖如春挨约,著一層夾襖步出監(jiān)牢的瞬間味混,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,621評(píng)論 1 274
  • 我被黑心中介騙來(lái)泰國(guó)打工诫惭, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留翁锡,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 49,099評(píng)論 3 378
  • 正文 我出身青樓夕土,卻偏偏與公主長(zhǎng)得像馆衔,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子隘弊,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,691評(píng)論 2 361