iOS底層原理總結 – 探尋Runtime本質(三)

xx_cc發表於2019-03-03

方法呼叫的本質

本文我們探尋方法呼叫的本質,首先通過一段程式碼,將方法呼叫程式碼轉為c++程式碼檢視方法呼叫的本質是什麼樣的。
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m

[person test];
//  --------- c++底層程式碼
((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("test"));
複製程式碼

通過上述原始碼可以看出c++底層程式碼中方法呼叫其實都是轉化為 objc_msgSend函式,OC的方法呼叫也叫訊息機制,表示給方法呼叫者傳送訊息。

拿上述程式碼舉例,上述程式碼中實際為給person例項物件傳送一條test訊息。
訊息接受者:person
訊息名稱:test

在方法呼叫的過程中可以分為三個階段。

訊息傳送階段:負責從類及父類的快取列表及方法列表查詢方法。
動態解析階段:如果訊息傳送階段沒有找到方法,則會進入動態解析階段,負責動態的新增方法實現。
訊息轉發階段:如果也沒有實現動態解析方法,則會進行訊息轉發階段,將訊息轉發給可以處理訊息的接受者來處理。

如果訊息轉發也沒有實現,就會報方法找不到的錯誤,無法識別訊息,unrecognzied selector sent to instance

接下來我們通過原始碼探尋訊息傳送者三個階段分別是如何實現的。

訊息傳送

在runtime原始碼中搜尋_objc_msgSend檢視其內部實現,在objc-msg-arm64.s彙編檔案可以知道_objc_msgSend函式的實現

	ENTRY _objc_msgSend
	UNWIND _objc_msgSend, NoFrame
	MESSENGER_START

	cmp	x0, #0			// nil check and tagged pointer check
	b.le	LNilOrTagged		//  (MSB tagged pointer looks negative)
	ldr	x13, [x0]		// x13 = isa
	and	x16, x13, #ISA_MASK	// x16 = class	
LGetIsaDone:
	CacheLookup NORMAL		// calls imp or objc_msgSend_uncached
複製程式碼

上述彙編原始碼中會首先判斷訊息接受者reveiver的值。
如果傳入的訊息接受者為nil則會執行LNilOrTaggedLNilOrTagged內部會執行LReturnZero,而LReturnZero內部則直接return0。

如果傳入的訊息接受者不為nill則執行CacheLookup,內部對方法快取列表進行查詢,如果找到則執行CacheHit,進而呼叫方法。否則執行CheckMissCheckMiss內部呼叫__objc_msgSend_uncached

__objc_msgSend_uncached內會執行MethodTableLookup也就是方法列表查詢,MethodTableLookup內部的核心程式碼__class_lookupMethodAndLoadCache3也就是c語言函式_class_lookupMethodAndLoadCache3

c語言_class_lookupMethodAndLoadCache3函式內部則是對方法查詢的核心原始碼。

首先通過一張圖看一下組合語言中_objc_msgSend的執行流程。

訊息傳送流程

方法查詢的核心函式就是_class_lookupMethodAndLoadCache3函式,接下來重點分析_class_lookupMethodAndLoadCache3函式內的原始碼。

_class_lookupMethodAndLoadCache3 函式

IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls)
{
    return lookUpImpOrForward(cls, sel, obj, 
                              YES/*initialize*/, NO/*cache*/, YES/*resolver*/);
}
複製程式碼

lookUpImpOrForward 函式

IMP lookUpImpOrForward(Class cls, SEL sel, id inst, 
                       bool initialize, bool cache, bool resolver)
{
    // initialize = YES , cache = NO , resolver = YES
    IMP imp = nil;
    bool triedResolver = NO;
    runtimeLock.assertUnlocked();

    // 快取查詢, 因為cache傳入的為NO, 這裡不會進行快取查詢, 因為在組合語言中CacheLookup已經查詢過
    if (cache) {
        imp = cache_getImp(cls, sel);
        if (imp) return imp;
    }

    runtimeLock.read();
    if (!cls->isRealized()) {
        runtimeLock.unlockRead();
        runtimeLock.write();
        realizeClass(cls);
        runtimeLock.unlockWrite();
        runtimeLock.read();
    }
    if (initialize  &&  !cls->isInitialized()) {
        runtimeLock.unlockRead();
        _class_initialize (_class_getNonMetaClass(cls, inst));
        runtimeLock.read();
    }

 retry:    
    runtimeLock.assertReading();

    // 防止動態新增方法,快取會變化,再次查詢快取。
    imp = cache_getImp(cls, sel);
    // 如果查詢到imp, 直接呼叫done, 返回方法地址
    if (imp) goto done;

    // 查詢方法列表, 傳入類物件和方法名
    {
        // 根據sel去類物件裡面查詢方法
        Method meth = getMethodNoSuper_nolock(cls, sel);
        if (meth) {
            // 如果方法存在,則快取方法,
            // 內部呼叫的就是 cache_fill 上文中已經詳細講解過這個方法,這裡不在贅述了。
            log_and_fill_cache(cls, meth->imp, sel, inst, cls);
            // 方法快取之後, 取出imp, 呼叫done返回imp
            imp = meth->imp;
            goto done;
        }
    }

    // 如果類方法列表中沒有找到, 則去父類的快取中或方法列表中查詢方法
    {
        unsigned attempts = unreasonableClassCount();
        // 如果父類快取列表及方法列表均找不到方法,則去父類的父類去查詢。
        for (Class curClass = cls->superclass;
             curClass != nil;
             curClass = curClass->superclass)
        {
            // Halt if there is a cycle in the superclass chain.
            if (--attempts == 0) {
                _objc_fatal("Memory corruption in class list.");
            }
            
            // 查詢父類的快取
            imp = cache_getImp(curClass, sel);
            if (imp) {
                if (imp != (IMP)_objc_msgForward_impcache) {
                    // 在父類中找到方法, 在本類中快取方法, 注意這裡傳入的是cls, 將方法快取在本類快取列表中, 而非父類中
                    log_and_fill_cache(cls, imp, sel, inst, curClass);
                    // 執行done, 返回imp
                    goto done;
                }
                else {
                    // 跳出迴圈, 停止搜尋
                    break;
                }
            }
            
            // 查詢父類的方法列表
            Method meth = getMethodNoSuper_nolock(curClass, sel);
            if (meth) {
                // 同樣拿到方法, 在本類進行快取
                log_and_fill_cache(cls, meth->imp, sel, inst, curClass);
                imp = meth->imp;
                // 執行done, 返回imp
                goto done;
            }
        }
    }
    
    // ---------------- 訊息傳送階段完成 ---------------------

    // ---------------- 進入動態解析階段 ---------------------
    // 上述列表中都沒有找到方法實現, 則嘗試解析方法
    if (resolver  &&  !triedResolver) {
        runtimeLock.unlockRead();
        _class_resolveMethod(cls, sel, inst);
        runtimeLock.read();
        triedResolver = YES;
        goto retry;
    }

    // ---------------- 動態解析階段完成 ---------------------

    // ---------------- 進入訊息轉發階段 ---------------------
    imp = (IMP)_objc_msgForward_impcache;
    cache_fill(cls, sel, imp, inst);

 done:
    runtimeLock.unlockRead();
    // 返回方法地址
    return imp;
}
複製程式碼

getMethodNoSuper_nolock 函式

方法列表中查詢方法

getMethodNoSuper_nolock(Class cls, SEL sel)
{
    runtimeLock.assertLocked();
    assert(cls->isRealized());
    // cls->data() 得到的是 class_rw_t
    // class_rw_t->methods 得到的是methods二維陣列
    for (auto mlists = cls->data()->methods.beginLists(), 
              end = cls->data()->methods.endLists(); 
         mlists != end;
         ++mlists)
    {
         // mlists 為 method_list_t
        method_t *m = search_method_list(*mlists, sel);
        if (m) return m;
    }
    return nil;
}
複製程式碼

上述原始碼中getMethodNoSuper_nolock函式中通過遍歷方法列表拿到method_list_t最終通過search_method_list函式查詢方法

search_method_list函式

static method_t *search_method_list(const method_list_t *mlist, SEL sel)
{
    int methodListIsFixedUp = mlist->isFixedUp();
    int methodListHasExpectedSize = mlist->entsize() == sizeof(method_t);
    // 如果方法列表是有序的,則使用二分法查詢方法,節省時間
    if (__builtin_expect(methodListIsFixedUp && methodListHasExpectedSize, 1)) {
        return findMethodInSortedMethodList(sel, mlist);
    } else {
        // 否則則遍歷列表查詢
        for (auto& meth : *mlist) {
            if (meth.name == sel) return &meth;
        }
    }
    return nil;
}
複製程式碼

findMethodInSortedMethodList函式內二分查詢實現原理

static method_t *findMethodInSortedMethodList(SEL key, const method_list_t *list)
{
    assert(list);

    const method_t * const first = &list->first;
    const method_t *base = first;
    const method_t *probe;
    uintptr_t keyValue = (uintptr_t)key;
    uint32_t count;
    // >>1 表示將變數n的各個二進位制位順序右移1位,最高位補二進位制0。
    // count >>= 1 如果count為偶數則值變為(count / 2)。如果count為奇數則值變為(count-1) / 2 
    for (count = list->count; count != 0; count >>= 1) {
        // probe 指向陣列中間的值
        probe = base + (count >> 1);
        // 取出中間method_t的name,也就是SEL
        uintptr_t probeValue = (uintptr_t)probe->name;
        if (keyValue == probeValue) {
            // 取出 probe
            while (probe > first && keyValue == (uintptr_t)probe[-1].name) {
                probe--;
            }
           // 返回方法
            return (method_t *)probe;
        }
        // 如果keyValue > probeValue 則折半向後查詢
        if (keyValue > probeValue) {
            base = probe + 1;
            count--;
        }
    }
    
    return nil;
}
複製程式碼

至此為止,訊息傳送階段已經完成。
我們通過一站圖來看一下_class_lookupMethodAndLoadCache3函式內部訊息傳送的整個流程

_class_lookupMethodAndLoadCache3內部流程

如果訊息傳送階段沒有找到方法,就會進入動態解析方法階段。

動態解析階段

當本類包括父類cache包括class_rw_t中都找不到方法時,就會進入動態方法解析階段。我們來看一下動態解析階段原始碼。

動態解析的方法

    if (resolver  &&  !triedResolver) {
        runtimeLock.unlockRead();
        _class_resolveMethod(cls, sel, inst);
        runtimeLock.read();
        // 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;
    }
複製程式碼

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

上述程式碼中可以發現,動態解析方法之後,會將triedResolver = YES;那麼下次就不會在進行動態解析階段了,之後會重新執行retry,會重新對方法查詢一遍。也就是說無論我們是否實現動態解析方法,無論動態解析方法是否成功,retry之後都不會在進行動態的解析方法了。

如何動態解析方法

動態解析物件方法時,會呼叫+(BOOL)resolveInstanceMethod:(SEL)sel方法。
動態解析類方法時,會呼叫+(BOOL)resolveClassMethod:(SEL)sel方法。

這裡以例項物件為例通過程式碼來看一下動態解析的過程

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

+ (BOOL)resolveInstanceMethod:(SEL)sel
{
    // 動態的新增方法實現
    if (sel == @selector(test)) {
        // 獲取其他方法 指向method_t的指標
        Method otherMethod = class_getInstanceMethod(self, @selector(other));
        
        // 動態新增test方法的實現
        class_addMethod(self, sel, method_getImplementation(otherMethod), method_getTypeEncoding(otherMethod));
        
        // 返回YES表示有動態新增方法
        return YES;
    }
    
    NSLog(@"%s", __func__);
    return [super resolveInstanceMethod:sel];
}

@end
複製程式碼
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Person *person = [[Person alloc] init];
        [person test];
    }
    return 0;
}
// 列印結果
// -[Person other]
複製程式碼

上述程式碼中可以看出,person在呼叫test方法時經過動態解析成功呼叫了other方法。

通過上面對訊息傳送的分析我們知道,當本類和父類cacheclass_rw_t中都找不到方法時,就會進行動態解析的方法,也就是說會自動呼叫類的resolveInstanceMethod:方法進行動態查詢。因此我們可以在resolveInstanceMethod:方法內部使用class_addMethod動態的新增方法實現。

這裡需要注意class_addMethod用來向具有給定名稱和實現的類新增新方法,class_addMethod將新增一個方法實現的覆蓋,但是不會替換已有的實現。也就是說如果上述程式碼中已經實現了-(void)test方法,則不會再動態新增方法,這點在上述原始碼中也可以體現,因為一旦找到方法實現就直接return imp並呼叫方法了,不會再執行動態解析方法了。

class_addMethod 函式

我們來看一下class_addMethod函式的引數分別代表什麼。

    /** 
     第一個引數: cls:給哪個類新增方法
     第二個引數: SEL name:新增方法的名稱
     第三個引數: IMP imp: 方法的實現,函式入口,函式名可與方法名不同(建議與方法名相同)
     第四個引數: types :方法型別,需要用特定符號,參考API
     */
class_addMethod(__unsafe_unretained Class cls, SEL name, IMP imp, const char *types)
複製程式碼

上述引數上文中已經詳細講解過,這裡不再贅述。

需要注意的是我們在上述程式碼中通過class_getInstanceMethod獲取Method的方法

// 獲取其他方法 指向method_t的指標
Method otherMethod = class_getInstanceMethod(self, @selector(other));
複製程式碼

其實Method是objc_method型別結構體,可以理解為其內部結構同method_t結構體相同,上文中提到過method_t是代表方法的結構體,其內部包含SEL、type、IMP,我們通過自定義method_t結構體,將objc_method強轉為method_t檢視方法是否能夠動態新增成功。

struct method_t {
    SEL sel;
    char *types;
    IMP imp;
};

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

+ (BOOL)resolveInstanceMethod:(SEL)sel
{
    // 動態的新增方法實現
    if (sel == @selector(test)) {
        // Method強轉為method_t
        struct method_t *method = (struct method_t *)class_getInstanceMethod(self, @selector(other));
        
        NSLog(@"%s,%p,%s",method->sel,method->imp,method->types);
        
        // 動態新增test方法的實現
        class_addMethod(self, sel, method->imp, method->types);
        
        // 返回YES表示有動態新增方法
        return YES;
    }
    
    NSLog(@"%s", __func__);
    return [super resolveInstanceMethod:sel];
}
複製程式碼

檢視列印內容

動態解析方法[3246:1433553] other,0x100000d00,v16@0:8
動態解析方法[3246:1433553] -[Person other]
複製程式碼

可以看出確實可以列印出相關資訊,那麼我們就可以理解為objc_method內部結構同method_t結構體相同,可以代表類定義中的方法。

另外上述程式碼中我們通過method_getImplementation函式和method_getTypeEncoding函式獲取方法的imptype。當然我們也可以通過自己寫的方式來呼叫,這裡以動態新增有引數的方法為例。

+(BOOL)resolveInstanceMethod:(SEL)sel
{
    if (sel == @selector(eat:)) {
        class_addMethod(self, sel, (IMP)cook, "v@:@");
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}
void cook(id self ,SEL _cmd,id Num)
{
    // 實現內容
    NSLog(@"%@的%@方法動態實現了,引數為%@",self,NSStringFromSelector(_cmd),Num);
}
複製程式碼

上述程式碼中當呼叫eat:方法時,動態新增了cook函式作為其實現並新增id型別的引數。

動態解析類方法

當動態解析類方法的時候,就會呼叫+(BOOL)resolveClassMethod:(SEL)sel函式,而我們知道類方法是儲存在元類物件裡面的,因此cls第一個物件需要傳入元類物件以下程式碼為例

void other(id self, SEL _cmd)
{
    NSLog(@"other - %@ - %@", self, NSStringFromSelector(_cmd));
}

+ (BOOL)resolveClassMethod:(SEL)sel
{
    if (sel == @selector(test)) {
        // 第一個引數是object_getClass(self),傳入元類物件。
        class_addMethod(object_getClass(self), sel, (IMP)other, "v16@0:8");
        return YES;
    }
    return [super resolveClassMethod:sel];
}
複製程式碼

我們在上述原始碼的分析中提到過,無論我們是否實現了動態解析的方法,系統內部都會執行retry對方法再次進行查詢,那麼如果我們實現了動態解析方法,此時就會順利查詢到方法,進而返回imp對方法進行呼叫。如果我們沒有實現動態解析方法。就會進行訊息轉發。

接下來看一下動態解析方法流程圖示

動態解析方法流程圖

訊息轉發

如果我們自己也沒有對方法進行動態的解析,那麼就會進行訊息轉發

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

自己沒有能力處理這個訊息的時候,就會進行訊息轉發階段,會呼叫_objc_msgForward_impcache函式。

通過搜尋可以在彙編中找到__objc_msgForward_impcache函式實現,__objc_msgForward_impcache函式中呼叫__objc_msgForward進而找到__objc_forward_handler

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), sel_getName(sel), self);
}
void *_objc_forward_handler = (void*)objc_defaultForwardHandler;
複製程式碼

我們發現這僅僅是一個錯誤資訊的輸出。
其實訊息轉發機制是不開源的,但是我們可以猜測其中可能拿返回的物件呼叫了objc_msgSend,重走了一遍訊息傳送,動態解析,訊息轉發的過程。最終找到方法進行呼叫。

我們通過程式碼來看一下,首先建立Car類繼承自NSObject,並且Car有一個- (void) driving方法,當Person類例項物件失去了駕車的能力,並且沒有在開車過程中動態的學會駕車,那麼此時就會將開車這條資訊轉發給Car,由Car例項物件來幫助person物件駕車。

#import "Car.h"
@implementation Car
- (void) driving
{
    NSLog(@"car driving");
}
@end

--------------

#import "Person.h"
#import <objc/runtime.h>
#import "Car.h"
@implementation Person
- (id)forwardingTargetForSelector:(SEL)aSelector
{
    // 返回能夠處理訊息的物件
    if (aSelector == @selector(driving)) {
        return [[Car alloc] init];
    }
    return [super forwardingTargetForSelector:aSelector];
}
@end

--------------

#import<Foundation/Foundation.h>
#import "Person.h"
int main(int argc, const char * argv[]) {
    @autoreleasepool {

        Person *person = [[Person alloc] init];
        [person driving];
    }
    return 0;
}

// 列印內容
// 訊息轉發[3452:1639178] car driving
複製程式碼

由上述程式碼可以看出,當本類沒有實現方法,並且沒有動態解析方法,就會呼叫forwardingTargetForSelector函式,進行訊息轉發,我們可以實現forwardingTargetForSelector函式,在其內部將訊息轉發給可以實現此方法的物件。

如果forwardingTargetForSelector函式返回為nil或者沒有實現的話,就會呼叫methodSignatureForSelector方法,用來返回一個方法簽名,這也是我們正確跳轉方法的最後機會。

如果methodSignatureForSelector方法返回正確的方法簽名就會呼叫forwardInvocation方法,forwardInvocation方法內提供一個NSInvocation型別的引數,NSInvocation封裝了一個方法的呼叫,包括方法的呼叫者,方法名,以及方法的引數。在forwardInvocation函式內修改方法呼叫物件即可。

如果methodSignatureForSelector返回的為nil,就會來到doseNotRecognizeSelector:方法內部,程式crash提示無法識別選擇器unrecognized selector sent to instance

我們通過以下程式碼進行驗證

- (id)forwardingTargetForSelector:(SEL)aSelector
{
    // 返回能夠處理訊息的物件
    if (aSelector == @selector(driving)) {
        // 返回nil則會呼叫methodSignatureForSelector方法
        return nil; 
        // return [[Car alloc] init];
    }
    return [super forwardingTargetForSelector:aSelector];
}

// 方法簽名:返回值型別、引數型別
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
    if (aSelector == @selector(driving)) {
       // return [NSMethodSignature signatureWithObjCTypes: "v@:"];
       // return [NSMethodSignature signatureWithObjCTypes: "v16@0:8"];
       // 也可以通過呼叫Car的methodSignatureForSelector方法得到方法簽名,這種方式需要car物件有aSelector方法
        return [[[Car alloc] init] methodSignatureForSelector: aSelector];

    }
    return [super methodSignatureForSelector:aSelector];
}

//NSInvocation 封裝了一個方法呼叫,包括:方法呼叫者,方法,方法的引數
//    anInvocation.target 方法呼叫者
//    anInvocation.selector 方法名
//    [anInvocation getArgument: NULL atIndex: 0]; 獲得引數
- (void)forwardInvocation:(NSInvocation *)anInvocation
{
//   anInvocation中封裝了methodSignatureForSelector函式中返回的方法。
//   此時anInvocation.target 還是person物件,我們需要修改target為可以執行方法的方法呼叫者。
//   anInvocation.target = [[Car alloc] init];
//   [anInvocation invoke];
    [anInvocation invokeWithTarget: [[Car alloc] init]];
}

// 列印內容
// 訊息轉發[5781:2164454] car driving
複製程式碼

上述程式碼中可以發現方法可以正常呼叫。接下來我們來看一下訊息轉發階段的流程圖

訊息轉發階段流程圖

NSInvocation

methodSignatureForSelector方法中返回的方法簽名,在forwardInvocation中被包裝成NSInvocation物件,NSInvocation提供了獲取和修改方法名、引數、返回值等方法,也就是說,在forwardInvocation函式中我們可以對方法進行最後的修改。

同樣上述程式碼,我們為driving方法新增返回值和引數,並在forwardInvocation方法中修改方法的返回值及引數。

#import "Car.h"
@implementation Car
- (int) driving:(int)time
{
    NSLog(@"car driving %d",time);
    return time * 2;
}
@end

#import "Person.h"
#import <objc/runtime.h>
#import "Car.h"

@implementation Person
- (id)forwardingTargetForSelector:(SEL)aSelector
{
    // 返回能夠處理訊息的物件
    if (aSelector == @selector(driving)) {
        return nil;
    }
    return [super forwardingTargetForSelector:aSelector];
}

// 方法簽名:返回值型別、引數型別
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
    if (aSelector == @selector(driving:)) {
         // 新增一個int引數及int返回值type為 i@:i
         return [NSMethodSignature signatureWithObjCTypes: "i@:i"];
    }
    return [super methodSignatureForSelector:aSelector];
}


//NSInvocation 封裝了一個方法呼叫,包括:方法呼叫者,方法,方法的引數
- (void)forwardInvocation:(NSInvocation *)anInvocation
{    
    int time;
    // 獲取方法的引數,方法預設還有self和cmd兩個引數,因此新新增的引數下標為2
    [anInvocation getArgument: &time atIndex: 2];
    NSLog(@"修改前引數的值 = %d",time);
    time = time + 10; // time = 110
    NSLog(@"修改前引數的值 = %d",time);
    // 設定方法的引數 此時將引數設定為110
    [anInvocation setArgument: &time atIndex:2];
    
    // 將tagert設定為Car例項物件
    [anInvocation invokeWithTarget: [[Car alloc] init]];
    
    // 獲取方法的返回值
    int result;
    [anInvocation getReturnValue: &result];
    NSLog(@"獲取方法的返回值 = %d",result); // result = 220,說明引數修改成功
    
    result = 99;
    // 設定方法的返回值 重新將返回值設定為99
    [anInvocation setReturnValue: &result];
    
    // 獲取方法的返回值
    [anInvocation getReturnValue: &result];
    NSLog(@"修改方法的返回值為 = %d",result);    // result = 99
}

#import<Foundation/Foundation.h>
#import "Person.h"
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Person *person = [[Person alloc] init];
        // 傳入100,並列印返回值
        NSLog(@"[person driving: 100] = %d",[person driving: 100]);
    }
    return 0;
}
複製程式碼
訊息轉發[6415:2290423] 修改前引數的值 = 100
訊息轉發[6415:2290423] 修改前引數的值 = 110
訊息轉發[6415:2290423] car driving 110
訊息轉發[6415:2290423] 獲取方法的返回值 = 220
訊息轉發[6415:2290423] 修改方法的返回值為 = 99
訊息轉發[6415:2290423] [person driving: 100] = 99
複製程式碼

從上述列印結果可以看出forwardInvocation方法中可以對方法的引數及返回值進行修改。

並且我們可以發現,在設定tagert為Car例項物件時,就已經對方法進行了呼叫,而在forwardInvocation方法結束之後才輸出返回值。

通過上述驗證我們可以知道只要來到forwardInvocation方法中,我們便對方法呼叫有了絕對的掌控權,可以選擇是否呼叫方法,以及修改方法的引數返回值等等。

類方法的訊息轉發

類方法訊息轉發同物件方法一樣,同樣需要經過訊息傳送,動態方法解析之後才會進行訊息轉發機制。我們知道類方法是儲存在元類物件中的,元類物件本來也是一種特殊的類物件。需要注意的是,類方法的訊息接受者變為類物件。

當類物件進行訊息轉發時,對呼叫相應的+號的forwardingTargetForSelector、methodSignatureForSelector、forwardInvocation方法,需要注意的是+號方法僅僅沒有提示,而不是系統不會對類方法進行訊息轉發。

下面通過一段程式碼檢視類方法的訊息轉發機制。

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        [Person driving];
    }
    return 0;
}

#import "Car.h"
@implementation Car
+ (void) driving;
{
    NSLog(@"car driving");
}
@end

#import "Person.h"
#import <objc/runtime.h>
#import "Car.h"

@implementation Person

+ (id)forwardingTargetForSelector:(SEL)aSelector
{
    // 返回能夠處理訊息的物件
    if (aSelector == @selector(driving)) {
        // 這裡需要返回類物件
        return [Car class]; 
    }
    return [super forwardingTargetForSelector:aSelector];
}
// 如果forwardInvocation函式中返回nil 則執行下列程式碼
// 方法簽名:返回值型別、引數型別
+ (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
    if (aSelector == @selector(driving)) {
        return [NSMethodSignature signatureWithObjCTypes: "v@:"];
    }
    return [super methodSignatureForSelector:aSelector];
}

+ (void)forwardInvocation:(NSInvocation *)anInvocation
{
    [anInvocation invokeWithTarget: [Car class]];
}

// 列印結果
// 訊息轉發[6935:2415131] car driving
複製程式碼

上述程式碼中同樣可以對類物件方法進行訊息轉發。需要注意的是類方法的接受者為類物件。其他同物件方法訊息轉發模式相同。

總結

OC中的方法呼叫其實都是轉成了objc_msgSend函式的呼叫,給receiver(方法呼叫者)傳送了一條訊息(selector方法名)。方法呼叫過程中也就是objc_msgSend底層實現分為三個階段:訊息傳送、動態方法解析、訊息轉發。本文主要對這三個階段相互之間的關係以及流程進行的探索。上文中已經講解的很詳細,這裡不再贅述。

底層原理文章專欄

底層原理文章專欄


文中如果有不對的地方歡迎指出。我是xx_cc,一隻長大很久但還沒有二夠的傢伙。需要視訊一起探討學習的coder可以加我Q:2336684744

相關文章