objc_msgSend消息轉(zhuǎn)發(fā)流程分析

代碼中我們難免會遇到未實現(xiàn)方法的情況(比如動使用方法名稱字符串冬天調(diào)用方法的情況),能不能避免unrecognized selector這種crash呢。答案當(dāng)然是肯定的,蘋果給我們提供了消息的動態(tài)轉(zhuǎn)發(fā)機(jī)制。下面我們來探究消息的轉(zhuǎn)發(fā)。
首先我們回到前面的lookUpImpOrForward方法。在方法查找過程中如果沒有找到方法的實現(xiàn),就會走到下面的一段代碼:

// No implementation found. Try method resolver once.

    if (resolver  &&  !triedResolver) {
        runtimeLock.unlock();
        _class_resolveMethod(cls, sel, inst);
        runtimeLock.lock();
        // Don't cache the result; we don't hold the lock so it may have 
        // changed already. Re-do the search from scratch instead.
        triedResolver = YES;
        goto retry;
    }

其中調(diào)用了_class_resolveMethod方法,繼續(xù)跟蹤:

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

如果不是元類,那么就是實例方法,否則就是類方法。
我們先進(jìn)入實例方法的流程查看:

static void _class_resolveInstanceMethod(Class cls, SEL sel, id inst)
{
    if (! lookUpImpOrNil(cls->ISA(), SEL_resolveInstanceMethod, cls, 
                         NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) 
    {
        // Resolver not implemented.
        return;
    }

    BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
    bool resolved = msg(cls, SEL_resolveInstanceMethod, 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(cls, sel, inst, 
                             NO/*initialize*/, YES/*cache*/, NO/*resolver*/);

    if (resolved  &&  PrintResolving) {...}
}

我們看到以上代碼中調(diào)用了兩次lookUpImpOrNil。
第一次是針對resolveInstanceMethod,如果沒有實現(xiàn)就直接返回了,所以O(shè)C底層肯定給我們實現(xiàn)了該方法。我們在源碼中搜索到了該方法的默認(rèn)實現(xiàn):

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

然后下面給對象發(fā)送了resolveInstanceMethod消息,實際上此時resolveInstanceMethod方法已經(jīng)可以在緩存中查到了。

第二次是針對我們自己未實現(xiàn)的方法。此時又開始查找目標(biāo)方法的實現(xiàn)。 所以我們可以感覺到前面的resolveInstanceMethod方法應(yīng)該是為我們提供了實現(xiàn)方法的機(jī)會。我們只需要在自己的對象中去實現(xiàn)resolveInstanceMethod方法,然后在這個方法中動態(tài)的添加目標(biāo)方法的實現(xiàn)。這樣的話在下面的lookUpImpOrNil方法查找中就能找到我們動態(tài)添加的方法實現(xiàn)了。

我們在我們的對象中添加resolveInstanceMethod的實現(xiàn):

- (void)sayHello{
    NSLog(@"%s",__func__);
}
+ (BOOL)resolveInstanceMethod:(SEL)sel{
    if (sel == @selector(saySomething)) {
        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];
}

這樣處理后,假設(shè)我們未實現(xiàn)saySomething方法,然后調(diào)用了saySomething方法。就會調(diào)用sayHello方法,而不會直接unrecognized selector崩潰了。

上面我們講的是實例方法的動態(tài)決議,下面我們再看下類方法的動態(tài)決議。
_class_resolveMethod方法中,如果是類方法的話,就會走下面的代碼:

        _class_resolveClassMethod(cls, sel, inst);
        if (!lookUpImpOrNil(cls, sel, inst, 
                            NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) 
        {
            // 對象方法 決議
            _class_resolveInstanceMethod(cls, sel, inst);
        }

其中_class_resolveClassMethod對應(yīng)著實例方法中的_class_resolveInstanceMethod。但是下面還可能會調(diào)用_class_resolveInstanceMethod。為什么類方法動態(tài)解析還會調(diào)用_class_resolveInstanceMethod呢?我們可以聯(lián)想下ISA的走位圖,類方法存在元類中,那么類方法的查找順序就:
元類---父元類---根元類---NSObject。所以不管是實例方法還是類方法,最后都會找到NSObject。系統(tǒng)可能是為了容錯處理,如果我們沒有在_class_resolveClassMethod方法中動態(tài)添加未實現(xiàn)的類方法,就會繼續(xù)走一遍_class_resolveInstanceMethod。
既然所有的方法(不管是實例方法還是類方法),如果未實現(xiàn)的話,都會走_class_resolveInstanceMethod方法。那么我們就可以在NSObject的該方法中做統(tǒng)一的處理。例如可以在此處做一個統(tǒng)一的容錯處理。比如我們可以在這里根據(jù)不同類別的方法(可以用方法前綴來區(qū)分),做出不同的容錯處理(可以跳轉(zhuǎn)到不同容錯界面)。但是在這里統(tǒng)一做容錯處理也有些弊端:
1、所以的處理都是在NSObject中統(tǒng)一處理,耦合度比較高;
2、如果具體的類實現(xiàn)了該方法,那么就會別攔截,走不到NSObject的這個方法了。

消息轉(zhuǎn)發(fā)

如果方法的動態(tài)解析沒有處理的話,就會進(jìn)入消息的轉(zhuǎn)發(fā)階段,消息轉(zhuǎn)發(fā)又分為快速轉(zhuǎn)發(fā)階段和慢速轉(zhuǎn)發(fā)階段。
1、快速轉(zhuǎn)發(fā):轉(zhuǎn)給另外一個對象去處理;

- (id)forwardingTargetForSelector:(SEL)aSelector{
    if (aSelector == @selector(saySomething)) {
        return [OtherObjc alloc];
    }
    return [super forwardingTargetForSelector:aSelector];
}

如果沒有實現(xiàn)方法的動態(tài)解析,接下來就會走到上面這個方法。這個方法需要返回一個對象,由該對象去實現(xiàn)該方法。這樣就會轉(zhuǎn)發(fā)給OtherObjc對象,去調(diào)用OtherObjc對象的saySomething方法。

@interface OtherObjc : NSObject

@end

@implementation OtherObjc

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

@end

2、慢速轉(zhuǎn)發(fā):將所有未實現(xiàn)的方法封裝成NSInvocation,放在統(tǒng)一的一個地方,你可以選擇進(jìn)行處理,也可以選擇不處理。
首先我們需要實現(xiàn)methodSignatureForSelector,只有實現(xiàn)了該方法,并且在該方法中返回對應(yīng)的簽名,后面才會走forwardInvocation方法。

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
    if (aSelector == @selector(saySomething)) { // v @ :
        return [NSMethodSignature signatureWithObjCTypes:"v@:"];
    }
    return [super methodSignatureForSelector:aSelector];
}

上面的方法返回了v@:的方法簽名,該簽名表示方法返回值為void,并且有一個參數(shù)。
然后就會調(diào)用下面的方法

- (void)forwardInvocation:(NSInvocation *)anInvocation{
    // 系統(tǒng)本質(zhì)
   SEL aSelector = [anInvocation selector];
    
   if ([[LGTeacher alloc] respondsToSelector:aSelector])
       [anInvocation invokeWithTarget:[LGTeacher alloc]];
   else
       [super forwardInvocation:anInvocation];
}

我們通過Invocation可以獲取到方法的相關(guān)信息,然后針對不同方法進(jìn)行相應(yīng)的處理??梢詫⒉煌悇e的方法交于不同的對象去處理。如果在這里我們只是實現(xiàn)了forwardInvocation方法,但是沒有針對未實現(xiàn)的Selector做出相應(yīng)的處理。調(diào)用未實現(xiàn)的方法的時候,也是不會報錯“unRecognize selector”的。

疑問一

我們調(diào)用一個未實現(xiàn)的OC實例方法saySomething,可以通過在resolveInstanceMethod方法中添加實現(xiàn),從而實現(xiàn)OC方法的動態(tài)解析。該方法只會調(diào)用一次。

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

但是如果實現(xiàn)了resolveInstanceMethod:方法。但是沒有在該方法中添加方法saySomething的實現(xiàn),就會調(diào)用兩次resolveInstanceMethod:方法,為什么呢?
resolveInstanceMethod:實現(xiàn)內(nèi)容如下

+ (BOOL)resolveInstanceMethod:(SEL)sel{
    NSLog(@"==== %s - %@",__func__,NSStringFromSelector(sel));
    return [super resolveInstanceMethod:sel];
}

我們可以推理,如果在resolveInstanceMethod:方法中添加了方法saySomething實現(xiàn),然后就會返回YES,此時resolveInstanceMethod:調(diào)用了一次。而如果未添加方法saySomething實現(xiàn),就會第二次調(diào)用resolveInstanceMethod:,那么第二次的調(diào)動肯定是在后面的流程。后面的流程是什么呢?
首先下面調(diào)用了[super resolveInstanceMethod:sel];,我們可以跟蹤該方法的調(diào)用是在第二次調(diào)用的前面還是后面,經(jīng)過追蹤,是在第二次調(diào)用的前面。然后我們繼續(xù)追蹤第二次調(diào)用的時機(jī)。
下面的流程就是消息的動態(tài)轉(zhuǎn)發(fā)流程,

- (id)forwardingTargetForSelector:(SEL)aSelector {
    NSLog(@"=====%s - %@",__func__,NSStringFromSelector(aSelector));
    return [super forwardingTargetForSelector:aSelector];
}

- (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{
    NSLog(@"=====%s - %@",__func__);
    [super forwardInvocation:anInvocation];
}

使用以上代碼流程繼續(xù)跟蹤,發(fā)現(xiàn)第二次調(diào)用的時機(jī)位于methodSignatureForSelectorforwardInvocation之間。
然后再怎么追蹤第二次方法的調(diào)用呢?只能通過匯編了。我們運(yùn)行項目,此時會crash,因為我們此時我們沒有在動態(tài)解析中添加方法實現(xiàn)。crash后我們可以看到崩潰前的調(diào)用堆棧,類似下圖

image.png

然后我們就可以去右邊的匯編代碼中查找具體的匯編流程了。但是最后還是沒有找到有用的信息,只能猜測是因為沒有開源,所以編譯器隱藏了這部分實現(xiàn)。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容