iOS底層探索:方法查找與消息轉發

Objective-C的方法的調用都是一個消息轉發的過程,objc_msgSend(receiver, selector, ...),第一個參數表示消息的接收者,第二個參數表示消息的名稱,即方法。

方法的調用,中間經歷了消息的快速查找、慢速查找、消息轉發三個過程。

1.方法快速查找

消息的快速查找在底層通過彙編語言實現,會在cache中依據繼承關係鏈來查找。


如果是實例方法instanceMethod,則會類的繼承鏈上進行查找,由subclass->superclass->rootclass中查找。如果是類方法classMethod,則會在元類的繼承鏈上查找,由subclass-meta->superclass-meta->rootclass-meta->rootclass中查找。

如果快速查找沒有命中緩存,則會進入慢速查找函數lookUpImpOrForward

2.方法慢速查找

方法的慢速查找入口在lookUpImpOrForward,看一下核心代碼:

NEVER_INLINE IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior){
    IMP imp = nil;
    Class curClass = cls;
    ...
    for (;;) {
        /*從當前類中查找符合條件的method,如果找到了,則進入done*/
        Method meth = getMethodNoSuper_nolock(curClass, sel);
        if (meth) {  imp = meth->imp(false); goto done;}

        /*沒有找到的話,則會向上查找superclass。沒有superclass的話話,則會進入_objc_msgForward_impcache*/
        if (slowpath((curClass = curClass->getSuperclass()) == nil)) {  imp = forward_imp; break; }

        /*優先查找cache,找到則進入done流程,否則繼續慢速查找父類的*/
        imp = cache_getImp(curClass, sel);
        if (slowpath(imp == forward_imp)) { break;  }
        if (fastpath(imp)) { goto done; }
    }
    
    /*仍然沒有找到:進入動態決議流程*/
    if (slowpath(behavior & LOOKUP_RESOLVER)) {
        behavior ^= LOOKUP_RESOLVER;
        return resolveMethod_locked(inst, sel, cls, behavior);
    }

    done:
    ...
    done_unlock:
    ...
    return imp;
}

消息轉發,即動態決議流程,判斷是類方法還是實例方法,來做不同的操作

static NEVER_INLINE IMP resolveMethod_locked(id inst, SEL sel, Class cls, int behavior){
    ...
    if (! cls->isMetaClass()) {
        /*如果是類,則執行resolveInstanceMethod*/
        resolveInstanceMethod(inst, sel, cls);
    } 
    else {
        /*如果是元類,則執行resolveClassMethod*/
        resolveClassMethod(inst, sel, cls);
        if (!lookUpImpOrNilTryCache(inst, sel, cls)) {
            resolveInstanceMethod(inst, sel, cls);
        }
    }
    return lookUpImpOrForwardTryCache(inst, sel, cls, behavior);
}

如若是實例方法則執行+resolveInstanceMethod,如果是類方法則執行+resolveClassMethod方法。

static void resolveClassMethod(id inst, SEL sel, Class cls){
     /*如果沒有實現+ resolveClassMethod方法則return,因爲在NSObject中實現了該方法,所以不存在找不到的情況??*/
    if (!lookUpImpOrNilTryCache(inst, @selector(resolveClassMethod:), cls)) {
        return;
    }

    ...
    BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
    bool resolved = msg(nonmeta, @selector(resolveClassMethod:), sel);
    //從緩存中查找一遍
    IMP imp = lookUpImpOrNilTryCache(inst, sel, cls);
}

static void resolveInstanceMethod(id inst, SEL sel, Class cls){
    /*如果沒有實現+ resolveInstanceMethod方法則return,因爲在NSObject中實現了該方法,所以不存在找不到的情況??*/
    if (!lookUpImpOrNilTryCache(cls, @selector(resolveInstanceMethod:), cls->ISA(/*authenticated*/true))) {
        return;
    }

    ...
    BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
    bool resolved = msg(cls, @selector(resolveInstanceMethod:), sel);
    //從緩存中查找一遍
    IMP imp = lookUpImpOrNilTryCache(inst, sel, cls);
}

2.1 代碼調用未實現的方法

知道系統處理異常的流程後我們就寫代碼試試,首先在NXPerson@interface中申明1個方法,並不實現該方法:

//定義一個NXPerson class
@interface NXPerson : NSObject
- (void)instanceMethod;
@end
@implementation NXPerson
@end
//調用未實現的方法
NXPerson *p = [NXPerson alloc];
[p instanceMethod];

錯誤打印:


調用一個沒有實現的方法,最終crash掉,且拋出錯誤-[NXPerson instanceMethod]: unrecognized selector sent to instance 0x1011044c0

2.2 通過 resolveInstanceMethod來添加實現

按照源碼中的提供的思路來實現resolveInstanceMethod方法,我們發現該方法在crash和拋出異常之前被調用了2次,儘管我們只在該方法中調用父類的方法。官方提供該方法的目的在於我們可以在這種情況下,爲一個沒有實現的方法動態添加方法的實現。

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    NSLog(@"%s",__func__);
    if(sel == NSSelectorFromString(@"instanceMethod")){
        //將instanceMethod的實現替換爲run方法的實現
        SEL __sel = NSSelectorFromString(@"run");
        IMP imp = class_getMethodImplementation(self, __sel);
        const char *type = method_getTypeEncoding(class_getInstanceMethod(self, __sel));
        return  class_addMethod(self, sel, imp, type);
    }
    return [super resolveInstanceMethod:sel];
}

通過如上代碼,我們將instanceMethod這個sel添加了一個實現imp,再調用[p instanceMethod]時,系統就會執行run方法,符合預期,結果如下:


這種方式我們需要保證我們添加的實現是確實存在的。

2.3 通過forwardingTargetForSelector:指定消息接收者

消息動態決議中還會有第二種補救措施,實現-(id)forwardingTargetForSelector:(SEL)sel方法,返回能夠消化該sel的實例即可。

@interface NXTarget : NSObject
- (void)instanceMethod;
@end

@implementation NXTarget
- (void)instanceMethod{
    NSLog(@"%s",__func__);
}
@end

測試如下方法,需要將resolveInstanceMethod方法屏蔽掉。

- (id)forwardingTargetForSelector:(SEL)aSelector {
    NSLog(@"%s",__func__);
    if(aSelector == NSSelectorFromString(@"instanceMethod")) {
        //需要保證NXTarget中有同名的instanceMethod方法
        return [[NXTarget alloc] init];
    }
    return [super forwardingTargetForSelector:aSelector];
}

通過上面的代碼,我們將名爲instanceMethod的方法指定了一個新的接收者,這裏需要注意在新的實例中需要有instanceMethod實例方法,那麼指定爲自己、當前類的實例、返回nil,或者指定的實例中不包含該方法仍然是無效的,會繼續觸發異常。

2.4通過forwardInvocation:轉發消息

其中methodSignatureForSelectorforwardInvocation必須要成堆出現,否則無效。

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    NSLog(@"%s",__func__);
    if(aSelector == NSSelectorFromString(@"instanceMethod")){
        //返回instanceMethod的簽名
        return [NSMethodSignature signatureWithObjCTypes:"v@:"];
    }
    return [super methodSignatureForSelector:aSelector];
}

- (void)forwardInvocation:(NSInvocation *)anInvocation {
    NSLog(@"%s",__func__);
    if(anInvocation.selector == NSSelectorFromString(@"instanceMethod")){
        //對instanceMethod進行處理,在這裏可以指定消息的接收者`anInvocation.target`,或者指定消息`anInvocation.selector`,通常只需要更改其中之一即可。
        //anInvocation.target = [[NXTarget alloc] init];
        anInvocation.selector = NSSelectorFromString(@"run");
        //執行方法
        [anInvocation invoke];
        return;
    }
    [super forwardInvocation:anInvocation];
}

重寫- (void)forwardInvocation:(NSInvocation *)anInvocation方法,我們可以指定新的消息接收者和消息體,最後調用[anInvocation invoke]方法將會觸發新的消息發送流程。如果我們只提供了該方法的空實現,那麼原有的方法將得不到執行,也不會拋出異常了。

- (void)forwardInvocation:(NSInvocation *)anInvocation {
    NSLog(@"%s",__func__);
}

綜上所述,在消息查找不到的情況下,有3次亡羊補牢的機會,通過流程圖表述如下:


3.類方法查找流程

類方法的查找流程在這裏不做贅述,與實例方法的查找流程大同效益

1.可以在+ (BOOL)resolveClassMethod:(SEL)sel中添加方法,將方法添加到元類
2.可以在+ (id)forwardingTargetForSelector:(SEL)aSelector中返回類對象。
3.可以在 + (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector中返回方法的簽名,實現+ (void)forwardInvocation:(NSInvocation *)anInvocation方法。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章