iOS底層探索 -- 動態方法決議 && 消息轉發流程

上一期在objc_msgSend()慢速查找 lookUpImpOrForward流程中如果一直沒有找到方法,那流程會走向
resolveMethod_locked
-> resolveInstanceMethod / resolveClassMethod
-> resolveInstanceMethod:/ resolveClassMethod:
也就是當方法一直無法找到的時候,會根據對象方法或者類方法的不同,走向最終對象方法或者類方法的動態方法決議
為了保持流程的完整性。我們研究一下 動態方法決議

動態方法決議

先用代碼測試一下。

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        FQPerson *person = [FQPerson alloc];

        [person sayHelloWorld];

    }
    return 0;
}

main中我們調用sayHelloWorld方法
FQPerson.m的中注釋掉 sayHelloWorld方法的實現,同時添加

+ (BOOL)resolveInstanceMethod:(SEL)sel{
    NSLog(@"沒找到 %@ 方法",NSStringFromSelector(sel));
    return [super resolveInstanceMethod:sel];
}

運行

動態方法決議的打印.jpg

說明之前的流程確實如我們源碼看到的那樣,走到了resolveInstanceMethod中。
動態方法決議其實是蘋果在我們無法找到方法時給我們提供的補救流程,在這里,我們如果實現了方法,我們還是能避免崩潰。我們來嘗試一下。

先引入頭文件

#import <objc/message.h>

然后再resolveInstanceMethod內部添加代碼

+ (BOOL)resolveInstanceMethod:(SEL)sel{
    NSLog(@"沒找到 %@ 方法",NSStringFromSelector(sel));
    if(sel == @selector(sayHelloWorld)){
        IMP imp          = class_getMethodImplementation(self, @selector(eat1));
        Method eatMethod = class_getInstanceMethod(self, @selector(eat1));
        const char *type = method_getTypeEncoding(eatMethod);
        return class_addMethod(self, sel, imp, type);
    }
    return [super resolveInstanceMethod:sel];
}

在這里,我們將已經實現過的方法eat1賦給了sayHelloWorld

運行


動態方法決議中動態添加方法.jpg

此時 并未崩潰,同時調用了eat1方法。

繼續,我們注釋掉eat1的實現

然后運行


eat1動態方法決議.jpg

此時,我們可以發現,在sayHelloWorld的動態決議之后,進入了eat1的動態方法決議,預估應該是在將eat1賦給sayHelloWorld后開始進入了eat1的方法轉發流程。

此時有一個問題,為什么在第一張動態方法決議的打印圖中打印了兩次?

沒找到 sayHelloWorld 方法
沒找到 sayHelloWorld 方法

那么,我們來研究一下動態方法決議之后,系統做了什么?

第二次 動態方法決議 的來源

我們在+ (BOOL)resolveInstanceMethod:(SEL)sel中打個斷點,在第二次進入時 bt 打印棧信息

(lldb) bt 
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
  * frame #0: 0x0000000100001885 KCObjc`+[FQPerson resolveInstanceMethod:](self=FQPerson, _cmd="resolveInstanceMethod:", sel="sayHelloWorld") at FQPerson.m:59:55 [opt]
    frame #1: 0x00000001002fd3a7 libobjc.A.dylib`resolveInstanceMethod(inst=0x0000000000000000, sel="sayHelloWorld", cls=FQPerson) at objc-runtime-new.mm:6001:21
    frame #2: 0x00000001002e8e73 libobjc.A.dylib`resolveMethod_locked(inst=0x0000000000000000, sel="sayHelloWorld", cls=FQPerson, behavior=0) at objc-runtime-new.mm:6043:9
    frame #3: 0x00000001002e879c libobjc.A.dylib`lookUpImpOrForward(inst=0x0000000000000000, sel="sayHelloWorld", cls=FQPerson, behavior=0) at objc-runtime-new.mm:6192:16
    frame #4: 0x00000001002c27c9 libobjc.A.dylib`class_getInstanceMethod(cls=FQPerson, sel="sayHelloWorld") at objc-runtime-new.mm:5922:5
    frame #5: 0x00007fff2ddc8c68 CoreFoundation`__methodDescriptionForSelector + 282
    frame #6: 0x00007fff2dde457c CoreFoundation`-[NSObject(NSObject) methodSignatureForSelector:] + 38
    frame #7: 0x00007fff2ddb0fc0 CoreFoundation`___forwarding___ + 408
    frame #8: 0x00007fff2ddb0d98 CoreFoundation`__forwarding_prep_0___ + 120
    frame #9: 0x0000000100001b30 KCObjc`main(argc=<unavailable>, argv=<unavailable>) + 64 [opt]
    frame #10: 0x00007fff67e65cc9 libdyld.dylib`start + 1
    frame #11: 0x00007fff67e65cc9 libdyld.dylib`start + 1

通過打印的方法信息的反推,我們大概能看見流程


棧方法逆推.jpg

我們想研究這個流程詳細流程,但是CF的代碼并未開源,我們只能借助其他工具來研究。

  • 通過lldbimage list 打印鏡像列表
(lldb)  image list
[  0] 02E8C081-F154-3A94-BF16-66811D081546 0x0000000100000000 /Users/fangqiang/Library/Developer/Xcode/DerivedData/objc-cmqgtagmrqfzeobzskdrohmygvsg/Build/Products/Debug/KCObjc 
[  1] F9D4DEDC-8296-3E3F-B517-9C8B89A4C094 0x000000010000b000 /usr/lib/dyld 
[  2] F9BB2E7A-E017-32C8-8DB8-5F748EE88EF9 0x00000001002bd000 /private/tmp/objc.dst/usr/lib/libobjc.A.dylib 
[  3] 7C69F845-F651-3193-8262-5938010EC67D 0x00007fff3040a000 /System/Library/Frameworks/Foundation.framework/Versions/C/Foundation 
[  4] C0C9872A-E730-37EA-954A-3CE087C15535 0x00007fff64e4a000 /usr/lib/libSystem.B.dylib 
[  5] C0D70026-EDBE-3CBD-B317-367CF4F1C92F 0x00007fff2dd4d000 /System/Library/Frameworks/CoreFoundation.framework/Versions/A/CoreFoundation 
[  6] E692F14F-C65E-303B-9921-BB7E97D77855 0x00007fff65183000 /usr/lib/libc++abi.dylib 
[  7] 59A8239F-C28A-3B59-B8FA-11340DC85EDC 0x00007fff65130000 /usr/lib/libc++.1.dylib 

得到CF的鏡像地址

  • 找到鏡像地址后 通過hopper我們來追蹤一下CF內部的實現流程


    hop1.jpg
  • 使用其中的偽代碼模式,方便我們閱讀 搜索__forwarding_prep_0___
    參考棧方法流程往下走

    hop2.jpg

  • 然后跳轉loc_649bb
    hop3.jpg
  • 其中調用了判斷了方法forwardingTargetForSelector是否實現,為空的話繼續跳轉loc_64a67

    hop4.jpg

  • 判斷是否為_NSZombie對象,不是則繼續跳轉loc_64dc1

    hop5.jpg

  • 繼續跳轉loc_64dd7

    hop6.jpg

*繼續跳轉loc_64e3c

hop7.jpg

hop8.jpg

上面大概是一個簡略的消息轉發失敗流程,似乎沒有找到答案。
我們回到上面的棧打印,其中有一個

CoreFoundation`-[NSObject(NSObject) methodSignatureForSelector:]
  • 在流程__forwarding_prep_0___走完之后,如果沒有實現中間的forwardingTargetForSelector的方法,那后續根據棧的打印,會走到methodSignatureForSelector

  • 我們來搜索一下methodSignatureForSelector

    methodSingnatureForSelector.jpg

  • 跳轉其中的__methodDescriptionForSelector

__methodDescriptionForSelector 1.jpg
  • 再跳轉loc_7c68b

    hop10.jpg

  • 其中得到class_getInstanceMethod(),再回到源碼

Method class_getInstanceMethod(Class cls, SEL sel)
{
    if (!cls  ||  !sel) return nil;


#warning fixme build and search caches
        

    lookUpImpOrForward(nil, sel, cls, LOOKUP_RESOLVER);

#warning fixme build and search caches

    return _class_getMethod(cls, sel);
}

在這里,我們再次進入lookUpImpOrForward流程,會進行第二次動態方法決議的打印

消息轉發

在我們剛剛通過hopper探索的過程中
我們還看到了其中一些其他處理

  1. 判斷是否響應forwardingTargetForSelector
  2. 如果不響應,會跳轉判斷是否響應methodSignatureForSelector
  3. 如果也不響應 則直接報錯
  4. 如果獲取 methodSignatureForSelector方法簽名nil,也將直接報錯。
  5. 如果返回值methodSignatureForSelector不為空,則在forwardInvocation中進行處理。

以上也就是我們消息轉發的流程。

我們通過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源碼,我們定位到打印文件的位置

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

得到文件結果


logMsg打印.jpg

所以最終我們的消息轉發流程為


消息轉發流程.png

其實這里的消息轉發流程和動態決議是系統給予我們的三次補救機會,可以在這里避免程序崩潰。
但在實際使用過程中還會有一些坑點,還有一些實際的使用,我們有空再細說

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,533評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,055評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,365評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,561評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,346評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,889評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,978評論 3 439
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,118評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,637評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,558評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,739評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,246評論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,980評論 3 346
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,362評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,619評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,347評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,702評論 2 370