1. 程式人生 > Android開發 >iOS的OC的方法的決議與訊息轉發原理

iOS的OC的方法的決議與訊息轉發原理

前言

筆者整理了一系列有關OC的底層文章,希望可以幫助到你。

1.iOS的OC物件建立的alloc原理

2.iOS的OC物件的記憶體對齊

3.iOS的OC的isa的底層原理

4.iOS的OC原始碼分析之類的結構分析

5.iOS的OC的方法快取的原始碼分析

6.iOS的OC的方法的查詢原理

OC的方法的查詢是通過訊息的傳送來查詢函式的IMP,首先通過objc_msgSend來進行慢速查詢(cache_t),如果慢速找不到,就需要進行方法的快速查詢,具體可以瞭解iOS的OC的方法的查詢原理這篇文章。但是,如果通過慢速和快速的查詢都找不到的話,就會直接報錯。是不是說如果找不到就沒有辦法走其他的操作了呢?並不是的,接下來這邊文章就是介紹方法的決議和訊息轉發原理

。為了接下來的內容介紹定義一個TestObject類。

1.方法的決議

實現下面的程式碼,其中testErrorMthod方法是沒有在TestObject類宣告和實現的,直接執行是報錯的。

TestObject *testObject = [[TestObject alloc] init];
[testObject performSelector:@selector(testErrorMthod)];

==========執行結果=================

LGTest[1639:40195] -[TestObject testErrorMthod]: unrecognized selector sent to instance 0x1010c3600
LGTest[1639:40195] *** Terminating app due to uncaught exception 'NSInvalidArgumentException'
,reason: '-[TestObject testErrorMthod]: unrecognized selector sent to instance 0x1010c3600' *** First throw call stack: ( 0 CoreFoundation 0x00007fff3c7438ab __exceptionPreprocess + 250 1 libobjc.A.dylib 0x000000010038dfea objc_exception_throw + 42 2 CoreFoundation 0x00007fff3c7c2b61 -[NSObject(NSObject) __retain_OA] + 0 3 CoreFoundation 0x00007fff3c6a7adf ___forwarding___ + 1427 4 CoreFoundation 0x00007fff3c6a74b8 _CF_forwarding_prep_0 + 120 5 libobjc.A.dylib 0x00000001003ccf26 -[NSObject performSelector:] + 70 6 LGTest 0x0000000100001afd main + 93 7 libdyld.dylib 0x00007fff73d6b7fd start + 1 ) libc++abi.dylib: terminating with uncaught exception of type
NSException 複製程式碼

在之前介紹方法的快速查詢流程中的lookUpImpOrForward函式中,有這段原始碼,在方法查詢不到的時候,會執行到裡面去。

 // 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;
    }
複製程式碼

1.1 _class_resolveMethod

下面是_class_resolveMethod的原始碼,其中cls是類,sel方法的編號,inst是例項物件。

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

        _class_resolveInstanceMethod(cls,inst);
    } 
    else {
        // try [nonMetaClass resolveClassMethod:sel]
        // and [cls resolveInstanceMethod:sel]
        _class_resolveClassMethod(cls,inst);
        if (!lookUpImpOrNil(cls,inst,NO/*initialize*/,YES/*cache*/,NO/*resolver*/)) 
        {
            _class_resolveInstanceMethod(cls,inst);
        }
    }
}
複製程式碼

這段原始碼對傳進來的cls判斷是否是元類,通過之前的文章可以知道類方法是存在元類中的。所以如果傳進來的cls是類,就直接執行_class_resolveInstanceMethod函式,如果是元類執行_class_resolveClassMethod函式。

1.2 _class_resolveInstanceMethod

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

    BOOL (*msg)(Class,SEL,SEL) = (typeof(msg))objc_msgSend;
    bool resolved = msg(cls,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,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->isMetaClass() ? '+' : '-',sel_getName(sel));
        }
    }
}

複製程式碼

其中lookUpImpOrNil函式對類方法SEL_resolveInstanceMethod查詢是否有存在,發現SEL_resolveInstanceMethod的實現是resolveInstanceMethod方法,這個是在NSObject類裡面有實現的,所以這個是返回true的。

然後接著執行,下面這段程式碼通過objc_msgSend查詢當前的類是否執行resolveInstanceMethod方法。

BOOL (*msg)(Class,SEL) = (typeof(msg))objc_msgSend;
bool resolved = msg(cls,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,NO/*resolver*/);
複製程式碼

通過前面的文章,可以知道如果當前的類沒有實現resolveInstanceMethod類方法就會查詢父類是否實現,因為在NSObject類中是預設實現返回NO的,如果當前的類有實現resolveInstanceMethod類方法就會執行方法裡的內容。並且在實現的resolveInstanceMethod類方法中對沒有找到的sel重新賦值一個IMP,下面是在TestObject類的實現程式碼。

-(void)testOk{
    NSLog(@"%p===testOk",__func__);
}

+(BOOL)resolveInstanceMethod:(SEL)sel{
    NSLog(@"===執行resolveInstanceMethod===%s===%@",__func__,NSStringFromSelector(sel));
    if(sel == @selector(testErrorMthod)){
        Method okMethod = class_getInstanceMethod(self,@selector(testOk));
        IMP okImp = method_getImplementation(okMethod);
        const char *type =  method_getTypeEncoding(okMethod);
        return class_addMethod(self,okImp,type);
    }
    return [super resolveInstanceMethod:sel];
}


======執行的結果=======
LGTest[2769:92089] ===執行resolveInstanceMethod===+[TestObject resolveInstanceMethod:]===testErrorMthod
LGTest[2769:92089] 0x100001f2a===testOk
Program ended with exit code: 0
複製程式碼

此時還執行一次lookUpImpOrNil函式在快取中查詢。所以在TestObject類中定義了這個方法並且為testErrorMthod賦值了新的IMP

    // 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,NO/*resolver*/);
複製程式碼

上面的是對例項方法的動態決議,其實對類方法的也是差不多的,如果是類方法此時會執行如下原始碼

        _class_resolveClassMethod(cls,inst);
        }

複製程式碼

1.3 _class_resolveClassMethod

static void _class_resolveClassMethod(Class cls,id inst)
{
    assert(cls->isMetaClass());

    if (! lookUpImpOrNil(cls,SEL_resolveClassMethod,SEL) = (typeof(msg))objc_msgSend;
    bool resolved = msg(_class_getNonMetaClass(cls,inst),sel);

    // Cache the result (good or bad) so the resolver doesn't fire next time.
    // +resolveClassMethod adds to self->ISA() a.k.a. cls
    IMP imp = lookUpImpOrNil(cls,imp);
        }
        else {
            // Method resolver didn't add anything?
            _objc_inform("RESOLVE: +[%s resolveClassMethod:%s] returned YES"
                         ",sel_getName(sel));
        }
    }
}
複製程式碼

通過原始碼可以知道是與_class_resolveInstanceMethod原始碼是差不多的,主要區別是,這個原始碼的SEL_resolveClassMethod是要實現resolveClassMethod這個類方法,需要在TestObject類中實現類方法resolveClassMethod並在這個方法裡面實現邏輯。但是為什麼執行完_class_resolveClassMethod函式之後還會再做一次lookUpImpOrNil函式的判斷呢?因為如果在_class_resolveClassMethod是沒有做處理的,由於元類的查詢方法查詢流程是會往根元類查詢最終會找到NSObject這個類,所以如果在根元類都找不到的情況下會找到NSObject類的方法裡面。而NSObject類是有預設實現了這兩個類方法的並且預設返回NO

2. 訊息轉發

如果對不存在的方法的查詢,沒有實現上面的方法決議,此時會在lookUpImpOrForward函式中執行

    // No implementation found,and method resolver didn't help. 
    // Use forwarding.

    imp = (IMP)_objc_msgForward_impcache;
    cache_fill(cls,imp,inst);
複製程式碼

其中_objc_msgForward_impcache就會執行到彙編中的內容,然後執行__objc_msgForward,最終會執行到_objc_forward_handler函式中,最終是會報錯的。


	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
	
// Default forward handler halts the process.
__attribute__((noreturn)) 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),self);
}
void *_objc_forward_handler = (void*)objc_defaultForwardHandler;
複製程式碼

這些流程就是在之前文章的方法的查詢原理有介紹。是不是就是說訊息的轉發流程就跟宗不了呢?並不是的。在lookUpImpOrForward函式中有一個可以列印log的方法log_and_fill_cache中的logMessageSend方法裡面有介紹可以根據objcMsgLogEnabled屬性來控制列印log

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

為了可以看到訊息轉發的過程中實現了那些方法,可以在mac的專案中實現

extern void instrumentObjcMessageSends(BOOL flag);
int main(int argc,const char * argv[]) {
    @autoreleasepool {
        
        TestObject *test = [TestObject alloc] ;
        instrumentObjcMessageSends(true);
        [test performSelector:@selector(testErrorMthod)];
        instrumentObjcMessageSends(false);

    }
    return 0;
}
複製程式碼

就可以在路徑:/tmp/msgSends-找到生成的msgSends檔案

這個msgSends-7265檔案的內容

這個列印log是要在mac的專案下才可以,如果在其他的專案下是會報objc[6984]: lock 0x100cbf0c0 (runtimeLock) acquired before 0x100cbf040 (objcMsgLogLock) with no defined lock order這種錯誤。

從打印出來的方法可以知道,在訊息的轉發的過程中執行的過程是resolveInstanceMethod-->forwardingTargetForSelector-->methodSignatureForSelector-->resolveInstanceMethod-->doesNotRecognizeSelector。所以如果不執行resolveInstanceMethod方法決議,會執行forwardingTargetForSelector方法。

2.1 訊息快速轉發

objc的原始碼中可以找到在NSObject.mm檔案中有定義和實現forwardingTargetForSelector的例項方法和類方法。

+ (id)forwardingTargetForSelector:(SEL)sel {
    return nil;
}

- (id)forwardingTargetForSelector:(SEL)sel {
    return nil;
}
複製程式碼

在官方的檔案中有對forwardingTargetForSelector方法的介紹

大概的意思是,forwardingTargetForSelector主要是返回一個不是自身(如果是self會進入死循壞)的物件去處理sel這個當前類無法處理的訊息,其他的情況可以呼叫super方法。如果處理不了,會轉到效率低下的forwardInvocation。在效率方面,forwardingTargetForSelector領先forwardInvocation一個數量級,因此,如果可以的話最好避免使用後者來做訊息轉發。下面在TestObject類中新增多一個TestForwardObject類,並且在TestObject類中實現forwardingTargetForSelector方法。

@interface TestForwardObject : NSObject
@end

@implementation TestForwardObject

-(void)testErrorMthod{
    NSLog(@"TestForwardObject的testErrorMthod方法%p",__func__);
}
@end

//在TestObject類中實現的方法
-(id)forwardingTargetForSelector:(SEL)aSelector{
    NSLog(@"方法名字:%@",NSStringFromSelector(aSelector));
    if(aSelector == @selector(testErrorMthod)){
        return [TestForwardObject alloc];
    }
    return [super forwardingTargetForSelector:aSelector];
}

//=========執行的結果========
LGTest[1308:29082] 方法名字:testErrorMthod
LGTest[1308:29082] TestForwardObject的testErrorMthod方法0x100001eb4

複製程式碼

從中可以看到訊息的轉發到TestForwardObjecttestErrorMthod方法執行了。但是需要注意的是轉發到其他的類執行的方法必須要和被呼叫的方法相同方法簽名的方法(方法名、引數列表、返回值型別都必須一致)。否則的話,還是報錯的。

2.2訊息慢速轉發

如果在訊息轉發的慢速流程中不做處理,此時會執行到訊息轉發的慢速流程中,需要分別執行兩個方法分別是methodSignatureForSelectorforwardInvocation

-(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
    NSLog(@"方法名字:%@",NSStringFromSelector(aSelector));
    if(aSelector == @selector(testErrorMthod)){
        return [NSMethodSignature signatureWithObjCTypes:"v@:"];
    }
    return [super methodSignatureForSelector:aSelector];
}

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

//======執行結果==========
LGTest[1222:21266] 方法名字:testErrorMthod
LGTest[1222:21266] 執行forwardInvocation:-[TestObject forwardInvocation:]
複製程式碼

在這個流程中methodSignatureForSelector是返回的方法的簽名,可以參考 蘋果官方型別編碼。可以發現在forwardInvocation方法中就算不做處理也不會奔潰,因為每個方法其實就是一個事務,不做處理就會失效,在forwardInvocation中做處理的話,可以如下:

- (void)forwardInvocation:(NSInvocation *)anInvocation{
    NSLog(@"執行forwardInvocation:%s",__func__);
    SEL inVocationSeletor = [anInvocation selector];
    if([[TestForwardObject alloc] respondsToSelector:inVocationSeletor]){
        [anInvocation invokeWithTarget:[TestForwardObject alloc]];
    }else{
        [super forwardInvocation:anInvocation];
    }
}

//=====執行結果===========
LGTest[1465:30634] 方法名字:testErrorMthod
LGTest[1465:30634] 執行forwardInvocation:-[TestObject forwardInvocation:]
LGTest[1465:30634] TestForwardObject的testErrorMthod方法-[TestForwardObject testErrorMthod]
複製程式碼

3.最後

OC方法呼叫是通過objc_msgSend先通過cache_t的快速查詢,如果找不到就要進行慢速查詢。如果都查詢不到方法,就會進入方法的決議訊息轉發流程。如果查詢的類有實現resolveInstanceMethodresolveClassMethod方法對需要查詢的方法做處理就完成,否則就進入訊息轉發流程。訊息轉發的流程中先進入訊息快速轉發流程,需要實現forwardingTargetForSelector方法。否則進入訊息慢速轉發流程,需要實現methodSignatureForSelectorforwardInvocation方法。如果都沒有,此時程式只能報錯了。最後附上訊息轉發的流程圖