iOS原理探索09--objc_msgSend流程分析之 動態(tài)方法決議 & 消息轉(zhuǎn)發(fā)

在前面的兩篇博客iOS原理探索08--objc_msgSend慢速查找流程分析iOS原理探索07--objc_msgSend快速查找流程分析中大脉,我們知道方法的調(diào)用查找流程:先在緩存中進(jìn)行快速查找朋譬,如果快速查找沒有找到,那么會進(jìn)入慢速查找流实抡,在方法的列表中進(jìn)行查找肮帐,在這慢速查找流程結(jié)束后纷责,沒有找到的時候,會執(zhí)行一次動態(tài)決議方法臣樱,如果動態(tài)決議沒有找到靶擦,會進(jìn)行消息轉(zhuǎn)發(fā)。如果消息轉(zhuǎn)發(fā)也沒有那么就會來到我們平時開發(fā)中的unrecognized selector sent to instance報錯提示雇毫!

日常開發(fā)中沒有方法實(shí)現(xiàn)的報錯分析

  • 條件設(shè)置:創(chuàng)建一個LGPerson類玄捕,添加一個+(void)sayNB方法,但是并沒有實(shí)現(xiàn)該方法棚放;在main函數(shù)中通過初始化一個LGPerson類對象枚粘,調(diào)用sayNB方法,運(yùn)行程序席吴。
運(yùn)行結(jié)果

可以看到赌结,這里已經(jīng)報錯了。下面可以跟進(jìn)源碼來看一下報錯的源代碼實(shí)現(xiàn)孝冒。

  • 源代碼實(shí)現(xiàn)
    根據(jù)慢速查找發(fā)現(xiàn)柬姚,報錯都走到了根據(jù)慢速查找的源碼,我們發(fā)現(xiàn)庄涡,其報錯最后都是走到__objc_msgForward_impcache方法
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量承,并沒有找到該方法的實(shí)現(xiàn),那么在源碼中去掉一個下劃線進(jìn)行全局搜索_objc_forward_handler,最后發(fā)現(xiàn)默認(rèn)執(zhí)行的是objc_defaultForwardHandler方法撕捍。下面是源碼實(shí)現(xiàn)拿穴。
// 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;

打印輸出的就是如上方法為找到的錯誤提示。

動態(tài)決議

  • 蘋果建議在慢速查找沒有找到方法實(shí)現(xiàn)的時候忧风,使用動態(tài)決議方法默色,可以算是補(bǔ)救崩潰的一個機(jī)會吧。
  • 這個補(bǔ)救崩潰的機(jī)會就在lookUpImpOrForward方法中
//如果沒有找到方法實(shí)現(xiàn)狮腿,嘗試方法解析
    if (slowpath(behavior & LOOKUP_RESOLVER)) {
        //動態(tài)方法決議的控制條件腿宰,表示流程只走一次
        behavior ^= LOOKUP_RESOLVER;
        return resolveMethod_locked(inst, sel, cls, behavior);
    }
  • resolveMethod_locked源碼實(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);
        //為什么要有這行代碼? -- 類方法在元類中是對象方法贴硫,所以還是需要查詢元類中對象方法的動態(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);
}

所以如果沒有實(shí)現(xiàn)的方法英遭,我們可以通過resolveInstanceMethod進(jìn)行一次補(bǔ)救间护,

  • 主要流程分為以下幾個步驟

類不是元類,調(diào)用對象的解析方法贪绘,執(zhí)行的是-(void) resolveInstanceMethod(id inst, SEL sel, Class cls)方法兑牡;
如果是元類央碟,調(diào)用類的解析方法+ (BOOL)resolveClassMethod:(SEL)sel税灌, 即類 -- 元類

  • 分析流程圖
    動態(tài)解析流程圖
  • 實(shí)例方法的動態(tài)決議
static void resolveInstanceMethod(id inst, SEL sel, Class cls)
{
    runtimeLock.assertUnlocked();
    ASSERT(cls->isRealized());
    SEL resolve_sel = @selector(resolveInstanceMethod:);
    
    // look的是 resolveInstanceMethod --相當(dāng)于是發(fā)送消息前的容錯處理
    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));
        }
    }
}

主要分為以下幾個步驟:

  • 在發(fā)送resolveInstanceMethod消息前,需要查找cls類是否有該方法的實(shí)現(xiàn)亿虽,即通過lookUpImpOrNil方法又會進(jìn)入lookUpImpOrForward慢速查找流程查找resolveInstanceMethod方法

  • 如果沒有菱涤,則直接返回

  • 如果,則發(fā)送resolveInstanceMethod消息再次慢速查找實(shí)例方法的實(shí)現(xiàn)洛勉,即通過lookUpImpOrNil方法又會進(jìn)入lookUpImpOrForward慢速查找流程查找實(shí)例方法

  • 實(shí)例方法示例:say666未實(shí)現(xiàn)的方法粘秆,通過resolveInstanceMethod動態(tài)決議后由sayMaster進(jìn)行實(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];
}
  • 類方法的動態(tài)決議
    解決未實(shí)現(xiàn)類方法調(diào)用崩潰問題收毫,比如上面的+(void)sayNB;方法添加如下代碼
+ (BOOL)resolveClassMethod:(SEL)sel{
    NSLog(@"%@ 來了",NSStringFromSelector(sel));
    if (sel == @selector(sayNB)) {

        IMP imp           = class_getMethodImplementation(objc_getMetaClass("LGPerson"), @selector(lgClassMethod));
        Method sayMMethod = class_getInstanceMethod(objc_getMetaClass("LGPerson"), @selector(lgClassMethod));
        const char *type  = method_getTypeEncoding(sayMMethod);
        return class_addMethod(objc_getMetaClass("LGPerson"), sel, imp, type);
    }
    return [super resolveClassMethod:sel];
}

輸出結(jié)果
根據(jù)結(jié)果我們發(fā)現(xiàn)沒有再出現(xiàn)崩潰的現(xiàn)象攻走,并且LGPerson打印了lgClassMethod方法。注意:resolveClassMethod類方法的重寫需要注意一點(diǎn)此再,傳入的cls不再是類昔搂,而是元類,可以通過objc_getMetaClass方法獲取類的元類输拇,原因是因為類方法在元類中是實(shí)例方法摘符。

動態(tài)決議相關(guān)優(yōu)化

我們通過繼承鏈可以知道,實(shí)例方法:類 -- 父類 -- 根類 -- nil,類方法元類 -- 根元類 -- 根類 -- nil逛裤。

isa流程圖.png

那么我們可以將動態(tài)決議方法瘩绒,放在NSObject的分類里面,統(tǒng)一實(shí)現(xiàn)未實(shí)現(xiàn)的方法带族。當(dāng)然我們需要注意的是锁荔,這樣會攔截系統(tǒng)的方法,我們可以按照項目模塊針對自定義類中方法統(tǒng)一方法名的前綴判斷蝙砌。

+ (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;
}

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

如果方法的快速查找堕战、慢速查找以及動態(tài)決議都找不到的情況下,就會進(jìn)行一個消息轉(zhuǎn)發(fā)拍霜,我們可以利用消息轉(zhuǎn)發(fā)來做一些操作避免出現(xiàn)崩潰嘱丢,這同樣是蘋果給我們的一個避免發(fā)生錯誤的機(jī)會。

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

通過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方法時,傳入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;
}

可以根據(jù)源碼提供的路徑/tmp/msgSends查看日志

bool logMessageSend(bool isClassMethod,
                    const char *objectsClass,
                    const char *implementingClass,
                    SEL selector)
{
    char    buf[ 1024 ];

    // Create/open the log file
    if (objcMsgLogFD == (-1))
    {
        snprintf (buf, sizeof(buf), "/tmp/msgSends-%d", (int) getpid ());
        objcMsgLogFD = secure_open (buf, O_WRONLY | O_CREAT, geteuid());
        if (objcMsgLogFD < 0) {
            // no log file - disable logging
            objcMsgLogEnabled = false;
            objcMsgLogFD = -1;
            return true;
        }
    }

    // Make the log entry
    snprintf(buf, sizeof(buf), "%c %s %s %s\n",
            isClassMethod ? '+' : '-',
            objectsClass,
            implementingClass,
            sel_getName(selector));

    objcMsgLogLock.lock();
    write (objcMsgLogFD, buf, strlen(buf));
    objcMsgLogLock.unlock();

    // Tell caller to not cache the method
    return false;
}

運(yùn)行程序崩潰并巍,按照路徑可以找到如下文件

  • 崩潰日志路徑


    崩潰日志路徑
  • 崩潰日志內(nèi)容


    崩潰日志內(nèi)容

打開日志我們可以看到

  • 兩次動態(tài)方法決議:resolveInstanceMethod方法
  • 兩次消息快速轉(zhuǎn)發(fā):forwardingTargetForSelector方法
  • 兩次消息慢速轉(zhuǎn)發(fā):methodSignatureForSelector + resolveInstanceMethod
  • 使用bt查看堆棧信息
    運(yùn)行程序崩潰查看堆棧信息
  • 發(fā)現(xiàn)___forwarding___來自CoreFoundation
    ___forwarding___來自CoreFoundation
  • image list讀取整個鏡像文件,然后搜索CoreFoundation,查看其可執(zhí)行文件的路徑如下圖所示
    CoreFoundation路徑
  • 根據(jù)CoreFoundation路徑找到該文件
    `CoreFoundation`文件
  • 通過反匯編工具hopper查看
    CoreFoundation文件拖入hopper

搜索__forwarding_prep_0___方法换途,通過跳轉(zhuǎn)____forwarding___懊渡,我們可以看到該方法的偽代碼了,如下所示

____forwarding___部分偽代碼

通過操作我們可以先判斷是否實(shí)現(xiàn)了forwardingTargetForSelector方法军拟,如果沒有響應(yīng)剃执,跳轉(zhuǎn)至loc_64a67也就是如果快速轉(zhuǎn)發(fā)沒有響應(yīng),則進(jìn)入慢速轉(zhuǎn)發(fā)流程懈息,
loc_64a67偽代碼實(shí)現(xiàn)
查看是否實(shí)現(xiàn)methodSignatureForSelector方法肾档,如果沒有響應(yīng),跳轉(zhuǎn)至loc_64e3辫继,則直接報錯怒见,如果獲取methodSignatureForSelector的方法簽名為nil,也是直接報錯
loc_64e3c偽代碼

如果methodSignatureForSelector返回值不為空姑宽,則在forwardInvocation方法中對invocation進(jìn)行處理
`methodSignatureForSelector`返回值`不為空邏輯處理偽代碼實(shí)現(xiàn)

  • 消息轉(zhuǎn)發(fā)機(jī)制流程圖


    消息轉(zhuǎn)發(fā)機(jī)制流程圖

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

【快速轉(zhuǎn)發(fā)】當(dāng)慢速查找遣耍,以及動態(tài)方法決議均沒有找到實(shí)現(xiàn)時,進(jìn)行消息轉(zhuǎn)發(fā)低千,首先是進(jìn)行快速消息轉(zhuǎn)發(fā)配阵,即走forwardingTargetForSelector方法如果返回消息接收者馏颂,在消息接收者中還是沒有找到,則進(jìn)入另一個方法的查找流程如果返回nil棋傍,則進(jìn)入慢速消息轉(zhuǎn)發(fā)救拉。

【慢速轉(zhuǎn)發(fā)】執(zhí)行到methodSignatureForSelector方法,如果返回的方法簽名為nil,則直接崩潰報錯

如果返回的方法簽名不為nil瘫拣,走到forwardInvocation方法中亿絮,對invocation事務(wù)進(jìn)行處理,如果不處理也不會報錯

總結(jié):消息轉(zhuǎn)發(fā)有三種
  • forwardingTargetForSelector快速轉(zhuǎn)發(fā)
    如果動態(tài)決議沒有找到方法麸拄,則需要在LGPerson中重寫forwardingTargetForSelector方法派昧,將LGPerson的實(shí)例方法接收者指定為LGStudent的對象,代碼如下
- (id)forwardingTargetForSelector:(SEL)aSelector{
    NSLog(@"%s - %@",__func__,NSStringFromSelector(aSelector));

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

輸出結(jié)果如下所示
forwardingTargetForSelector進(jìn)行消息快速轉(zhuǎn)發(fā)
  • methodSignatureForSelectorforwardInvocation慢速轉(zhuǎn)發(fā)

如果快速轉(zhuǎn)發(fā)沒有找到方法的實(shí)現(xiàn)蒂萎,就會進(jìn)行慢速轉(zhuǎn)發(fā)流程,代碼如下

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

- (void)forwardInvocation:(NSInvocation *)anInvocation{
    NSLog(@"%s - %@",__func__,anInvocation);
    // GM  sayHello - anInvocation - 漂流瓶 - anInvocation
    anInvocation.target = [LGStudent alloc];
    // anInvocation 保存 - 方法
    [anInvocation invoke];
}

輸出結(jié)果如下所示淮椰,并且發(fā)現(xiàn)forwardInvocation方法中不對invocation進(jìn)行處理五慈,也不會崩潰報錯

快速轉(zhuǎn)發(fā)流程輸出結(jié)果

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末主穗,一起剝皮案震驚了整個濱河市泻拦,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌忽媒,老刑警劉巖争拐,帶你破解...
    沈念sama閱讀 216,651評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異晦雨,居然都是意外死亡架曹,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,468評論 3 392
  • 文/潘曉璐 我一進(jìn)店門金赦,熙熙樓的掌柜王于貴愁眉苦臉地迎上來音瓷,“玉大人,你說我怎么就攤上這事夹抗。” “怎么了纵竖?”我有些...
    開封第一講書人閱讀 162,931評論 0 353
  • 文/不壞的土叔 我叫張陵漠烧,是天一觀的道長。 經(jīng)常有香客問我靡砌,道長已脓,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,218評論 1 292
  • 正文 為了忘掉前任通殃,我火速辦了婚禮度液,結(jié)果婚禮上厕宗,老公的妹妹穿的比我還像新娘。我一直安慰自己堕担,他們只是感情好已慢,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,234評論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著霹购,像睡著了一般佑惠。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上齐疙,一...
    開封第一講書人閱讀 51,198評論 1 299
  • 那天膜楷,我揣著相機(jī)與錄音,去河邊找鬼贞奋。 笑死赌厅,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的轿塔。 我是一名探鬼主播察蹲,決...
    沈念sama閱讀 40,084評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼催训!你這毒婦竟也來了洽议?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,926評論 0 274
  • 序言:老撾萬榮一對情侶失蹤漫拭,失蹤者是張志新(化名)和其女友劉穎亚兄,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體采驻,經(jīng)...
    沈念sama閱讀 45,341評論 1 311
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡审胚,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,563評論 2 333
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了礼旅。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片膳叨。...
    茶點(diǎn)故事閱讀 39,731評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖痘系,靈堂內(nèi)的尸體忽然破棺而出菲嘴,到底是詐尸還是另有隱情,我是刑警寧澤汰翠,帶...
    沈念sama閱讀 35,430評論 5 343
  • 正文 年R本政府宣布龄坪,位于F島的核電站,受9級特大地震影響复唤,放射性物質(zhì)發(fā)生泄漏健田。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,036評論 3 326
  • 文/蒙蒙 一佛纫、第九天 我趴在偏房一處隱蔽的房頂上張望妓局。 院中可真熱鬧,春花似錦局雄、人聲如沸哎榴。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,676評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽驼侠。三九已至倒源,卻和暖如春笋熬,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背昔馋。 一陣腳步聲響...
    開封第一講書人閱讀 32,829評論 1 269
  • 我被黑心中介騙來泰國打工秘遏, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留嘉竟,地道東北人。 一個月前我還...
    沈念sama閱讀 47,743評論 2 368
  • 正文 我出身青樓铡俐,卻偏偏與公主長得像,于是被迫代替她去往敵國和親吏够。 傳聞我的和親對象是個殘疾皇子滩报,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,629評論 2 354