iOS底層原理探索— Runtime之消息機(jī)制

探索底層原理,積累從點(diǎn)滴做起。大家好,我是Mars。

往期回顧

iOS底層原理探索—OC對象的本質(zhì)
iOS底層原理探索—class的本質(zhì)
iOS底層原理探索—KVO的本質(zhì)
iOS底層原理探索— KVC的本質(zhì)
iOS底層原理探索— Category的本質(zhì)(一)
iOS底層原理探索— Category的本質(zhì)(二)
iOS底層原理探索— 關(guān)聯(lián)對象的本質(zhì)
iOS底層原理探索— block的本質(zhì)(一)
iOS底層原理探索— block的本質(zhì)(二)
iOS底層原理探索— Runtime之isa的本質(zhì)
iOS底層原理探索— Runtime之class的本質(zhì)

今天繼續(xù)帶領(lǐng)大家探索iOS之Runtime的本質(zhì)。

前言

OC是一門動(dòng)態(tài)性比較強(qiáng)的編程語言,它的動(dòng)態(tài)性是基于RuntimeAPI。Runtime在我們的實(shí)際開發(fā)中占據(jù)著重要的地位,在面試過程中也經(jīng)常遇到Runtime相關(guān)的面試題,我們在之前幾期的探索分析時(shí)也經(jīng)常會(huì)到Runtime的底層源碼中查看相關(guān)實(shí)現(xiàn)。Runtime對于iOS開發(fā)者的重要性不言而喻,想要學(xué)習(xí)和掌握Runtime的相關(guān)技術(shù),就要從Runtime底層的一些常用數(shù)據(jù)結(jié)構(gòu)入手。掌握了它的底層結(jié)構(gòu),我們學(xué)習(xí)起來也能達(dá)到事半功倍的效果。今天研究OC消息機(jī)制

消息機(jī)制

OC語言中方法調(diào)用通過消息機(jī)制來實(shí)現(xiàn),方法調(diào)用其實(shí)都是轉(zhuǎn)換為 objc_msgSend函數(shù)調(diào)用。

objc_msgSend函數(shù).png

OC消息機(jī)制可以分為一下三個(gè)階段:

1、消息發(fā)送階段:從類及父類的方法緩存列表及方法列表查找方法;

2、動(dòng)態(tài)解析階段:如果消息發(fā)送階段沒有找到方法,則會(huì)進(jìn)入動(dòng)態(tài)解析階段,負(fù)責(zé)動(dòng)態(tài)的添加方法實(shí)現(xiàn);

3、消息轉(zhuǎn)發(fā)階段:如果也沒有實(shí)現(xiàn)動(dòng)態(tài)解析方法,則會(huì)進(jìn)行消息轉(zhuǎn)發(fā)階段,將消息轉(zhuǎn)發(fā)給可以處理消息的接受者來處理;

如果消息轉(zhuǎn)發(fā)也沒有實(shí)現(xiàn),就會(huì)報(bào)出經(jīng)典的錯(cuò)誤:unrecognzied selector sent to instance,方法找不到的錯(cuò)誤,無法識(shí)別消息。

接下來我們通過源碼分析消息機(jī)制的三個(gè)階段分別是如何實(shí)現(xiàn)的。

1、消息發(fā)送

在項(xiàng)目中方法調(diào)用的頻率很高,所以為了能夠提升效率,在底層代碼中objc_msgSend函數(shù)的實(shí)現(xiàn)是通過匯編語言編寫的,我們在源碼中找到objc-msg-arm64.s匯編文件,來具體分析一下objc_msgSend函數(shù)的實(shí)現(xiàn)。

objc_msgSend內(nèi)部實(shí)現(xiàn).png

objc_msgSend函數(shù)中首先判斷消息接收者receiver是否為空。如果傳入的消息接受者為nil則會(huì)執(zhí)行LNilOrTaggedLNilOrTagged內(nèi)部會(huì)執(zhí)行LReturnZero,而LReturnZero內(nèi)部則直接return 0

如果傳入的消息接收者receiver不為空則通過消息接收者receiverisa指針找到消息接收者的class,執(zhí)行CacheLookup從方法緩存中取查找。如果在方法緩存列表找到則執(zhí)行CacheHit,調(diào)用方法或者返回函數(shù)地址;如果找到就執(zhí)行CheckMiss。CheckMiss內(nèi)調(diào)用__objc_msgSend_uncached,方法沒有被緩存。

__objc_msgSend_uncached內(nèi)會(huì)執(zhí)行MethodTableLookup,去方法列表中查找。MethodTableLookup內(nèi)部的核心代碼__class_lookupMethodAndLoadCache3也就是c語言函數(shù)_class_lookupMethodAndLoadCache3(雙下劃線開頭變成單下劃線)。

以上分析我們用簡單的流程圖來總結(jié):

消息發(fā)送階段流程.png

接下來我們進(jìn)入_class_lookupMethodAndLoadCache3函數(shù),分析是如何從方法列表中查找方法。

_class_lookupMethodAndLoadCache3函數(shù)

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

函數(shù)內(nèi)部調(diào)用lookUpImpOrForward方法,傳入三個(gè)BOOL類型的參數(shù)。

lookUpImpOrForward 函數(shù)

IMP lookUpImpOrForward(Class cls, SEL sel, id inst, 
                       bool initialize, bool cache, bool resolver)
{
    //接收傳入的參數(shù), initialize = YES , cache = NO , resolver = YES
    IMP imp = nil;
    bool triedResolver = NO;
    runtimeLock.assertUnlocked();

    // 緩存查找, 因?yàn)閏ache傳入的為NO, 這里不會(huì)進(jìn)行緩存查找, 因?yàn)樵趨R編語言中CacheLookup已經(jīng)查找過
    if (cache) {
        imp = cache_getImp(cls, sel);
        if (imp) return imp;
    }

    runtimeLock.read();
    if (!cls->isRealized()) {
        runtimeLock.unlockRead();
        runtimeLock.write();
        realizeClass(cls);
        runtimeLock.unlockWrite();
        runtimeLock.read();
    }
    if (initialize  &&  !cls->isInitialized()) {
        runtimeLock.unlockRead();
        _class_initialize (_class_getNonMetaClass(cls, inst));
        runtimeLock.read();
    }

 retry:    
    runtimeLock.assertReading();

    // 防止動(dòng)態(tài)添加方法,緩存會(huì)變化,再次查找緩存。
    imp = cache_getImp(cls, sel);
    // 如果找到imp方法地址, 直接調(diào)用done, 返回方法地址
    if (imp) goto done;

    // 查找方法列表, 傳入類對象和方法名
    {
        // 根據(jù)sel去類對象里面查找方法
        Method meth = getMethodNoSuper_nolock(cls, sel);
        if (meth) {
            // 如果方法存在,則緩存方法,
            // 內(nèi)部調(diào)用的就是 cache_fill。
            log_and_fill_cache(cls, meth->imp, sel, inst, cls);
            // 方法緩存之后, 取出函數(shù)地址imp并返回
            imp = meth->imp;
            goto done;
        }
    }

    // 如果類方法列表中沒有找到, 則去父類的緩存中或方法列表中查找方法
    {
        unsigned attempts = unreasonableClassCount();
        // 如果父類緩存列表及方法列表均找不到方法,則去父類的父類去查找。
        for (Class curClass = cls->superclass;
             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.");
            }
            
            // 查找父類的緩存
            imp = cache_getImp(curClass, sel);
            if (imp) {
                if (imp != (IMP)_objc_msgForward_impcache) {
                    // 在父類中找到方法, 在本類中緩存方法, 注意這里傳入的是cls, 將方法緩存在本類緩存列表中, 而非父類中
                    log_and_fill_cache(cls, imp, sel, inst, curClass);
                    // 執(zhí)行done, 返回imp
                    goto done;
                }
                else {
                    // 跳出循環(huán), 停止搜索
                    break;
                }
            }
            
            // 查找父類的方法列表
            Method meth = getMethodNoSuper_nolock(curClass, sel);
            if (meth) {
                // 同樣拿到方法, 在本類進(jìn)行緩存
                log_and_fill_cache(cls, meth->imp, sel, inst, curClass);
                imp = meth->imp;
                // 執(zhí)行done, 返回imp
                goto done;
            }
        }
    }
    
    // ---------------- 消息發(fā)送階段完成,沒有找到方法實(shí)現(xiàn),進(jìn)入動(dòng)態(tài)解析階段 ---------------------
    //首先檢查是否已經(jīng)被標(biāo)記為動(dòng)態(tài)方法解析,如果沒有才會(huì)進(jìn)入動(dòng)態(tài)方法解析
    if (resolver  &&  !triedResolver) {
        runtimeLock.unlockRead();
        _class_resolveMethod(cls, sel, inst);
        runtimeLock.read();
        //將triedResolver標(biāo)記為YES,下次就不會(huì)再進(jìn)入動(dòng)態(tài)方法解析
        triedResolver = YES;
        goto retry;
    }

    // ---------------- 動(dòng)態(tài)解析階段完成,進(jìn)入消息轉(zhuǎn)發(fā)階段 ---------------------
    imp = (IMP)_objc_msgForward_impcache;
    cache_fill(cls, sel, imp, inst);

 done:
    runtimeLock.unlockRead();
    // 返回方法地址
    return imp;
}

getMethodNoSuper_nolock 函數(shù)

方法列表中查找方法

getMethodNoSuper_nolock(Class cls, SEL sel)
{
    runtimeLock.assertLocked();
    assert(cls->isRealized());
    // cls->data() 得到的是 class_rw_t
    // class_rw_t->methods 得到的是methods二維數(shù)組
    for (auto mlists = cls->data()->methods.beginLists(), 
              end = cls->data()->methods.endLists(); 
         mlists != end;
         ++mlists)
    {
         // mlists 為 method_list_t
        method_t *m = search_method_list(*mlists, sel);
        if (m) return m;
    }
    return nil;
}

getMethodNoSuper_nolock函數(shù)中通過遍歷方法列表拿到method_list_t最終通過search_method_list函數(shù)查找方法
search_method_list函數(shù)

static method_t *search_method_list(const method_list_t *mlist, SEL sel)
{
    int methodListIsFixedUp = mlist->isFixedUp();
    int methodListHasExpectedSize = mlist->entsize() == sizeof(method_t);
    // 如果方法列表已經(jīng)排序好了,則通過二分查找法查找方法,以節(jié)省時(shí)間
    if (__builtin_expect(methodListIsFixedUp && methodListHasExpectedSize, 1)) {
        return findMethodInSortedMethodList(sel, mlist);
    } else {
        //  如果方法列表沒有排序好就遍歷查找
        for (auto& meth : *mlist) {
            if (meth.name == sel) return &meth;
        }
    }
    return nil;
}

findMethodInSortedMethodList函數(shù)內(nèi)二分查找實(shí)現(xiàn)原理

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;
    // >>1 表示將變量n的各個(gè)二進(jìn)制位順序右移1位,最高位補(bǔ)二進(jìn)制0。
    // count >>= 1 如果count為偶數(shù)則值變?yōu)?count / 2)。如果count為奇數(shù)則值變?yōu)?count-1) / 2 
    for (count = list->count; count != 0; count >>= 1) {
        // probe 指向數(shù)組中間的值
        probe = base + (count >> 1);
        // 取出中間method_t的name,也就是SEL
        uintptr_t probeValue = (uintptr_t)probe->name;
        if (keyValue == probeValue) {
            // 取出 probe
            while (probe > first && keyValue == (uintptr_t)probe[-1].name) {
                probe--;
            }
           // 返回方法
            return (method_t *)probe;
        }
        // 如果keyValue > probeValue 則折半向后查詢
        if (keyValue > probeValue) {
            base = probe + 1;
            count--;
        }
    }
    
    return nil;
}

通過以上分析,我們了解了消息機(jī)制中的第一階段消息發(fā)送階段,下面我們用一張圖來總結(jié)一下整體流程:

消息發(fā)送.png

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

當(dāng)在類和父類的方法緩存列表、方法列表中都找不到方法時(shí),就會(huì)進(jìn)入動(dòng)態(tài)方法解析階段。我們在消息發(fā)送階段源碼中看到,進(jìn)入動(dòng)態(tài)方法解析階段是通過函數(shù)_class_resolveMethod。

_class_resolveMethod函數(shù)

_class_resolveMethod函數(shù).png

函數(shù)內(nèi)部會(huì)根據(jù)是元類還是類,并且類方法和對象方法的動(dòng)態(tài)方法解析是調(diào)用不同的函數(shù):
動(dòng)態(tài)解析對象方法時(shí),會(huì)調(diào)用+(BOOL)resolveInstanceMethod:(SEL)sel方法。
動(dòng)態(tài)解析類方法時(shí),會(huì)調(diào)用+(BOOL)resolveClassMethod:(SEL)sel方法。

動(dòng)態(tài)解析方法之后,會(huì)將triedResolver = YES;那么下次就不會(huì)在進(jìn)行動(dòng)態(tài)解析階段了,之后會(huì)回到消息發(fā)送階段,重新執(zhí)行retry,重新對方法查找一遍。

動(dòng)態(tài)方法解析流程圖.png

我們可以利用動(dòng)態(tài)方法解析來動(dòng)態(tài)的添加方法。我們將MPerson類中的test方法實(shí)現(xiàn)注釋掉,用other方法的實(shí)現(xiàn)來替代test方法實(shí)現(xiàn):
動(dòng)態(tài)添加方法.png

從圖中的可以看到,我們注釋掉test方法實(shí)現(xiàn)后系統(tǒng)已經(jīng)報(bào)出了警告,下面我們測試一下代碼:
測試動(dòng)態(tài)添加方法.png

當(dāng)調(diào)用MPersontest方法時(shí),打印了[MPerson other]。動(dòng)態(tài)添加方法成功。

這里需要注意class_addMethod函數(shù)用來向具有給定名稱和實(shí)現(xiàn)的類添加新方法,class_addMethod將添加一個(gè)方法實(shí)現(xiàn)的覆蓋,但是不會(huì)替換已有的實(shí)現(xiàn)。也就是說如果上述代碼中已經(jīng)實(shí)現(xiàn)了-(void)test方法,則不會(huì)再動(dòng)態(tài)添加方法。

3、消息轉(zhuǎn)發(fā)階段

如果上面兩個(gè)階段都失敗的話,就會(huì)來到第三階段:消息轉(zhuǎn)發(fā)階段。
由于OC中消息機(jī)制并不是開源的,這里就直接將消息轉(zhuǎn)發(fā)的原理告訴給大家了。

進(jìn)入消息轉(zhuǎn)發(fā)階段后,就會(huì)判斷是否指定了其它對象來執(zhí)行方法。具體查看當(dāng)前類是否實(shí)現(xiàn)了forwardingTargetForSelector函數(shù),如果返回值不為空,那么說明指定了轉(zhuǎn)發(fā)目標(biāo),那么就會(huì)讓轉(zhuǎn)發(fā)目標(biāo)處理消息。

如果forwardingTargetForSelector函數(shù)返回為nil,沒有指定轉(zhuǎn)發(fā)目標(biāo),就會(huì)調(diào)用methodSignatureForSelector方法,用來返回一個(gè)方法簽名,這也是跳轉(zhuǎn)方法的最后機(jī)會(huì)。

如果methodSignatureForSelector方法返回正確的方法簽名就會(huì)調(diào)用forwardInvocation方法,forwardInvocation方法內(nèi)提供一個(gè)NSInvocation類型的參數(shù),NSInvocation封裝了一個(gè)方法的調(diào)用,包括方法的調(diào)用者,方法名,以及方法的參數(shù)。在forwardInvocation函數(shù)內(nèi)修改方法調(diào)用對象即可。

如果methodSignatureForSelector返回的為nil,就會(huì)來到doseNotRecognizeSelector:方法內(nèi)部,程序crash報(bào)出經(jīng)典的錯(cuò)誤unrecognized selector sent to instance。

消息轉(zhuǎn)發(fā).png

至此,OC消息機(jī)制的分析就告一段落,OC中的方法調(diào)用其實(shí)都是轉(zhuǎn)成了objc_msgSend函數(shù)的調(diào)用,給方法調(diào)用者(receiver)發(fā)送一條消息(selector方法名)。方法調(diào)用過程包括三個(gè)階段:消息發(fā)送、動(dòng)態(tài)方法解析、消息轉(zhuǎn)發(fā)。

更多技術(shù)知識(shí)請關(guān)注公眾號(hào)
iOS進(jìn)階


iOS進(jìn)階.jpg
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • Swift1> Swift和OC的區(qū)別1.1> Swift沒有地址/指針的概念1.2> 泛型1.3> 類型嚴(yán)謹(jǐn) 對...
    cosWriter閱讀 11,619評論 1 32
  • 方法調(diào)用的本質(zhì) Runtime-Demo 本文我們探尋方法調(diào)用的本質(zhì),首先通過一段代碼,將方法調(diào)用代碼轉(zhuǎn)為c++代...
    二斤寂寞閱讀 501評論 0 1
  • 如果想了解Runtime的實(shí)際應(yīng)用請看Runtime全面剖析之簡單使用 一:Runtime簡介二: Runtime...
    iYeso閱讀 841評論 0 2
  • 方法調(diào)用的本質(zhì) 本文我們探尋方法調(diào)用的本質(zhì),首先通過一段代碼,將方法調(diào)用代碼轉(zhuǎn)為c++代碼查看方法調(diào)用的本質(zhì)是什么...
    xx_cc閱讀 6,776評論 6 28
  • 仰天大笑出門去,我輩豈是蓬蒿人。 踏足遠(yuǎn)方,遙遙60華里,不禁會(huì)讓我們在寒風(fēng)與激情的碰撞中有些想松...
    清歡1223閱讀 370評論 0 4

友情鏈接更多精彩內(nèi)容