手撕iOS底層16 -- 消息解析&消息轉發原理

前倆篇objc_msgSend快速查找objc_msgSend慢速查找的流程,主要分析了經過彙編流程快速查找緩存,經過類的方法列表慢速查找,本章着重接着上倆章深刻分析沒有找到方法的狀況下, 蘋果給開發者提供了二個建議。c++

  1. 動態方法解析: 在慢速查找過程當中,未找到IMP,會執行一次動態方法解析
  2. 消息轉發: 若是動態方法決議仍是沒有找到IMP,則開始消息轉發

0x00 - forward_imp

若是以上倆步都沒有作相應的操做,就會報平常開發常見的錯誤方法未實現的崩潰報錯objective-c

以下示例代碼:windows

@interface Student : NSObject
@property (nonatomic, copy) NSString *lgName;
@property (nonatomic, strong) NSString *nickName;

- (void)sayNB;
- (void)sayMaster;
- (void)say666;
- (void)sayHello;

+ (void)sayNB;
+ (void)lgClassMethod;
@end
  
@implements Student
- (void)sayHello{
    NSLog(@"%s",__func__);
}
- (void)sayNB{
    NSLog(@"%s",__func__);
}
- (void)sayMaster{
    NSLog(@"%s",__func__);
}
+ (void)lgClassMethod{
    NSLog(@"%s",__func__);
}
@end
複製代碼
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Student *stu = [Student alloc];
        [stu say666];
        //[Student performSelector:@selector(sayNB)];
    }
    return 0;
}
複製代碼

main方法中分別調用實例方法類方法緩存

  • 調用類方法

  • 分析: 在慢速查找的源碼中,IMP未找到,會賦值稱爲forward_imp=(IMP)_objc_msgForward_impcache;,經過搜索_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
複製代碼

搜索__objc_forward_handler,根據以前總結的規則, 去掉一個下劃線來搜索。markdown

// 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,這就是咱們平常中常常見到的崩潰錯誤。 下面深刻分析崩潰發生以前的補救方法架構

0x01 - 方法的動態解析

lookUpImpOrForward方法裏,方法慢速查找走完以後,會開始走方法動態解析流程,給開發者提供第一次機會,來處理找不到消息的錯誤。函數

// No implementation found. Try method resolver once.
if (slowpath(behavior & LOOKUP_RESOLVER)) {
    behavior ^= LOOKUP_RESOLVER;
    return resolveMethod_locked(inst, sel, cls, behavior);
}
複製代碼

經過註釋也能夠得知, 這個實在IMP沒有找到的時候,會走這裏解決,而且只走一次。工具

/*********************************************************************** * resolveMethod_locked * Call +resolveClassMethod or +resolveInstanceMethod. * * Called with the runtimeLock held to avoid pressure in the caller * Tail calls into lookUpImpOrForward, also to avoid pressure in the callerb **********************************************************************/
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);
}
複製代碼
  • 主要分如下幾步:oop

    1. 先是判斷cls是不是元類post

      1. 若是是,調用對象方法的動態解析resolveInstanceMethod
      2. 若是是元類,調用類方法的動態解析resolveClassMethod來處理,而後判斷是否能找到sel,找不到接着再調用一次resolveInstanceMethod,由於類方法,即帶+號的方法相對於元類來講也是實例方法, 調用resolveInstanceMethod,參數第一個是inst=類,第二個查找是sel方法名字,第三個cls=元類,
      if (!lookUpImpOrNil(cls, resolve_sel, cls->ISA())) { 
              // Resolver not implemented.
              return;
      }
      複製代碼

      若是這裏查找的是類方法, 是在cls->ISA根元類裏找這個解析方法的實現, 找到就去發送消息, 找不到返回默認實現。

實例方法崩潰修復

+ (BOOL)resolveInstanceMethod:(SEL)sel{
    if (sel == @selector(say666)) {
        NSLog(@"%@ 來了", NSStringFromSelector(sel));
        //獲取sayMaster方法的imp
        IMP imp = class_getMethodImplementation(self, @selector(sayMaster));
        //獲取sayMaster的實例方法
        Method sayMethod  = class_getInstanceMethod(self, @selector(sayMaster));
        //獲取sayMaster的方法簽名
        const char *type = method_getTypeEncoding(sayMethod);
        //將sel的實現指向sayMaster
        return class_addMethod(self, sel, imp, type);
    }
    return [super resolveInstanceMethod:sel];
}
複製代碼

在類裏邊重寫類方法resolveInstanceMethod,消息崩潰以前, 會執行一次實例方法動態解析,在這個方法裏,經過runtime把沒找到的sel指向一個存在的imp上,打印結果

這裏會看到這個方法打印裏倆次,這個問題留在文章末尾分析。

類方法崩潰修復

發送類方法消息找不到imp致使的崩潰修復,與實例方法相似方法修復, 重寫resolveClassMethod來解決,在該方法中, 把崩潰的sel指向一個能夠找到的imp

+ (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];
}
複製代碼

⚠️ 這裏要注意獲取類方法是要到元類,添加類方法也要到元類中,可使用objc_getMetaClass獲取元類。

總結與優化

經過上邊的方法的動態解析分析, 獲得這樣的結論

  • 實例方法 類 -> 父類 -> 根類 -> nil
  • 類方法(resolveClassMethod) 元類 -> 父元類 -> 根元類 -> 根類 -> nil
  • 類方法(resolveInstanceMethod) 根元類 -> 根類 -> nil

以前的修復崩潰都是在對應的類中重寫resolveInstanceMethod或者resolveClassMethod,經過上邊這三條路線,能夠根類NSObject中重寫resolveInstanceMethod統一處理實例方法類方法的崩潰處理。

resolveInstanceMethodNSObject有默認實現

+ (BOOL)resolveClassMethod:(SEL)sel {
    return NO;
}

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    return NO;
}
複製代碼

以下,建立一個NSObject的分類,統一處理以下,由於有默認實現,因此返回NO,不能調用[super resolveInstanceMethod:sel]

+ (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;
}
複製代碼

固然這種統一處理的方式,仍是會有一些問題, 一些系統的方法會走進這裏, 能夠針對類中的方法名統一前綴,根據前綴判斷對應的模塊來處理,好比mine模塊, 屬於這個模塊的崩潰統一跳轉到mine模塊首頁, 也能夠作一些錯誤上報的操做。

0x03 - 消息轉發流程

快速查找+慢速查找沒有找到以及動態消息解析也未處理,就會進入消息轉發過程

lookUpImpOrForward的函數末尾, 在log_and_fill_cache有這麼一個控制條件objcMsgLogEnabled,經過它能夠控制日誌保存到本地,經過日誌能夠看到調用流程

控制這個objcMsgLogEnabled的是這個函數instrumentObjcMessageSends,給它傳入true,控制開啓本地日誌保存

經過lookUpImpOrForward -> log_and_fill_cache -> logMessageSend 找到如下源碼實現

bool objcMsgLogEnabled = false;
static int objcMsgLogFD = -1;

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

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;
}
複製代碼

由於這個instrumentObjcMessageSends是內部函數,在外部使用須要使用extern外部聲明

extern void instrumentObjcMessageSends(BOOL flag);

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        LGPerson *person = [LGPerson alloc];
        instrumentObjcMessageSends(YES);
        [person sayHello];
        instrumentObjcMessageSends(NO);
    }
    return 0;
}
複製代碼

經過以上源碼瞭解到日誌的保存路徑在/tmp/msgSends目錄中,運行代碼,就能夠看到以下內容

在目錄中打開msgSends開頭的文件, 調用完resolveInstanceMethod方法,並無在方法動態解析處理,因此來到forwardingTargetForSelector快速轉發以及後續 的慢速轉發

0x04 - Hopper/IDA 反彙編看流程

HopperIDA是一個能夠幫助咱們靜態分析的反彙編工具,將可執行文件反彙編爲僞代碼 和流程圖形式,幫助咱們去分析,因爲IDA在mac上不穩定,能夠在windows系統上測試, 如下使用Hopper來分析。

運行崩潰後,經過bt看堆棧信息,

經過彙編查看,__forwarding___也是在CoreFoundation中。

經過image list調試命令查看CoreFoundation image的位置

找到CoreFoundation後,用Hopper打開它

打開Hopper, 選擇Try the Demo,將CoreFoundation拖入裏邊

點擊OK

默認點擊Next

等待加載完成,

搜索__forwarding_prep_0___,查看僞代碼, 跳轉到___forwarding___裏邊的僞代碼

首先判斷是否實現forwardingTargetForSelector

  • 沒有實現跳轉到loc_64a67
  • 能夠找到實現走loc_649fc,經過forwardingTargetForSelector獲取接受對象給rax, 再對rax做容錯處理,有錯誤跳到loc_64e3c
loc_64a67僞代碼

跳到這裏後,首先判斷是否爲殭屍對象,在下邊繼續判斷是否響應 methodSignatureForSelector

  • 不響應跳轉到loc_64dd7, 直接報錯
  • 響應的話接着往下走, 獲取返回值, 做容錯處理,有錯誤跳到loc_64e3c

loc_64dd7僞代碼和loc_64e3c僞代碼

經過獲取methodSignatureForSelector方法簽名爲nil也直接報錯

上邊的流程獲取到方法簽名,開始在forwardInvocation方法中進行處理

因此經過以上分析, 消息轉發有倆種

  • 快速轉發 forwardingTargetForSelector
  • 慢速轉發methodSignatureForSelector +forwardingTargetForSelector實現

方法動態決議-動態決議和轉發流程

lookUpImpOrForward中,慢速也沒有找到imp

  • 第一步開始方法的動態解析處理,這步未處理, 即走消息轉發
  • 消息轉發第一步開始forwardingTargetForSelector,即快速消息轉發,將消息轉發給別等對象處理,這步未處理,交給慢速轉發
  • 慢速轉發使用methodSignatureForSelector返回方法簽名,不能夠返回nil或者簽名內容爲空,使用方法簽名生成NSInvocation對象, 因此須要重寫forwardInvocation進行消息轉發。

0x05 - resolveInstanceMethod爲何執行倆次?

解決以前遺留的問題, 在實例動態方法解析的時候, 只重寫了, 並未對未找到的sel做處理, 會調用倆次

上帝視角探索

實例動態方法解析的時候, 會走到lookUpImpOrForward -> resolveMethod_locked -> resolveInstanceMethod,是經過這裏觸發

IMP imp = lookUpImpOrNil(inst, sel, cls);加個斷點, 當selsay666停下來,打印了了say66 來了經過bt查看堆棧,

第一次打印的信息, 經過堆棧能夠看出是第一次經過方法動態解析執行打印的。

經過第二次打印, 經過[NSObject(NSObject) methodSignatureForSelector:] -> __methodDescriptionForSelector -> class_getInstanceMethod再次來到方法的動態解析並打印了第二次,經過堆棧分析, 能夠經過Hopper反彙編CoreFoundation文件,查看methodSignatureForSelector的僞代碼

在跳進到___methodDescriptionForSelector看它的實現

結合以前的堆棧信查看, 這裏調用了objc 的方法 class_getInstanceMethod,在源碼工程查看

/*********************************************************************** * class_getInstanceMethod. Return the instance method for the * specified class and selector. **********************************************************************/
Method class_getInstanceMethod(Class cls, SEL sel) {
    if (!cls  ||  !sel) return nil;

    // This deliberately avoids +initialize because it historically did so.

    // This implementation is a bit weird because it's the only place that 
    // wants a Method instead of an IMP.

#warning fixme build and search caches
        
    // Search method lists, try method resolver, etc.
    lookUpImpOrForward(nil, sel, cls, LOOKUP_RESOLVER);

#warning fixme build and search caches

    return _class_getMethod(cls, sel);
}
複製代碼

經過源碼查看,這裏又調用了lookUpImpOrForward, 又走了一次方法動態解析,系統在調用完methodSignatureForSelector,返回方法簽名,在調用invocation以前,又去調用class_getInstanceMethod,因此又走了一遍lookUpImpOrForward,查詢一遍sel,沒查到再走方法動態解析消息轉發流程。

無上帝視角探索

由於在源碼工程裏探索, 因此有上帝視角, 若是沒有環境, 如何驗證上邊的流程?

普通工程裏重寫resolveInstanceMethod ,在方法裏解決sel找不到的錯誤,使用class_addMethod添加一個IMP, 看看這個方法是否會走倆次?

+ (BOOL)resolveInstanceMethod:(SEL)sel{
    if (sel == @selector(say666)) {
        NSLog(@"%@ 來了", NSStringFromSelector(sel));
        //獲取sayMaster方法的imp
        IMP imp = class_getMethodImplementation(self, @selector(sayHello));
        //獲取sayMaster的實例方法
        Method sayMethod  = class_getInstanceMethod(self, @selector(sayHello));
        //獲取sayMaster的方法簽名
        const char *type = method_getTypeEncoding(sayMethod);
        //將sel的實現指向sayMaster
        return class_addMethod(self, sel, imp, type);
    }
    return [super resolveInstanceMethod:sel];
}
複製代碼

經過結果看,經過動態方法解析,賦值了IMP, 只執行了一次,說明第二次不在這裏。按照消息轉發流程, 把resolveInstanceMethod裏的imp去掉,重寫forwardingTargetForSelector,並指定[LGStudent alloc],從新運行, 看是否resolveInstanceMethod打印倆次, 打印倆次,說明在forwardingTargetForSelector以前執行了方法動態解析,反之,則在以後執行的方法動態解析

+ (BOOL)resolveInstanceMethod:(SEL)sel{
    if (sel == @selector(say666)) {
        NSLog(@"%s -- %@ 來了",__func__, NSStringFromSelector(sel));
//        //獲取sayMaster方法的imp
//        IMP imp = class_getMethodImplementation(self, @selector(sayHello));
//        //獲取sayMaster的實例方法
//        Method sayMethod  = class_getInstanceMethod(self, @selector(sayHello));
//        //獲取sayMaster的方法簽名
//        const char *type = method_getTypeEncoding(sayMethod);
//        //將sel的實現指向sayMaster
//        return class_addMethod(self, sel, imp, type);
    }
    return [super resolveInstanceMethod:sel];
}
- (id)forwardingTargetForSelector:(SEL)aSelector{
    NSLog(@"%s - %@",__func__,NSStringFromSelector(aSelector));
    // runtime + aSelector + addMethod + imp
    return [LGStudent alloc];
}
複製代碼

經過運行結果看, 並無在以前答應倆次, 說明在forwardingTargetForSelector以後執行的方法動態解析

接着根據流程,重寫methodSignatureForSelectorforwardInvocation

+ (BOOL)resolveInstanceMethod:(SEL)sel{
    NSLog(@"%s -- %@ 來了",__func__, NSStringFromSelector(sel));
    if (sel == @selector(say666)) {
//        //獲取sayMaster方法的imp
//        IMP imp = class_getMethodImplementation(self, @selector(sayHello));
//        //獲取sayMaster的實例方法
//        Method sayMethod  = class_getInstanceMethod(self, @selector(sayHello));
//        //獲取sayMaster的方法簽名
//        const char *type = method_getTypeEncoding(sayMethod);
//        //將sel的實現指向sayMaster
//        return class_addMethod(self, sel, imp, type);
    }
    return [super resolveInstanceMethod:sel];
}
- (id)forwardingTargetForSelector:(SEL)aSelector{
    NSLog(@"%s - %@",__func__,NSStringFromSelector(aSelector));
    return [super forwardingTargetForSelector:aSelector];
}

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

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

通過上邊的分析,第二次動態決議是在methodSignatureForSelectorforwardInvocation之間調用的,第二種分析方法驗證結果和第一種反彙編的結果是同樣的。獲得以下的圖

總結

本篇是消息流程分析方法動態解析消息轉發的最後一篇

  • 首先消息經過彙編流程快速查找,沒有找到跳到lookupImpOrForward開始慢速查找
  • 慢速查找消息也沒有找到,開始方法動態決議
  • 方法動態決議根據消息是類方法仍是實例方法重寫resolveInstanceMethodresolveClassMethod方法,開始第一次補救
  • 方法動態決議也沒有處理, 開始進行消息轉發即【快速轉發】
  • 快速轉發, 即重寫forwardingTargetForSelector方法, 將消息甩給能夠處理的對象,進行第二次補救
  • 慢速轉發使用methodSignatureForSelector返回方法簽名,不能夠返回nil或者簽名內容爲空,使用方法簽名生成NSInvocation對象, 因此須要重寫forwardInvocation進行消息轉發。

Objective-C 方法簽名和調用

iOS開發·runtime原理與實踐: 消息轉發篇(Message Forwarding) (消息機制,方法未實現+API不兼容奔潰,模擬多繼承)


歡迎大佬留言指正😄,碼字不易,以爲好給個贊👍 有任何表達或者理解失誤請留言交流;共同進步;

相關文章
相關標籤/搜索