OC底層探索(十)objc_msgSend 流程之方法的動態方法決議和訊息的快速轉發、慢速轉發

2020-09-28 09:10:26

前提

在前面兩篇文章OC底層探索(八)objc_msgSend 流程之方法快速查詢OC底層探索(九)objc_msgSend 流程之方法慢速查詢中,分別分析了objc_msgSend的快速查詢和慢速查詢,在這兩種都沒找到方法實現的情況下,蘋果給了兩個建議

  • 動態方法決議:慢速查詢流程未找到後,會執行一次動態方法決議
  • 訊息轉發:如果動態方法決議仍然沒有找到實現,則進行訊息轉發

環境準備

新建Person類

Person.h

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

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

+ (void)sayNB;
+ (void)lgClassMethod;

@end

Person.m

@implementation Person
- (void)sayHello{
    NSLog(@"%s",__func__);
}

- (void)sayNB{
    NSLog(@"%s",__func__);
}
- (void)sayMaster{
    NSLog(@"%s",__func__);
}


+ (void)lgClassMethod{
    NSLog(@"%s",__func__);
}
@end

main.m檔案

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        
        Person *person = [Person alloc];
        //呼叫實體方法
        [person say666];
        //呼叫類方法
        [Person sayNB];

    }
    return 0;
}

動態方法決議【第一次機會】

在文章OC底層探索(九)objc_msgSend 流程之方法慢速查詢中,我們介紹慢速查詢的時候,沒有找到imp,那麼蘋果就會給一次機會,挽救一下。—— 【第一次機會】

原始碼

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
    · 如果是元類,執行類方法的動態方法決議resolveClassMethod,如果在元類中沒有找到或者為,則在元類實體方法的動態方法決議resolveInstanceMethod中查詢,主要是因為類方法元類中是實體方法,所以還需要查詢元類實體方法動態方法決議
  • 如果動態方法決議中,將其實現指向了其他方法,則繼續查詢指定的imp,即繼續慢速查詢lookUpImpOrForward流程

呼叫未實現的實體方法,檢視列印

  • 在main.m檔案中呼叫person的say666方法
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        
        Person *person = [Person alloc];
        [person say666];
        [Person sayNB];

    }
    return 0;
}
  • 列印結果
    呼叫實體方法say666報的錯
    在這裡插入圖片描述
    呼叫類方法sayNB報的錯
    在這裡插入圖片描述

  • 分析
    由於在快速查詢和慢速查詢時,都沒有找到say666實體方法sayNB類方法的實現,所以程式崩潰。

  • resolveInstanceMethod的原始碼

static void resolveInstanceMethod(id inst, SEL sel, Class cls)
{
    runtimeLock.assertUnlocked();
    ASSERT(cls->isRealized());
    SEL resolve_sel = @selector(resolveInstanceMethod:);

    // lookup 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);

    // Cache the result (good or bad) so the resolver doesn't fire next time.
    // +resolveInstanceMethod adds to self a.k.a. cls
    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));
        }
    }
}

實體方法

第一次補救

  • person.m檔案中重寫resolveInstanceMethod方法,先return父類別的此方法。
+ (BOOL)resolveInstanceMethod:(SEL)sel{
    NSLog(@"%@ say666來了",NSStringFromSelector(sel));

    return [super resolveInstanceMethod:sel];
}

列印結果:
在這裡插入圖片描述

分析
· 在執行實體方法say666時,在快速、慢速查詢都沒有找到的情況下,就會執行到resolveInstanceMethod方法。但是我們檢視列印,執行了兩遍這個方法,第次執行是在慢速查詢結束後執行的,那麼第二次是在什麼時候呢?此問題我們先記錄一下。

  • 繼續修改resolveInstanceMethod方法,將其返回一個固定的imp
+ (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];
}

列印結果:
在這裡插入圖片描述
分析:

· 在第動態方法決議時,返回sayMaster方法的imp,那麼此時說明已經找到一個方法的IMP,直接返回imp

類方法

針對類方法,與實體方法類似,同樣可以通過重寫resolveClassMethod類方法來解決前文的崩潰問題,即在LGPerson類中重寫該方法,並將sayNB類方法的實現指向類方法lgClassMethod

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

列印結果:
在這裡插入圖片描述
分析:
resolveClassMethod類方法的重寫需要注意一點,傳入的cls不再是,而是元類,可以通過objc_getMetaClass方法獲取類的元類,原因是因為類方法元類中是實體方法

  • 在NSObject中實現resolveInstanceMethod,也可以解決類方法找不到問題

在原始碼中,如果是元類的話,會有下面這行程式碼,如果在沒有找到類方法,那麼就會在元類的物件方法解析方法中查詢。

if (!lookUpImpOrNil(inst, sel, cls)) { //如果沒有找到或者為空,在元類的物件方法解析方法中查詢
            resolveInstanceMethod(inst, sel, cls);
        }

· 在isa走點陣圖中,繼承關係是:元類 -> 根元類 -> NSObject -> nil元類最終繼承於NSObject,那麼我們在NSObject中實現resolveInstanceMethod,那麼類方法也會最終執行到NSObject中。

NSObject分類

@implementation NSObject (LG)

// 呼叫方法的時候 - 分類

+ (BOOL)resolveInstanceMethod:(SEL)sel{
    

    NSLog(@"%@ 來了",NSStringFromSelector(sel));
    if (sel == @selector(say666)) {
        NSLog(@"%@ 來了",NSStringFromSelector(sel));

        IMP imp           = class_getMethodImplementation(self, @selector(sayMaster));
        Method sayMMethod = class_getInstanceMethod(self, @selector(sayMaster));
        const char *type  = method_getTypeEncoding(sayMMethod);
        return class_addMethod(self, sel, imp, type);
    }
    else if (sel == @selector(sayNB)) {
        
        IMP imp           = class_getMethodImplementation(objc_getMetaClass("Person"), @selector(lgClassMethod));
        Method sayMMethod = class_getInstanceMethod(objc_getMetaClass("Person"), @selector(lgClassMethod));
        const char *type  = method_getTypeEncoding(sayMMethod);
        return class_addMethod(objc_getMetaClass("Person"), sel, imp, type);
    }
    return NO;
}
@end

在這裡插入圖片描述
分析:
· 列印結果中呼叫NSObjectresolveInstanceMethod方法,
· 但是也列印一些不相關的資訊,這些資訊說明了別的方法也會走到此方法中。

總結:

  • 實體方法動態方法決議時執行的是resolveInstanceMethod方法進行補救。
  • 類方法動態方法決議時執行的是resolveClassMethodresolveInstanceMethod方法進行補救。

訊息轉發

在慢速查詢的流程中,我們瞭解到,如果快速+慢速沒有找到方法實現,動態方法決議也不行,就使用訊息轉發,但是,我們找遍了原始碼也沒有發現訊息轉發的相關原始碼,可以通過以下方式來了解,方法呼叫崩潰前都走了哪些方法。

流程圖:
在這裡插入圖片描述

通過instrumentObjcMessageSends方式列印傳送訊息的紀錄檔

  • 通過lookUpImpOrForward –> log_and_fill_cache -->
    logMessageSend,在logMessageSend原始碼下方找到instrumentObjcMessageSends的原始碼實現,所以,在main中呼叫instrumentObjcMessageSends列印方法呼叫的紀錄檔資訊,有以下兩點準備工作

1、開啟 objcMsgLogEnabled 開關,即呼叫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;
}

  • 通過logMessageSend原始碼,瞭解到訊息傳送列印資訊儲存在/tmp/msgSends 目錄,如下所示
    在這裡插入圖片描述

  • 執行程式碼,並前往/tmp/msgSends 目錄,發現有msgSends開頭的紀錄檔檔案,開啟發現在崩潰前,執行了以下方法

    · 兩次動態方法決議resolveInstanceMethod方法
    · 兩次訊息快速轉發forwardingTargetForSelector方法
    · 兩次訊息慢速轉發methodSignatureForSelector + resolveInstanceMethod
    在這裡插入圖片描述

訊息快速轉發【第二次機會】

訊息的快速轉發是執行的forwardingTargetForSelector方法。

  • 慢速查詢,以及動態方法決議沒有找到實現時,進行訊息轉發,首先是進行快速訊息轉發,即走到forwardingTargetForSelector方法

  • 如果返回訊息接收者,在訊息接收者中還是沒有找到,則進入另一個方法的查詢流程

  • 如果返回nil,則進入慢速訊息轉發

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

    // runtime + aSelector + addMethod + imp
    return [super forwardingTargetForSelector:aSelector];
}

訊息慢速轉發【第三次機會】

慢速轉發

針對第次機會即快速轉發中還是沒有找到,則進入最後的一次挽救機會,即在Person中重寫methodSignatureForSelector,如下所示

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

列印結果:
在這裡插入圖片描述
分析:

  • methodSignatureForSelector方法中返回了一個方法型別
  • forwardInvocation,處理返回的方法型別,並處理invocation事務,修改invocationtarget[LGStudent alloc],呼叫 [anInvocation invoke] 觸發 即LGPerson類的say666實體方法的呼叫會呼叫LGStudentalloc方法。

慢速轉發中的動態方法決議

  • 我們在resolveInstanceMethodlookUpImpOrNil處打個斷點,檢視當前堆疊資訊

  • 第一次執行動態方法決議時的堆疊是:
    _objc_msgSend_uncached ==> lookUpImpOrForward ==> resolveInstanceMethod
    在這裡插入圖片描述

  • 次執行動態方法決議時的堆疊是:
    methodSignatureForSelector > lookUpImpOrForward > resolveInstanceMethod
    在這裡插入圖片描述

  • 在堆疊資訊中,第次執行動態方法決議時,是在執行訊息慢速轉發時執行的,並且是methodSignatureForSelector之後,forwardInvocation之前。

總結

OC方法呼叫就是訊息傳送,那麼objc_msgSend流程分別是: