iOS 方法動態決議和消息轉發機制

經過上一篇章的學習,OC調用方法,底層是調用 objc_msgSend 發送消息。在發送消息時會通過一系列的快速 查找、慢速查找,若是查找到對應的 IMP,直接返回;若是沒有找到,就會進入到方法的動態解析和消息轉發流程。 緩存

1. 動態方法決議

經過探索objc_msgSend源碼,當慢速查找依然沒有找到IMP時,會進入方法動態解析階段,源碼以下:bash

在通過 _class_resolveMethod方法後,在進行一次 retry,從新進行一遍方法的查找流程,而只有一次動態方法解析的機會就是在 _class_resolveMethod方法中。

_class_resolveMethod源碼以下:學習

void _class_resolveMethod(Class cls, SEL sel, id inst)
{
    // 是不是元類
    if (! cls->isMetaClass()) {
        // try [cls resolveInstanceMethod:sel]

        _class_resolveInstanceMethod(cls, sel, inst);
    } 
    else {
        // try [nonMetaClass resolveClassMethod:sel]
        // and [cls resolveInstanceMethod:sel]
        _class_resolveClassMethod(cls, sel, inst); // 已經處理
        if (!lookUpImpOrNil(cls, sel, inst, 
                            NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) 
        {
            // 對象方法 決議
            _class_resolveInstanceMethod(cls, sel, inst);
        }
    }
}
複製代碼

由於類方法是存儲在元類中,因此在_class_resolveMethod中的處理有所不一樣ui

元  類:說明是對元類中的類方法進行處理,可是元類中的方法是在根元類中以實例方法的形式存儲的,因此
最終會查找根元類的實例方法,調用實例方法解析查找。

非元類:對儲存在類中的實例方法進行處理。
複製代碼

1.1 實例方法動態解析 _class_resolveInstanceMethod

_class_resolveInstanceMethod方法中對實例方法動態解析,源碼以下:spa

static void _class_resolveInstanceMethod(Class cls, SEL sel, id inst)
{
    // 1. 判斷系統是否實現SEL_resolveInstanceMethod方法
    // 即+(BOOL)resolveInstanceMethod:(SEL)sel, 
    // 繼承自NSObject的類,默認實現,返回NO
    if (! lookUpImpOrNil(cls->ISA(), SEL_resolveInstanceMethod, cls, 
                         NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) 
    {
        // Resolver not implemented.
        // 不是NSObject的子類,也未實現+(BOOL)resolveInstanceMethod:(SEL)sel,
        // 直接返回,沒有動態解析的必要
        return;
    }

    // 2. 系統給你一次機會 - 你要不要針對 sel 來操做一下下
    BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
    bool resolved = msg(cls, SEL_resolveInstanceMethod, sel);

    
    // 3. 再次尋找IMP
    IMP imp = lookUpImpOrNil(cls, sel, inst, 
                             NO/*initialize*/, YES/*cache*/, NO/*resolver*/);

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

由此:咱們能夠在+(BOOL)resolveInstanceMethod:(SEL)sel方法中對未實現的方法指定已實現方法的IMP,並添加到類中,實現方法動態解析,防止系統崩潰。3d

+ (BOOL)resolveInstanceMethod:(SEL)sel{
    
    NSLog(@"來了老弟:%s - %@",__func__,NSStringFromSelector(sel));

    if (sel == @selector(saySomething)) {
        NSLog(@"說話了");
        IMP sayHIMP = class_getMethodImplementation(self, @selector(sayHello));
        Method sayHMethod = class_getInstanceMethod(self, @selector(sayHello));
        const char *sayHType = method_getTypeEncoding(sayHMethod);
        return class_addMethod(self, sel, sayHIMP, sayHType);
        }
    
    return [super resolveInstanceMethod:sel];
}
複製代碼

咱們也能夠在此方法中根據方法的前綴、路由、事務,跳轉的不一樣的頁面,進行bug收集。日誌

1.2 _class_resolveClassMethod

若是是元類,在_class_resolveClassMethod方法中對相關類方法的進行動態解析,該方法的實現步驟和實例方法的實現步驟相似,區別是消息發送的時候獲取的是元類,即:code

_class_resolveClassMethod源碼以下:orm

static void _class_resolveClassMethod(Class cls, SEL sel, id inst)
{
    assert(cls->isMetaClass());
    // 1. 判斷系統是否實現SEL_resolveClassMethod方法
    // 即+(BOOL)resolveClassMethod:(SEL)sel, 
    // 繼承自NSObject的類,默認實現,返回NO 
    if (! lookUpImpOrNil(cls, SEL_resolveClassMethod, inst, 
                         NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) 
    {
        // Resolver not implemented.
        return;
    }
    //2. 系統給你一次機會 
    //   經過objc_msgSend調用一下+(BOOL)resolveClassMethod:(SEL)sel方法
    BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
    bool resolved = msg(_class_getNonMetaClass(cls, inst), 
                        SEL_resolveClassMethod, sel);

    // Cache the result (good or bad) so the resolver doesn't fire next time. // +resolveClassMethod adds to self->ISA() a.k.a. cls //3. 再次查找IMP IMP imp = lookUpImpOrNil(cls, sel, inst, NO/*initialize*/, YES/*cache*/, NO/*resolver*/); 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 resolveClassMethod:%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));
        }
    }
}

複製代碼

所以,當咱們要進行類方法的動態解析時,須要添加+ (BOOL)resolveClassMethod:(SEL)sel進行動態方法解析:cdn

+ (BOOL)resolveClassMethod:(SEL)sel{
    
    NSLog(@"來了類方法:%s - %@",__func__,NSStringFromSelector(sel));

     if (sel == @selector(sayLove)) {
         NSLog(@"說- 說你你愛我");
         IMP sayHIMP = class_getMethodImplementation(self, @selector(sayObjc));
         Method sayHMethod = class_getClassMethod(self, @selector(sayObjc));
         const char *sayHType = method_getTypeEncoding(sayHMethod);
         // 類方法在元類 objc_getMetaClass("LGStudent")
         return class_addMethod(self, sel, sayHIMP, sayHType);
     }
     return [super resolveClassMethod:sel];
}
複製代碼

小結

  1. 方法先通過緩存查找,方法列表查找後,後進入動態方法解析階段
  2. 實例方法解析須要實現resolveInstanceMethod方法
  3. 類方法解析須要實現resolveClassMethod方法
  4. 類方法存儲在元類之中,因爲元類繼承自根元類根元類最終繼承自NSObject,所以對類方法解析的時候,最終會查找到NSObject。因爲元類和根源類由系統建立,沒法修改,因此能夠再根元類的父類NSObject中,添加對應的實例方法resolveInstanceMethod進行動態解析。

2. 消息轉發

在方法查找過程當中,通過緩存查找,方法列表查找和動態方法解析,若是以上步驟都沒有查找到IMP,也沒有進行方法動態解析,那麼就會進入最後一步,崩潰。

_objc_msgForward_impcache是彙編方法,以下:

STATIC_ENTRY __objc_msgForward_impcache
	// Method cache version

	// THIS IS NOT A CALLABLE C FUNCTION
	// Out-of-band Z is 0 (EQ) for normal, 1 (NE) for stret

	beq	__objc_msgForward
	b	__objc_msgForward_stret
	
	END_ENTRY __objc_msgForward_impcache
	

	ENTRY __objc_msgForward
	// Non-stret version

	MI_GET_EXTERN(r12, __objc_forward_handler)
	ldr	r12, [r12]
	bx	r12

	END_ENTRY __objc_msgForward


	ENTRY __objc_msgForward_stret
複製代碼

_objc_msgForward_impcache中,調用__objc_msgForward,而後調用__objc_forward_handler,轉掉_objc_forward_handler OC方法以下,而後就是經典崩潰。

那麼,在崩潰時,爲何會打印如上圖的一系列堆棧信息呢 ?

經過查看 lookUpImpOrForward源碼,如上圖,當查找到 IMP時,會調用 log_and_fill_cache方法,進行緩存填充和日誌存儲。

log_and_fill_cache如上圖,經過控制 objcMsgLogEnabled來控制日誌存儲,日誌會記錄在 /tmp/msgSends目錄下,而 objcMsgLogEnabled的賦值是在 instrumentObjcMessageSends之中,能夠暴露這個方法,來達到外部打日誌的操做。

在查看/tmp/msgSends目錄下的文件,如圖:

發現調用 resolveInstanceMethod:, forwardingTargetForSelectormethodSignatureForSelectordoesNotRecognizeSelector一系列方法,進行消息轉發。

2.1 快速轉發流程

經過查看forwardingTargetForSelector的官方文檔,

- (id)forwardingTargetForSelector:(SEL)aSelector方法,

1. 返回一個對象。若是這個對象非空、非nil,系統會將消息轉發給這個對象執行,不然,繼續查找其餘流程。
系統給了將這個SEL轉給其餘對象的機會。
2. 若是返回nil,或者沒有處理消息轉發,會走到forwardInvocation:方法進行處理,進入慢速消息轉發流程。
複製代碼

能夠經過一下代碼,將saySomething方法的消息轉發到LGTeacher類中實現,而不會引發系統崩潰,至此消息快速轉發結束。

- (id)forwardingTargetForSelector:(SEL)aSelector{
    NSLog(@"%s -- %@",__func__,NSStringFromSelector(aSelector));
    if (aSelector == @selector(saySomething)) {
        return [LGTeacher alloc];
    }
    return [super forwardingTargetForSelector:aSelector];
}
複製代碼

2.2 慢速轉發流程

進入慢速查找流程,首先必須先實現methodSignatureForSelector方法,返回一個簽名,這個方法簽名裏面封裝了返回值類型,參數類型等信息。

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
    NSLog(@"%s -- %@",__func__,NSStringFromSelector(aSelector));
    if (aSelector == @selector(saySomething)) { // v @ :
        return [NSMethodSignature signatureWithObjCTypes:"v@:"];
    }
    return [super methodSignatureForSelector:aSelector];
}
複製代碼

而後還必須實現- (void)forwardInvocation:(NSInvocation *)anInvocation;

- (void)forwardInvocation:(NSInvocation *)invocation
{
    SEL aSelector = [invocation selector];
 
    if ([friend respondsToSelector:aSelector])
        [invocation invokeWithTarget:friend];
    else
        [super forwardInvocation:invocation];
}
複製代碼

注意:

1. forwardInvocation 方法和 methodSignatureForSelector 方法必須同時實現
2. methodSignatureForSelector 會生成一個簽名,NSInvocation對象,將NSInvocation對象做爲
參數傳給 forwardInvocation 方法的
3. 在forwardInvocation方法裏面將消息給能處理該消息的對象,以免對象調用
didNotRecognizeSelector 方法致使崩潰
4. forwardInvocation 這個方法相似於將消息當作事務堆放起來,在這裏誰能夠操做就在這裏面操做,
就算不操做也不會崩潰,這裏也是防崩潰的最後處理機會。
複製代碼

接下來看一下系統NSObjectforwardInvocation的實現:

+ (void)forwardInvocation:(NSInvocation *)invocation {
    [self doesNotRecognizeSelector:(invocation ? [invocation selector] : 0)];
}

- (void)forwardInvocation:(NSInvocation *)invocation {
    [self doesNotRecognizeSelector:(invocation ? [invocation selector] : 0)];
}

// Replaced by CF (throws an NSException)
+ (void)doesNotRecognizeSelector:(SEL)sel {
    _objc_fatal("+[%s %s]: unrecognized selector sent to instance %p", 
                class_getName(self), sel_getName(sel), self);
}

// Replaced by CF (throws an NSException)
- (void)doesNotRecognizeSelector:(SEL)sel {
    _objc_fatal("-[%s %s]: unrecognized selector sent to instance %p", 
                object_getClassName(self), sel_getName(sel), self);
}

複製代碼

因而可知,系統最後是在doesNotRecognizeSelector方法中拋出異常的,因此重寫forwardInvocation方法後,無論裏面有麼有實現,或者執行父類的方法,程序都是不會崩潰的。

消息轉發流程圖:

總結

當調用了未實現的方法,三個解決途徑:

一、resolveInstanceMethod:爲發送消息的對象的添加一個IMP,而後再讓該對象去處理
二、forwardingTargetForSelector:將該消息轉發給能處理該消息的對象
三、methodSignatureForSelector和 forwardInvocation:第一個方法生成方法簽名,而後建立 
NSInvocation 對象做爲參數給第二個方法,而後在forwardInvocation  方法裏面作消息處理,
只要在第二個方法裏面不執行父類的方法,即便不處理也不會崩潰
複製代碼
相關文章
相關標籤/搜索