iOS 消息流程分析之 動(dòng)態(tài)方法決議 & 消息轉(zhuǎn)發(fā)

在前面兩篇文章objc_msgSend分析之快速查找objc_msgSend分析之慢速查找中,分別分析了objc_msgSend快速查找慢速查找,在這兩種都沒找到方法實(shí)現(xiàn)的情況下,蘋果給了兩個(gè)建議

  • 動(dòng)態(tài)方法決議:慢速查找流程未找到后,會(huì)執(zhí)行一次動(dòng)態(tài)方法決議
  • 消息轉(zhuǎn)發(fā):如果動(dòng)態(tài)方法決議仍然沒有找到實(shí)現(xiàn),則進(jìn)行消息轉(zhuǎn)發(fā)

如果這兩個(gè)建議都沒有做任何操作,就會(huì)報(bào)我們?nèi)粘i_發(fā)中常見的方法未實(shí)現(xiàn)崩潰報(bào)錯(cuò),其步驟如下

  • 定義LGPerson類,其中say666實(shí)例方法 和 sayNB類方法均沒有實(shí)現(xiàn)

  • main中 分別調(diào)用LGPerson實(shí)例方法say666類方法sayNB,運(yùn)行程序,均會(huì)報(bào)錯(cuò),提示方法未實(shí)現(xiàn),如下所示

    • 調(diào)用實(shí)例方法say666的報(bào)錯(cuò)結(jié)果
    實(shí)例方法報(bào)錯(cuò)
    • 調(diào)用類方法sayNB的報(bào)錯(cuò)結(jié)果
    • image

方法未實(shí)現(xiàn)報(bào)錯(cuò)源碼

根據(jù)慢速查找的源碼,我們發(fā)現(xiàn),其報(bào)錯(cuò)最后都是走到__objc_msgForward_impcache方法,以下是報(bào)錯(cuò)流程的源碼

STATIC_ENTRY __objc_msgForward_impcache

// No stret specialization.
b   __objc_msgForward

END_ENTRY __objc_msgForward_impcache

//??
ENTRY __objc_msgForward

adrp    x17, __objc_forward_handler@PAGE
ldr p17, [x17, __objc_forward_handler@PAGEOFF]
TailCallFunctionPointer x17

END_ENTRY __objc_msgForward

  • 匯編實(shí)現(xiàn)中查找__objc_forward_handler,并沒有找到,在源碼中去掉一個(gè)下劃線進(jìn)行全局搜索_objc_forward_handler,有如下實(shí)現(xiàn),本質(zhì)是調(diào)用的objc_defaultForwardHandler方法
// Default forward handler halts the process.
__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;

看著objc_defaultForwardHandler有沒有很眼熟,這就是我們在日常開發(fā)中最常見的錯(cuò)誤:沒有實(shí)現(xiàn)函數(shù),運(yùn)行程序,崩潰時(shí)報(bào)的錯(cuò)誤提示。

下面,我們來講講如何在崩潰前,如何操作,可以防止方法未實(shí)現(xiàn)的崩潰。

三次方法查找的挽救機(jī)會(huì)

根據(jù)蘋果的兩個(gè)建議,我們一共有三次挽救的機(jī)會(huì):

  • 【第一次機(jī)會(huì)】動(dòng)態(tài)方法決議

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

    • 【第二次機(jī)會(huì)】快速轉(zhuǎn)發(fā)
    • 【第三次機(jī)會(huì)】慢速轉(zhuǎn)發(fā)

【第一次機(jī)會(huì)】動(dòng)態(tài)方法決議

慢速查找流程未找到方法實(shí)現(xiàn)時(shí),首先會(huì)嘗試一次動(dòng)態(tài)方法決議,其源碼實(shí)現(xiàn)如下:

static NEVER_INLINE IMP
resolveMethod_locked(id inst, SEL sel, Class cls, int behavior)
{
    runtimeLock.assertLocked();
    ASSERT(cls->isRealized());

    runtimeLock.unlock();
    //對象 -- 類
    if (! cls->isMetaClass()) { //類不是元類,調(diào)用對象的解析方法
        // try [cls resolveInstanceMethod:sel]
        resolveInstanceMethod(inst, sel, cls);
    } 
    else {//如果是元類,調(diào)用類的解析方法, 類 -- 元類
        // try [nonMetaClass resolveClassMethod:sel]
        // and [cls resolveInstanceMethod:sel]
        resolveClassMethod(inst, sel, cls);
        //為什么要有這行代碼? -- 類方法在元類中是對象方法,所以還是需要查詢元類中對象方法的動(dòng)態(tài)方法決議
        if (!lookUpImpOrNil(inst, sel, cls)) { //如果沒有找到或者為空,在元類的對象方法解析方法中查找
            resolveInstanceMethod(inst, sel, cls);
        }
    }

    // chances are that calling the resolver have populated the cache
    // so attempt using it
    //如果方法解析中將其實(shí)現(xiàn)指向其他方法,則繼續(xù)走方法查找流程
    return lookUpImpOrForward(inst, sel, cls, behavior | LOOKUP_CACHE);
}

主要分為以下幾步

  • 判斷類是否是元類
    • 如果是,執(zhí)行實(shí)例方法的動(dòng)態(tài)方法決議resolveInstanceMethod
    • 如果是元類,執(zhí)行類方法的動(dòng)態(tài)方法決議resolveClassMethod,如果在元類中沒有找到或者為,則在元類實(shí)例方法的動(dòng)態(tài)方法決議resolveInstanceMethod中查找,主要是因?yàn)?code>類方法在元類中是實(shí)例方法,所以還需要查找元類中實(shí)例方法的動(dòng)態(tài)方法決議
  • 如果動(dòng)態(tài)方法決議中,將其實(shí)現(xiàn)指向了其他方法,則繼續(xù)查找指定的imp,即繼續(xù)慢速查找lookUpImpOrForward流程

其流程如下

實(shí)例方法

針對實(shí)例方法調(diào)用,在快速-慢速查找均沒有找到實(shí)例方法的實(shí)現(xiàn)時(shí),我們有一次挽救的機(jī)會(huì),即嘗試一次動(dòng)態(tài)方法決議,由于是實(shí)例方法,所以會(huì)走到resolveInstanceMethod方法,其源碼如下

static void resolveInstanceMethod(id inst, SEL sel, Class cls)
{
    runtimeLock.assertUnlocked();
    ASSERT(cls->isRealized());
    SEL resolve_sel = @selector(resolveInstanceMethod:);
    
    // look的是 resolveInstanceMethod --相當(dāng)于是發(fā)送消息前的容錯(cuò)處理
    if (!lookUpImpOrNil(cls, resolve_sel, cls->ISA())) {
        // Resolver not implemented.
        return;
    }

    BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
    bool resolved = msg(cls, resolve_sel, sel); //發(fā)送resolve_sel消息

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

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

主要分為以下幾個(gè)步驟:

  • 發(fā)送resolveInstanceMethod消息前,需要查找cls類中是否有該方法的實(shí)現(xiàn),即通過lookUpImpOrNil方法又會(huì)進(jìn)入lookUpImpOrForward慢速查找流程查找resolveInstanceMethod方法
    • 如果沒有,則直接返回
    • 如果有,則發(fā)送resolveInstanceMethod消息
  • 再次慢速查找實(shí)例方法的實(shí)現(xiàn),即通過lookUpImpOrNil方法又會(huì)進(jìn)入lookUpImpOrForward慢速查找流程查找實(shí)例方法

崩潰修改

所以,針對實(shí)例方法say666未實(shí)現(xiàn)的報(bào)錯(cuò)崩潰,可以通過在類中重寫resolveInstanceMethod類方法,并將其指向其他方法的實(shí)現(xiàn),即在LGPerson中重寫resolveInstanceMethod類方法,將實(shí)例方法say666的實(shí)現(xiàn)指向sayMaster方法實(shí)現(xiàn),如下所示

+ (BOOL)resolveInstanceMethod:(SEL)sel{
    if (sel == @selector(say666)) {
        NSLog(@"%@ 來了", NSStringFromSelector(sel));
        //獲取sayMaster方法的imp
        IMP imp = class_getMethodImplementation(self, @selector(sayMaster));
        //獲取sayMaster的實(shí)例方法
        Method sayMethod  = class_getInstanceMethod(self, @selector(sayMaster));
        //獲取sayMaster的豐富簽名
        const char *type = method_getTypeEncoding(sayMethod);
        //將sel的實(shí)現(xiàn)指向sayMaster
        return class_addMethod(self, sel, imp, type);
    }

    return [super resolveInstanceMethod:sel];
}

重新運(yùn)行,其打印結(jié)果如下

從結(jié)果中可以發(fā)現(xiàn),resolveInstanceMethod動(dòng)態(tài)決議方法中“來了”打印了兩次,這是為什么呢?通過堆棧信息可以看出

    • 堆棧信息
  • 【第一次動(dòng)態(tài)決議】第一次的“來了”是在查找say666方法時(shí)會(huì)進(jìn)入動(dòng)態(tài)方法決議

  • 【第二次動(dòng)態(tài)決議】第二次“來了”是在慢速轉(zhuǎn)發(fā)流程中調(diào)用了CoreFoundation框架中的NSObject(NSObject) methodSignatureForSelector:后,會(huì)再次進(jìn)入動(dòng)態(tài)決議

注:詳細(xì)的分析流程請看文末的問題探索

類方法

針對類方法,與實(shí)例方法類似,同樣可以通過重寫resolveClassMethod類方法來解決前文的崩潰問題,即在LGPerson類中重寫該方法,并將sayNB類方法的實(shí)現(xiàn)指向類方法lgClassMethod

+ (BOOL)resolveClassMethod:(SEL)sel{
    
    if (sel == @selector(sayNB)) {
        NSLog(@"%@ 來了", NSStringFromSelector(sel));
        
        IMP imp = class_getMethodImplementation(objc_getMetaClass("LGPerson"), @selector(lgClassMethod));
        Method lgClassMethod  = class_getInstanceMethod(objc_getMetaClass("LGPerson"), @selector(lgClassMethod));
        const char *type = method_getTypeEncoding(lgClassMethod);
        return class_addMethod(objc_getMetaClass("LGPerson"), sel, imp, type);
    }
    
    return [super resolveClassMethod:sel];
}

resolveClassMethod類方法的重寫需要注意一點(diǎn),傳入的cls不再是,而是元類,可以通過objc_getMetaClass方法獲取類的元類,原因是因?yàn)?code>類方法在元類中是實(shí)例方法

優(yōu)化

上面的這種方式是單獨(dú)在每個(gè)類中重寫,有沒有更好的,一勞永逸的方法呢?其實(shí)通過方法慢速查找流程可以發(fā)現(xiàn)其查找路徑有兩條

實(shí)例方法:類 -- 父類 -- 根類 -- nil
類方法:元類 -- 根元類 -- 根類 -- nil
它們的共同點(diǎn)是如果前面沒找到,都會(huì)來到根類即NSObject中查找,所以我們是否可以將上述的兩個(gè)方法統(tǒng)一整合在一起呢?答案是可以的,可以通過NSObject添加分類的方式來實(shí)現(xiàn)統(tǒng)一處理,而且由于類方法的查找,在其繼承鏈,查找的也是實(shí)例方法,所以可以將實(shí)例方法 和 類方法的統(tǒng)一處理放在resolveInstanceMethod方法中,如下所示

+ (BOOL)resolveInstanceMethod:(SEL)sel{
    if (sel == @selector(say666)) {
        NSLog(@"%@ 來了", NSStringFromSelector(sel));
        
        IMP imp = class_getMethodImplementation(self, @selector(sayMaster));
        Method sayMethod  = class_getInstanceMethod(self, @selector(sayMaster));
        const char *type = method_getTypeEncoding(sayMethod);
        return class_addMethod(self, sel, imp, type);
    }else if (sel == @selector(sayNB)) {
        NSLog(@"%@ 來了", NSStringFromSelector(sel));
        
        IMP imp = class_getMethodImplementation(objc_getMetaClass("LGPerson"), @selector(lgClassMethod));
        Method lgClassMethod  = class_getInstanceMethod(objc_getMetaClass("LGPerson"), @selector(lgClassMethod));
        const char *type = method_getTypeEncoding(lgClassMethod);
        return class_addMethod(objc_getMetaClass("LGPerson"), sel, imp, type);
    }
    return NO;
}

這種方式的實(shí)現(xiàn),正好與源碼中針對類方法的處理邏輯是一致的,即完美闡述為什么調(diào)用了類方法動(dòng)態(tài)方法決議,還要調(diào)用對象方法動(dòng)態(tài)方法決議,其根本原因還是類方法在元類中的實(shí)例方法

當(dāng)然,上面這種寫法還是會(huì)有其他的問題,比如系統(tǒng)方法也會(huì)被更改,針對這一點(diǎn),是可以優(yōu)化的,即我們可以針對自定義類中方法統(tǒng)一方法名的前綴,根據(jù)前綴來判斷是否是自定義方法,然后統(tǒng)一處理自定義方法,例如可以在崩潰前pop到首頁,主要是用于app線上防崩潰的處理,提升用戶的體驗(yàn)。

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

在慢速查找的流程中,我們了解到,如果快速+慢速?zèng)]有找到方法實(shí)現(xiàn),動(dòng)態(tài)方法決議也不行,就使用消息轉(zhuǎn)發(fā),但是,我們找遍了源碼也沒有發(fā)現(xiàn)消息轉(zhuǎn)發(fā)的相關(guān)源碼,可以通過以下方式來了解,方法調(diào)用崩潰前都走了哪些方法

  • 通過instrumentObjcMessageSends方式打印發(fā)送消息的日志

  • 通過hopper/IDA反編譯

通過instrumentObjcMessageSends

通過lookUpImpOrForward --> log_and_fill_cache --> logMessageSend,在logMessageSend源碼下方找到instrumentObjcMessageSends的源碼實(shí)現(xiàn),所以,在main中調(diào)用instrumentObjcMessageSends打印方法調(diào)用的日志信息,有以下兩點(diǎn)準(zhǔn)備工作
1、打開 objcMsgLogEnabled 開關(guān),即調(diào)用instrumentObjcMessageSends方法時(shí),傳入YES

2、在main中通過extern 聲明instrumentObjcMessageSends方法

extern void instrumentObjcMessageSends(BOOL flag);

int main(int argc, const char * argv[]) {
    @autoreleasepool {

        LGPerson *person = [LGPerson alloc];
        instrumentObjcMessageSends(YES);
        [person sayHello];
        instrumentObjcMessageSends(NO);
        NSLog(@"Hello, World!");
    }
    return 0;
}

通過logMessageSend源碼,了解到消息發(fā)送打印信息存儲在/tmp/msgSends 目錄,如下所示


運(yùn)行代碼,并前往/tmp/msgSends 目錄,發(fā)現(xiàn)有msgSends開頭的日志文件,打開發(fā)現(xiàn)在崩潰前,執(zhí)行了以下方法

  • 兩次動(dòng)態(tài)方法決議resolveInstanceMethod方法

  • 兩次消息快速轉(zhuǎn)發(fā)forwardingTargetForSelector方法

  • 兩次消息慢速轉(zhuǎn)發(fā)methodSignatureForSelector + resolveInvocation

    消息發(fā)送日志詳情

通過hopper/IDA反編譯

Hopper和IDA是一個(gè)可以幫助我們靜態(tài)分析可視性文件的工具,可以將可執(zhí)行文件反匯編成偽代碼、控制流程圖等,下面以Hopper為例(注:hopper高級版本是一款收費(fèi)軟件,針對比較簡單的反匯編需求來說,demo版本足夠使用了)

  • 運(yùn)行程序崩潰,查看堆棧信息

  • 發(fā)現(xiàn)___forwarding___來自CoreFoundation

通過image list,讀取整個(gè)鏡像文件,然后搜索CoreFoundation,查看其可執(zhí)行文件的路徑


    • 通過文件路徑,找到CoreFoundation可執(zhí)行文件

    • 打開hopper,選擇Try the Demo,然后將上一步的可執(zhí)行文件拖入hopper進(jìn)行反匯編,選擇x86(64 bits)
    • hopper選擇Demo版本
    • hopper反匯編

以下是反匯編后的界面,主要使用上面的三個(gè)功能,分別是 匯編、流程圖、偽代碼

hoppper主要使用的三個(gè)功能

通過左側(cè)的搜索框搜索__forwarding_prep_0___,然后選擇偽代碼

  • 以下是__forwarding_prep_0___的匯編偽代碼,跳轉(zhuǎn)至___forwarding___

以下是___forwarding___的偽代碼實(shí)現(xiàn),首先是查看是否實(shí)現(xiàn)forwardingTargetForSelector方法,如果沒有響應(yīng),跳轉(zhuǎn)至loc_6459b即快速轉(zhuǎn)發(fā)沒有響應(yīng),進(jìn)入慢速轉(zhuǎn)發(fā)流程,

  • 跳轉(zhuǎn)至loc_6459b,在其下方判斷是否響應(yīng)methodSignatureForSelector方法,

  • 如果沒有響應(yīng),跳轉(zhuǎn)至loc_6490b,則直接報(bào)錯(cuò)

  • 如果獲取methodSignatureForSelector方法簽名為nil,也是直接報(bào)錯(cuò)


    • 所以,通過上面兩種查找方式可以驗(yàn)證,消息轉(zhuǎn)發(fā)的方法有3個(gè)

  • 【快速轉(zhuǎn)發(fā)】forwardingTargetForSelector

  • 【慢速轉(zhuǎn)發(fā)】

    • methodSignatureForSelector
    • forwardInvocation

所以,綜上所述,消息轉(zhuǎn)發(fā)整體的流程如下

    • image

消息轉(zhuǎn)發(fā)的處理主要分為兩部分:

  • 【快速轉(zhuǎn)發(fā)】當(dāng)慢速查找,以及動(dòng)態(tài)方法決議均沒有找到實(shí)現(xiàn)時(shí),進(jìn)行消息轉(zhuǎn)發(fā),首先是進(jìn)行快速消息轉(zhuǎn)發(fā),即走到forwardingTargetForSelector方法

    • 如果返回消息接收者,在消息接收者中還是沒有找到,則進(jìn)入另一個(gè)方法的查找流程

    • 如果返回nil,則進(jìn)入慢速消息轉(zhuǎn)發(fā)

  • 【慢速轉(zhuǎn)發(fā)】執(zhí)行到methodSignatureForSelector方法

    • 如果返回的方法簽名為nil,則直接崩潰報(bào)錯(cuò)

    • 如果返回的方法簽名不為nil,走到forwardInvocation方法中,對invocation事務(wù)進(jìn)行處理,如果不處理也不會(huì)報(bào)錯(cuò)

第二次機(jī)會(huì)】快速轉(zhuǎn)發(fā)

針對前文的崩潰問題,如果動(dòng)態(tài)方法決議也沒有找到實(shí)現(xiàn),則需要在LGPerson中重寫forwardingTargetForSelector方法,將LGPerson的實(shí)例方法的接收者指定為LGStudent的對象(LGStudent類中有say666的具體實(shí)現(xiàn)),如下所示

- (id)forwardingTargetForSelector:(SEL)aSelector{
    NSLog(@"%s - %@",__func__,NSStringFromSelector(aSelector));

//     runtime + aSelector + addMethod + imp
    //將消息的接收者指定為LGStudent,在LGStudent中查找say666的實(shí)現(xiàn)
    return [LGStudent alloc];
}

執(zhí)行結(jié)果如下


也可以直接不指定消息接收者,直接調(diào)用父類的該方法,如果還是沒有找到,則直接報(bào)錯(cuò)

【第三次機(jī)會(huì)】慢速轉(zhuǎn)發(fā)

針對第二次機(jī)會(huì)即快速轉(zhuǎn)發(fā)中還是沒有找到,則進(jìn)入最后的一次挽救機(jī)會(huì),即在LGPerson中重寫methodSignatureForSelector,如下所示

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
    NSLog(@"%s - %@",__func__,NSStringFromSelector(aSelector));
    return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}

- (void)forwardInvocation:(NSInvocation *)anInvocation{
    NSLog(@"%s - %@",__func__,anInvocation);
}

打印結(jié)果如下,發(fā)現(xiàn)forwardInvocation方法中不對invocation進(jìn)行處理,也不會(huì)崩潰報(bào)錯(cuò)

也可以處理invocation事務(wù),如下所示,修改invocationtarget[LGStudent alloc],調(diào)用 [anInvocation invoke] 觸發(fā) 即LGPerson類的say666實(shí)例方法的調(diào)用會(huì)調(diào)用LGStudentsay666方法

- (void)forwardInvocation:(NSInvocation *)anInvocation{
    NSLog(@"%s - %@",__func__,anInvocation);
    anInvocation.target = [LGStudent alloc];
    [anInvocation invoke];
}

打印結(jié)果如下

所以,由上述可知,無論在forwardInvocation方法中是否處理invocation事務(wù),程序都不會(huì)崩潰。

“動(dòng)態(tài)方法決議為什么執(zhí)行兩次?” 問題探索

在前文中提及了動(dòng)態(tài)方法決議方法執(zhí)行了兩次,有以下兩種分析方式

啟用上帝視角的探索

在慢速查找流程中,我們了解到resolveInstanceMethod方法的執(zhí)行是通過lookUpImpOrForward --> resolveMethod_locked --> resolveInstanceMethod來到resolveInstanceMethod源碼,在源碼中通過發(fā)送resolve_sel消息觸發(fā),如下所示

所以可以在resolveInstanceMethod方法中IMP imp = lookUpImpOrNil(inst, sel, cls);處加一個(gè)斷點(diǎn),通過bt打印堆棧信息來看到底發(fā)生了什么

  • resolveInstanceMethod方法中IMP imp = lookUpImpOrNil(inst, sel, cls);處加一個(gè)斷點(diǎn),運(yùn)行程序,直到第一次“來了”,通過bt查看第一次動(dòng)態(tài)方法決議的堆棧信息,此時(shí)的sel是say666

    第一次動(dòng)態(tài)方法決議堆棧信息

繼續(xù)往下執(zhí)行,直到第二次“來了”打印,查看堆棧信息,在第二次中,我們可以看到是通過CoreFoundation-[NSObject(NSObject) methodSignatureForSelector:]方法,然后通過class_getInstanceMethod再次進(jìn)入動(dòng)態(tài)方法決議,

第二次動(dòng)態(tài)方法決議堆棧信息

通過上一步的堆棧信息,我們需要去看看CoreFoundation中到底做了什么?通過Hopper反匯編CoreFoundation的可執(zhí)行文件,查看methodSignatureForSelector方法的偽代碼

methodSignatureForSelector偽代碼進(jìn)入方式

通過methodSignatureForSelector偽代碼進(jìn)入___methodDescriptionForSelector的實(shí)現(xiàn)

進(jìn)入 ___methodDescriptionForSelector的偽代碼實(shí)現(xiàn),結(jié)合匯編的堆棧打印,可以看到,在___methodDescriptionForSelector這個(gè)方法中調(diào)用了objc4-781class_getInstanceMethod

在objc中的源碼中搜索class_getInstanceMethod,其源碼實(shí)現(xiàn)如下所示

這一點(diǎn)可以通過代碼調(diào)試來驗(yàn)證,如下所示,在class_getInstanceMethod方法處加一個(gè)斷點(diǎn),在執(zhí)行了methodSignatureForSelector方法后,返回了簽名,說明方法簽名是生效的,蘋果在走到invocation之前,給了開發(fā)者一次機(jī)會(huì)再去查詢,所以走到class_getInstanceMethod這里,又去走了一遍方法查詢say666,然后會(huì)再次走到動(dòng)態(tài)方法決議

class_getInstanceMethod方法調(diào)試驗(yàn)證

所以,上述的分析也印證了前文中resolveInstanceMethod方法執(zhí)行了兩次的原因

無上帝視角的探索

如果在沒有上帝視角的情況下,我們也可以通過代碼推導(dǎo)在哪里再次調(diào)用了動(dòng)態(tài)方法決議

  • LGPerson中重寫resolveInstanceMethod方法,并加上class_addMethod操作即賦值IMP,此時(shí)resolveInstanceMethod會(huì)走兩次嗎?

去掉resolveInstanceMethod方法中的賦值IMP,在LGPerson類中重寫forwardingTargetForSelector方法,并指定返回值為[LGStudent alloc],重新運(yùn)行,如果resolveInstanceMethod打印了兩次,說明是在forwardingTargetForSelector方法之前執(zhí)行了 動(dòng)態(tài)方法決議,反之,在forwardingTargetForSelector方法之后

image
  • 【結(jié)論】:發(fā)現(xiàn)resolveInstanceMethod中的打印還是只打印了一次,數(shù)排名第二次動(dòng)態(tài)方法決議 在forwardingTargetForSelector方法后* 在LGPerson中重寫 methodSignatureForSelectorforwardInvocation,運(yùn)行

  • 【結(jié)論】:第二次動(dòng)態(tài)方法決議methodSignatureForSelectorforwardInvocation方法之間

第二種分析同樣可以論證前文中resolveInstanceMethod執(zhí)行了兩次的原因

經(jīng)過上面的論證,我們了解到其實(shí)在慢速小子轉(zhuǎn)發(fā)流程中,在methodSignatureForSelectorforwardInvocation方法之間還有一次動(dòng)態(tài)方法決議,即蘋果再次給的一個(gè)機(jī)會(huì),如下圖所示

總結(jié)

到目前為止,objc_msgSend發(fā)送消息的流程就分析完成了,在這里簡單總結(jié)下

  • 【快速查找流程】首先,在類的緩存cache中查找指定方法的實(shí)現(xiàn)

  • 【慢速查找流程】如果緩存中沒有找到,則在類的方法列表中查找,如果還是沒找到,則去父類鏈的緩存和方法列表中查找

  • 【動(dòng)態(tài)方法決議】如果慢速查找還是沒有找到時(shí),第一次補(bǔ)救機(jī)會(huì)就是嘗試一次動(dòng)態(tài)方法決議,即重寫resolveInstanceMethod/resolveClassMethod 方法

  • 【消息轉(zhuǎn)發(fā)】如果動(dòng)態(tài)方法決議還是沒有找到,則進(jìn)行消息轉(zhuǎn)發(fā),消息轉(zhuǎn)發(fā)中有兩次補(bǔ)救機(jī)會(huì):快速轉(zhuǎn)發(fā)+慢速轉(zhuǎn)發(fā)

  • 如果轉(zhuǎn)發(fā)之后也沒有,則程序直接報(bào)錯(cuò)崩潰unrecognized selector sent to instance

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

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

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