objc_msgSend流程分析-動態決議與消息轉發

前言

在前兩篇節我們了解了快速方法查找如果找不到,則會進入慢速查找流程,其查找流程主要為在當前類方法列表中查找,如果還是沒有找到,則去父類鏈的緩存和方法列表中查找。

  • 快速查找

objc_msgSend流程分析(快速查找)

  • 慢速查找

objc_msgSend流程分析(慢速查找)

  • 根據慢速查找找不到時,程序崩潰會出現+[LGPerson sayNB]: unrecognized selector sent to class 0x1000022b0 像是這樣的提示,表示方法並未實現.

防止崩潰

  • 為了提升用戶體驗,蘋果給予我們兩個建議
    • 【建議一】:動態方法決議
    • 【建議二】:消息轉發(快速轉發/慢速轉發)

【建議一】:動態方法決議

  • 如果慢速查找沒有找到方法的實現地址,則會進入第一次動態方法決議,如下是resolveMethod_locked 的源碼實現
static NEVER_INLINE IMP
resolveMethod_locked(id inst, SEL sel, Class cls, int behavior)
{
    runtimeLock.assertLocked();
    ASSERT(cls->isRealized());

    runtimeLock.unlock();
    // 對象 - 類
    if (! cls->isMetaClass()) {  //如果類不是元類,調用對象的解析方法
        // try [cls resolveInstanceMethod:sel]
        resolveInstanceMethod(inst, sel, cls);
    } 
    else { // 對象 - 元類 ,如果元類則掉用類的解析方法
        // try [nonMetaClass resolveClassMethod:sel]
        // and [cls resolveInstanceMethod:sel]
        resolveClassMethod(inst, sel, cls);
        // 為什麼會有以下判斷?
        // 因為類方法在元類中是對象方法,所以需要查詢元類中對象方法的動態決議
        if (!lookUpImpOrNil(inst, sel, cls)) { //如果沒有找到或者為空,則在元類的對象方法中的解析方法查找           
             resolveInstanceMethod(inst, sel, cls);
        }
    }
    // chances are that calling the resolver have populated the cache
    // so attempt using it
    // 如果方法解析中將期實現指向其他方法,則繼續走方法查找流程
    return lookUpImpOrForward(inst, sel, cls, behavior | LOOKUP_CACHE);
}

流程圖

實例方法解析-resolveInstanceMethod

  • 針對實例方法調用時,如果快速/慢速都沒有找到實例方法的實現地址時,有一次的挽救機會,就是執行動態方法決議,由於不是元類,是一個實例方法,程式走到resolveInstanceMethod。
static void resolveInstanceMethod(id inst, SEL sel, Class cls)
{
    runtimeLock.assertUnlocked();
    ASSERT(cls->isRealized());
    SEL resolve_sel = @selector(resolveInstanceMethod:);

    // 查看是否有resolveInstanceMethod的實現
    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);//發送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));
        }
    }
}

實例方法解析-resolveInstanceMethod源碼簡述

  • 首先在發送resolveInstanceMethod消息前,需要查找cls類中是否有該方法的實現,即通過lookUpImpOrNil方法進入lookUpImpOrForward 慢速查找流程查找resolveInstanceMethod方法。
    • 如果沒有查找到,則直接返回
    • 如果查找到,則接下來進行發送resolveInstanceMethod消息
  • 再次慢速查找實例方法的實現,即通過lookUpImpOrNil 方法進入lookUpImpOrForward 慢速查找實例方法

避免崩潰

實例方法

  • 我們知道了在沒有找到方法的實現地址前,程序會進到resolveInstanceMethod 這裡,我們可以透過重寫類方法resolveInstanceMethod 方式,也就是在LGPerson中重寫resolveInstanceMethod類方法,將實例方法say666的實現指向sayMaster方法實現,程式碼如下所示
+ (BOOL)resolveInstanceMethod:(SEL)sel{
    if (sel == @selector(say666)) {
        NSLog(@"%@ 来了",NSStringFromSelector(sel));
        //獲取sayMaster方法的imp
        IMP imp           = class_getMethodImplementation(self, @selector(sayMaster));
        //獲取sayMaster的實例方法
        Method sayMMethod = class_getInstanceMethod(self, @selector(sayMaster));
       //獲取sayMaster的方法簽名
        const char *type  = method_getTypeEncoding(sayMMethod);
       //將sel的實現指向sayMaster
        return class_addMethod(self, sel, imp, type);
    }
    return [super resolveInstanceMethod:sel];
}

  • 打印結果如下

類方法

  • 針對類方法的防止崩潰,與實例方法相似,一樣使用重寫resolveClassMethod 類方法來解決崩潰的問題,一樣也是在LGPerson類 中重寫此方法,並將sayNB類方法的實現指向類方法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此處傳入的cls不是類,而是元類,通過objc_getMetaClass 方法獲取類的元類,因為類方法在元類中是實例方法

優化-避免崩潰

  • 我們知道isa的走位圖,而其中方法查找流程如下
    • 實例方法:類→父類→根類→nil
    • 類方法: 元類→根元類→根類→nil
  • 上節兩種對於實例方法及類方法避免崩潰的方式,可以透過寫加寫NSObject分類 的方式進行整合,由於類方法的查找,其實也是查找元類的實例方法,所以我們可以在根類將兩者(要查找的實例方法以及類方法)統合,一起放在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;
}

  • 這樣的統整也示範了,調用了類方法的動態決議,還會調用實例對象的動態方法決議,原因如上所述,因為類方法在元類中是實例方法
  • 但是上述這樣的寫法,如果經過系統版次的更新 ,系統方法是有可能會被修改的,所以其實我們可以依照自定義的方法名,習慣在方法名加入自定義的前綴,根據前綴判斷是否是自定義方法,統一處理,這樣處理屬於AOP(Aspect Oriented Programming)面向切面編程,比如說可以在檢測到崩潰時pop到首頁,進而提升用戶體驗。
  • 在此我們在這個動態方法決議內,先不處理,而是流到下一個環節,消息轉發流程來處理。

消息轉發流程

  • 如果快速/慢速查找流程與動態方法決議,還是沒有找到方法的實現地址(imp),就進行的是消息轉發流程,我們透過以下方式來探討,找不到方法地址崩潰前,調用了哪些方法。
  • 接著我們將使用以下兩種方式
    • 通過instrumentObjcMessageSends 方法查看log
    • 通過hopper/IDA反編譯

通過instrumentObjcMessageSends 方法查看log

  • 查找流程lookUpImpOrForwardlog_and_fill_cachelogMessageSend 並且在logMessageSend下找到instrumentObjcMessageSends
  • 如下為instrumentObjcMessageSends 的源碼實現
void instrumentObjcMessageSends(BOOL flag)
{
    bool enable = flag;

    // Shortcut NOP
    if (objcMsgLogEnabled == enable)
        return;

    // If enabling, flush all method caches so we get some traces
    if (enable)
        _objc_flush_caches(Nil);

    // Sync our log file
    if (objcMsgLogFD != -1)
        fsync (objcMsgLogFD);

    objcMsgLogEnabled = enable;
}

  • 接下來,我們打算在main中調用instrumentObjcMessageSends ,打印出log信息,需要完成 下列動作。
    • 傳入flag為YES,也就是objcMsgLogEnabled,即調用instrumentObjcMessageSends ,傳入參數為YES。
    • 需要使用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 ,可以看到消息發送打印其實都放在了 目錄下,如下圖所示
  • 運行程式並前往/tmp/msgSends 目錄,發現有msgSends開頭的日誌文件,打開查看在程式崩潰前,執行了以下方法
    • 兩次動態方法決議:resolveInstanceMethod方法
    • 兩次消息快速轉發:forwardingTargetForSelector方法
    • 兩次消息慢速轉發:methodSignatureForSelectorresolveInstanceMethod

通過反彙編-Hopper/IDA反編譯

  • Hopper與IDA是逆向工程時靜態分析常用的工具,將可執行文件經由反彙編工具(Hopper/IDA)反彙編程彙編語言,透過工具還能將其轉換為偽代碼,控制流程圖。

程式碼編譯為彙編語言在編譯成機器語言(可執行文件),最後由計算器運行

  • 可以看到___forwarding___來自CoreFoundation
  • 利用image list調用讀取鏡相文件,然後搜索CoreFoundation ,查看執行路徑
  • 通過文件路徑,找到CoreFoundation可執行文件 /System/Library/Frameworks/CoreFoundation.framework/Versions/A/CoreFoundation
  • 將可執行文件,拖入hopper開啟
  • 通過左側的搜索框搜索_ forwarding_prep_0,然後選擇偽代碼
  • 以下是__forwarding_prep_0 ___ 的彙編偽代碼,跳轉至___forwarding___
  • 以下是___forwarding___ 的偽代碼實現,首先是查看是否實現 forwardingTargetForSelector 方法,如果沒有響應.跳轉

偽代碼-forwardingTargetForSelector

  • 如果沒有響應,跳轉至loc_64b9b ,則直接報錯
  • 如果獲取methodSignatureForSelector方法签名 為nil(if (rax == 0x0) goto loc_6501c;),也是直接報錯if (strncmp(r13, "*NSZombie*", 0xa) == 0x0) goto loc_64fa1;
  • 如果methodSignatureForSelector 返回值不為空,則在forwardInvocation 方法中對invocation 進行處理

偽代碼-forwardInvocation

  • 所以,通過上面兩種查找方式可以驗證,消息轉發的方法有3個
    • 【快速转发】forwardingTargetForSelector
    • 【慢速转发】
      • methodSignatureForSelector
      • forwardInvocation

綜上所述,消息轉發流程圖如下

  • 消息轉發的處理主要分為兩個部分:
    • 【快速轉發】當慢速查找,以及動態方法決議均沒有找到方法實現時,進行消息轉發,首先是進行快速消息轉發 即走到forwardingTargetForSelector 方法
      • 如果返回消息接收者 ,在消息接收者中還是沒有找到,則進入另一個 方法的查找流程
      • 如果返回nil,則進入慢速轉發
    • 【慢速轉發】執行到methodSignatureForSelector 方法
      • 如過返回的方法簽名為nil,則直接崩潰報錯
      • 如果返回的方法簽名不為nil,走到forwardInvocation 方法中,對invocation事務進行處理,如果不處理也不會報錯

【建議二 】快速轉發

  • 針對前文解決崩潰問題,如果動態方法也沒有找到實現,則需要在LGPerson中重寫forwardingTargetForSelector 方法,將LGPerson 的實力方法的接收者指定為LGStudent對象(LGStudent类中有say666的具体实现),如下所示
- (id)forwardingTargetForSelector:(SEL)aSelector{
    NSLog(@"%s - %@",__func__,NSStringFromSelector(aSelector));

//     runtime + aSelector + addMethod + imp
    //将消息的接收者指定为LGStudent,在LGStudent中查找say666的实现
    return [LGStudent alloc];
}

執行結果如下

  • 也可以直接不指定消息接收者,直接調用父類的該方法,如果還是沒有找到,則直接報錯

【建議三】慢速轉發

  • 針對第二次機會(快速轉發)中還是沒有找到,則進入最後的一次挽救機會,即在LGPerson 中重寫methodSignatureForSelector ,如下所示
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
    NSLog(@"%s - %@",__func__,NSStringFromSelector(aSelector));
    return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}

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

  • 打印結果如下,發現forwardInvocation方法中不對invocation進行處理,也不會崩潰報錯
  • 另外也可以處理invocation事務,如下所示,修改invocationtarget[LGStudent alloc] ,調用[anInvocation invoke] 觸發,即LGPerson 類的say666實例方法的調用會調用LGStudentsay666 方法
- (void)forwardInvocation:(NSInvocation *)anInvocation{
    NSLog(@"%s - %@",__func__,anInvocation);
    anInvocation.target = [LGStudent alloc];
    [anInvocation invoke];
}

打印结果如下

  • 所以,由上述可知,無論在forwardInvocation 方法中是否處裡invocation事務,程序都不會崩潰

問題探索

動態方法為何執行兩次?

  • 以下使用兩種方式分析動態方法決議
    • 通過偽代碼驗證
    • 通過程式推導

通過偽代碼+調適驗證

  • 首先LGPerson類裡面只有聲明,沒有say666的實現,通過慢速查找流程我們知道方法執行是通過lookUpImpOrForward --> resolveMethod_locked --> resolveInstanceMethod,來到resolveInstanceMethod源码,在源碼中通過發送resolve_sel消息觸發,如下所示
  • 所以可以在resolveInstanceMethod 方法中IMP imp = lookUpImpOrNil(inst, sel, cls); 處加一個斷點,通過bt打印堆棧信息來看到底發生了什麼

  • resolveInstanceMethod 方法中IMP imp = lookUpImpOrNil(inst, sel, cls); 處加一個斷點,運行程序,直到第一次“来了” ,我們透過lldb調適輸入bt打印堆棧信息,此时的sel是say666

  • 過掉斷點繼執行到第二次的“来了” ,查看堆棧信息,在第二次中,我們可以看到是通過CoreFoundation[NSObject(NSObject) methodSignatureForSelector:] 方法,然後通過class_getInstanceMethod 再次進入動態方法決議。
  • 通過上一步的堆棧信息,我們需要去看看CoreFoundation中到底做了什麼?通過Hopper 反編譯彙編CoreFoundation 的可執行文件,查看methodSignatureForSelector 方法的偽代碼。
  • 接著我們透過hopper反編譯CoreFoundation 的可執行文件,開啟偽代碼模式,搜尋methodSignatureForSelector
  • 通過methodSignatureForSelector 偽代碼進入___methodDescriptionForSelector 的實現。
  • 進入___methodDescriptionForSelector的偽代碼實現,結合彙編的堆棧打印,可以看到,在___methodDescriptionForSelector 這個方法中調用了objc4-781class_getInstanceMethod
  • 在objc中的源碼搜索class_getInstanceMethod ,其源碼實現如下所示
  • 這一點可以通過代碼調適來驗證,如下所示,在class_getInstanceMethod 方法處加一個斷點,在執行了methodSignatureForSelector 方法後,返回了簽名.說明方法簽名是生效的,蘋果在走到invocation之前,給了開發者一次機會再去查詢,所以走到class_getInstanceMethod 這裏,又去走了一遍方法查詢say666,然後會再次走到動態方法決議
  • 所以,上述的分析也印證了前文中resolveInstanceMethod 方法執行了兩次的原因

通過程式推導

如果LGPerson類中重寫resolveInstanceMethod方法,並加上class_addMethod操作即賦值IMP,此時resolveInstanceMethod會走兩次嗎?

  • 答案是只走一次,如果賦值了IMP,動態方法決議只會走一次,說明不是在這裡走第二次動態方法決議
  • 排除掉resolveInstanceMethod 方法中的賦值IMP,在LGPerson類中重寫forwardingTargetForSelector ,並指定返回值為[LGStudent alloc] ,重新運行,如果resolveInstanceMethod 打印兩次,說明是在forwardingTargetForSelector 方法之前執行了動態方法決議,反之在forwardingTargetForSelector方法之後.
  • 如圖可以看到 第二次動態法決議在methodSignatureForSelectorforwardInvocation 方法之間

  • 經過上面的論證,我們了解到其實在慢速消息轉發流程中,在methodSignatureForSelectorforwardInvocation 方法之間還有一次動態方法決議,即蘋果再次給的一個機會,如下圖所示

總結

當進行objc_msgSend發送消息時

  • 首先進行快速查找流程,在類的緩存cache中查找指定方法的實現
  • 如果緩存沒有查找到,則進行慢速查找流程,會先在類的方法列表中查找,如果還是沒有找到,則去父類鏈的緩存和方法列表中查找
  • 如果慢速差找也沒有找到,第一次的機會是動態方法決議,也就是重寫resolveInstanceMethod/resolveClassMethod 方法
  • 如果動態方法決議還是沒有找到,則進行消息轉發,消息轉發有兩次機會:快速轉發+慢速轉發
  • 如果轉發後也沒有則報錯崩潰unrecognized selector sent to instance
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,047评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,807评论 3 386
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,501评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,839评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,951评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,117评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,188评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,929评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,372评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,679评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,837评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,536评论 4 335
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,168评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,886评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,129评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,665评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,739评论 2 351

推荐阅读更多精彩内容