OC底層消息轉發機制

1. 前言

上一篇文章(OC底層方法的本質、查找流程)主要說了方法的查找流程,可是方法最後找到了NSObject都沒有對應的方法實現,就直接崩潰嗎?固然不是的,蘋果還給咱們提供一個消息轉發的機制,下面我們來具體看一下。面試

做爲一個開發者,有一個學習的氛圍跟一個交流圈子特別重要,這是一個個人iOS交流羣:812157648,無論你是小白仍是大牛歡迎入駐 ,分享BAT,阿里面試題、面試經驗,討論技術, 你們一塊兒交流學習成長!markdown

2. 動態方法決議

在上一篇文章介紹的查找IMP的方法中,當找不到的時候,會執行一次_class_resolveMethod方法,具體實現以下(詳細解釋見註釋):app

/***********************************************************************
* _class_resolveMethod
* Call +resolveClassMethod or +resolveInstanceMethod.
* Returns nothing; any result would be potentially out-of-date already.
* Does not check if the method already exists.
**********************************************************************/
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判斷可謂重點。
        通過上面_class_resolveClassMethod方法後,有可能尚未找到一個方法的IMP。
        此時判斷一下,是否有能夠實現的類方法的IMP。
        若是有,該if方法不進。
        若是沒有,則再走一遍_class_resolveInstanceMethod方法,調用NSObject的resolveInstanceMethod方法,爲何這麼作呢,由於根據以前文章介紹的isa走位圖可知,根元類是繼承NSObject的,同時擁有NSObject全部的方法,因此再給一次調用resolveInstanceMethod機會,來處理這個類方法。
        */
        if (!lookUpImpOrNil(cls, sel, inst, 
                            NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) 
        {
            _class_resolveInstanceMethod(cls, sel, inst);
        }
    }
}
複製代碼

上面的方法則是一個統一的調用入口,下面分別看一下。oop

2.1 實例方法動態決議

實例方法動態決議方法以下:學習

/***********************************************************************
* _class_resolveInstanceMethod
* Call +resolveInstanceMethod, looking for a method to be added to class cls.
* cls may be a metaclass or a non-meta class.
* Does not check if the method already exists.
**********************************************************************/
static void _class_resolveInstanceMethod(Class cls, SEL sel, id inst)
{
	/**
	此判斷會去查找當前類以及父類等是否實現了resolveInstanceMethod方法。
	若是沒有實現,則直接return,由於if下面的邏輯便是向該方法發送消息,蘋果不可能讓本身的程序崩潰的。
	其實NSObject類已經實現了這個方法,具體見下面,那麼老祖宗都實現了,這個判斷是否多餘呢?若是你寫的類不繼承NSObject呢,是否是就沒有默認實現了呢。
	若是有實現,則繼續走下面的邏輯。
	*/
    if (! lookUpImpOrNil(cls->ISA(), SEL_resolveInstanceMethod, cls, 
                         NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) 
    {
        // Resolver not implemented.
        return;
    }
	// 建立消息,給類發送resolveInstanceMethod消息,此時本類的resolveInstanceMethod方法會被調用,若是沒實現,那麼調用父類的該方法。
    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) {
        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));
        }
    }
}
複製代碼

下面看一下NSObject類的resolveInstanceMethod方法實現:spa

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    return NO;
}
複製代碼

那麼當咱們實現的這個方法被調用了,均可以作什麼呢?來看下面一段代碼:.net

@interface GYMPerson : NSObject
- (void)playGame;
- (void)sleep;
@end

@implementation GYMPerson
- (void)sleep {
    NSLog(@"%s 調用了", __func__);
}
+ (BOOL)resolveInstanceMethod:(SEL)sel {
    return NO;
}
@end
複製代碼

上面有一個GYMPerson類,有兩個實例方法,其中playGame只定義,沒有具體實現, sleep既定義又實現,而後我們開始調用playGame方法。code

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        GYMPerson *person = [GYMPerson alloc];
        [person playGame];
    }
    return 0;
}
複製代碼

當調用playGame方法後,程序直接掛掉了,報錯以下:orm

*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[GYMPerson playGame]: unrecognized selector sent to instance 0x101843700'
複製代碼

緣由在於咱們沒有playGame方法的具體實現,雖然類中複寫了resolveInstanceMethod方法,可是在方法裏沒有作任何操做。對象

下面將resolveInstanceMethod方法裏面加點代碼:

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    if (sel == @selector(playGame)) {
        Method method = class_getInstanceMethod(self, @selector(sleep));
        const char *types = method_getTypeEncoding(method);
        IMP imp = class_getMethodImplementation(self, @selector(sleep));
        bool isAdded = class_addMethod(self, sel, imp, types);
        return isAdded;
    }
    return [super resolveInstanceMethod:sel];
}
複製代碼

而後再運行程序,便會獲得:

GYMDemo[21368:7583751] -[GYMPerson sleep] 調用了
複製代碼

此時sleep方法調用了,程序沒有崩潰,由於在resolveInstanceMethod方法裏面,咱們將sleep方法的IMP綁定給了playGame方法,因此執行playGame方法,就是執行sleep方法。

以上就是實例方法的動態決議,下面再看一下類方法的動態決議。

2.2 類方法動態決議

類方法動態決議方法以下:

/***********************************************************************
* _class_resolveClassMethod
* Call +resolveClassMethod, looking for a method to be added to class cls.
* cls should be a metaclass.
* Does not check if the method already exists.
**********************************************************************/
//cls: 元類, sel: 方法, inst: 類
static void _class_resolveClassMethod(Class cls, SEL sel, id inst)
{
    assert(cls->isMetaClass());
	/**
	判斷元類中是否實現了resolveClassMethod方法。
	若是元類沒有實現,找根元類,根元類沒有,則找NSObject。
	*/
    if (! lookUpImpOrNil(cls, SEL_resolveClassMethod, inst, 
                         NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) 
    {
        // Resolver not implemented.
        return;
    }
	// 建立消息,給類發送resolveClassMethod消息,此時本類的resolveClassMethod方法會被調用,若是沒實現,那麼調用父類的該方法。
    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
    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));
        }
    }
}
複製代碼

下面看一下NSObject類的resolveClassMethod方法實現:

+ (BOOL)resolveClassMethod:(SEL)sel {
    return NO;
}
複製代碼

那麼當咱們實現的這個方法被調用了,均可以作什麼呢?來看下面一段代碼:

@interface GYMPerson : NSObject
+ (void)loveGirl;
+ (void)loveLife;
@end

@implementation GYMPerson
+ (void)loveLife {
    NSLog(@"%s 調用了", __func__);
}
+ (BOOL)resolveClassMethod:(SEL)sel {
    return [super resolveClassMethod:sel];
}
@end
複製代碼

上面有一個GYMPerson類,有兩個類方法,其中loveGirl只定義,沒有具體實現, loveLife既定義又實現,而後我們開始調用loveGirl方法。

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        [GYMPerson loveGirl];
    }
    return 0;
}
複製代碼

當調用loveGirl方法後,程序直接掛掉了,報錯以下:

*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '+[GYMPerson loveGirl]: unrecognized selector sent to class 0x100002750'
複製代碼

緣由在於咱們沒有loveGirl方法的具體實現,雖然類中複寫了resolveClassMethod方法,可是在方法裏沒有作任何操做。

下面將resolveClassMethod方法裏面加點代碼:

+ (BOOL)resolveClassMethod:(SEL)sel {
    if (sel == @selector(loveGirl)) {
        Method method = class_getClassMethod(objc_getMetaClass("GYMPerson"), @selector(loveLife));
        const char *types = method_getTypeEncoding(method);
        IMP imp = class_getMethodImplementation(objc_getMetaClass("GYMPerson"), @selector(loveLife));
        bool isAdded = class_addMethod(objc_getMetaClass("GYMPerson"), sel, imp, types);
        return isAdded;
    }
    return [super resolveClassMethod:sel];
}
複製代碼

而後再運行程序,便會獲得:

GYMDemo[16988:9071636] +[GYMPerson loveLife] 調用了
複製代碼

此時loveLife方法調用了,程序沒有崩潰,由於在resolveClassMethod方法裏面,咱們將loveLife方法的IMP綁定給了loveGirl方法,因此執行loveGirl方法,就是執行loveLife方法。

3. 消息轉發

若是咱們在代碼裏面沒有實現對應的動態決議方法,那麼程序在崩潰以前,蘋果還給咱們提供了一次轉發的機會,分爲快速轉發和慢速轉發,具體以下。

3.1 快速轉發流程

所謂的快速轉發流程就是指定給別人作。

實例方法快速轉發以下:

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

這個方法返回一個對象類型,意思就是當前對象解決不了的話,那麼可能別的類型的對象有,能解決。 好比有這麼一個類GYMAthlete:

@interface GYMAthlete : NSObject
- (void)playGame;
@end

@implementation GYMAthlete
- (void)playGame {
    NSLog(@"%s", __func__);
}運行程序[person playGame];後沒有崩潰,而是調用了GYMAthlete類的playGame方法。
@end
複製代碼

GYMPerson類裏面沒有實現playGame方法,可是GYMAthlete類裏面卻有同名的方法,且實現了。

此時咱們修改forwardingTargetForSelector方法:

- (id)forwardingTargetForSelector:(SEL)sel {
    if (sel == @selector(playGame)) {
        return [GYMAthlete alloc];
    }
    return nil;
}
複製代碼

運行程序[person playGame];後沒有崩潰,而是調用了GYMAthlete類的playGame方法。

+ (id)forwardingTargetForSelector:(SEL)sel {
    if (sel == @selector(loveGirl)) {
        return [GYMAthlete class];
    }
    return nil;
}
複製代碼

運行程序[GYMPerson loveGirl];後沒有崩潰,而是調用了GYMAthlete類的loveGirl方法。

GYMDemo[18597:9145003] +[GYMAthlete loveGirl]
複製代碼

以上就是快速轉發流程,本身作不了的事情指定給別人作。

3.2 慢速轉發流程

慢速轉發流程則是我把方法拋出去,誰能解決誰就幫我解決了,沒人解決也不會崩潰了,由於我已經扔出去了。 與快速轉發不一樣的是,慢速轉發不指定誰去處理,而是扔出去,那麼仍出去的是什麼呢? 方法簽名 下面看一下簽名的方法:

// 實例方法簽名
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
    return nil;
}
// 類方法方法簽名
+ (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
    return nil;
}
複製代碼

只要一個簽名方法仍是沒用呢,它還有一個好搭檔,以下:

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

+ (void)forwardInvocation:(NSInvocation *)invocation {
    NSLog(@"%s", __func__);
}
複製代碼

只要一個簽名方法仍是沒用呢,它還有一個好搭檔,以下:

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

+ (void)forwardInvocation:(NSInvocation *)invocation {
    NSLog(@"%s", __func__);
}
複製代碼

能夠通俗的說,先把方法簽名,而後扔到invocation裏面等待處理,有人處理則好,沒人處理也不會崩潰。

下面試驗一下:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        GYMPerson *person = [GYMPerson alloc];
        [person playGame]; 		// 沒有具體實現
        [GYMPerson loveGirl];	// 沒有具體實現
    }
    return 0;
}

// 運行playGame方法時,先進入到這裏簽名。
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
    if (sel == @selector(playGame)) {
        NSMethodSignature *signature = [NSMethodSignature signatureWithObjCTypes:"v@:"];
        return signature;
    }
    return nil;
}
// 運行loveGirl方法時,先進入到這裏簽名。
+ (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
    if (sel == @selector(loveGirl)) {
        NSMethodSignature *signature = [NSMethodSignature signatureWithObjCTypes:"v@:"];
        return signature;
    }
    return nil;
}

// playGame簽名後,將進入forwardInvocation這個方法等待處理。
- (void)forwardInvocation:(NSInvocation *)anInvocation {
    SEL sel = [anInvocation selector];
    if (sel == @selector(playGame)) {
    	// 這裏分別判斷了GYMAthlete和GYMDeveloper的實例對象能不能處理,誰能處理交給誰處理。
        if ([[GYMAthlete alloc] respondsToSelector:sel]) {
            [anInvocation invokeWithTarget:[GYMAthlete alloc]];
        }else if ([[GYMDeveloper alloc] respondsToSelector:sel]) {
            [anInvocation invokeWithTarget:[GYMDeveloper alloc]];
        }
    }
}
// loveGirl簽名後,將進入forwardInvocation這個方法等待處理。
+ (void)forwardInvocation:(NSInvocation *)invocation {
    SEL sel = [invocation selector];
    if (sel == @selector(loveGirl)) {
    // 這裏分別判斷了GYMAthlete和GYMDeveloper類能不能處理,誰能處理交給誰處理。
        if ([[GYMAthlete class] respondsToSelector:sel]) {
            [invocation invokeWithTarget:[GYMAthlete class]];
        }else if ([[GYMDeveloper class] respondsToSelector:sel]) {
            [invocation invokeWithTarget:[GYMDeveloper class]];
        }
    }
}
複製代碼

結果以下:

2020-10-30 22:27:53.729149+0800 GYMDemo[74259:10203752] -[GYMAthlete playGame]
2020-10-30 22:27:53.729812+0800 GYMDemo[74259:10203752] +[GYMAthlete loveGirl]
複製代碼

4. 總結

本篇文章主要講了消息的轉發機制,當消息查找失敗時就會進入轉發階段,轉發分爲三個階段:

第一階段: 動態決議_class_resolveMethod,指定實現某個方法。

第二階段: forwardingTargetForSelector: 指定某個對象或者類去實現同名的方法。

第三階段: methodSignatureForSelector forwardInvocation組合階段:將方法簽名扔出去,而後再forwardInvocation方法中,誰想處理就處理。

以上三個階段是逐一順序實現的,若是某個階段實現了,那麼後續階段再也不調用了。

以上內容出自blog.csdn.net/guoyongming…的博客,轉載請註明來源。

原文做者:Daniel_Coder

原文地址:blog.csdn.net/guoyongming…

相關文章
相關標籤/搜索