iOS的消息轉發機制詳解


iOS開發過程中,有一類的錯誤會經常遇到,就是找不到所調用的方法,當然這類問題比較好解決,給當前對象或其父類對象添加該方法即可,使得編譯器在編譯時能正確找到該方法;或者,還有另外的方法,由於Objective-C是一門動態語言,我們也可以在運行期再給類添加該方法,一樣可以解決該問題,而這就涉及了類的消息轉發機制。

本文就主要來介紹一下iOS系統的消息轉發機制,探究一下在調用一個方法時,如果本類中沒有該方法時,對象究竟是如何進行消息轉發的,來避免程序拋出異常。

異常現象

當調用的對象方法不存在,即使經過消息轉發也不存在時,就會拋出下面的異常

 -[Teacher playPiano]: unrecognized selector sent to instance 0x6000000114c0
 *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[Teacher playPiano]: unrecognized selector sent to instance 0x6000000114c0'

解決辦法

針對上述的異常問題,最簡單的方法就是直接在類中添加playPiano方法,或者在其繼承樹中添加該方法,均可以解決該問題,所以這種方法再次不再贅述,下面介紹一下如何利用消息轉發機制解決該問題。

消息轉發是在運行時進行的,大致分為兩個階段:第一階段是先檢查接收者,看是否能通過runtime動態添加一個方法,來處理這個unknown selector的消息;第二階段就是完整的消息轉發機制,首先會先查看有沒有其它對象能夠處理該消息,如果沒有,就把該消息的全部信息封裝到NSInvocation對象中,看那個對象能否處理,如果還無法處理,就查看繼承樹中的類是否能夠處理該消息,如果到NSObject之前都無法處理該消息,那么最后就會調用NSObject類的doesNotRecognizeSelector方法來拋出異常,表明調用的方法不存在。

1.動態方法解析

對象在收到無法處理的消息時,會調用下面的方法,前者是調用類方法時會調用,后者是調用對象方法時會調用

// 類方法專用
+ (BOOL)resolveClassMethod:(SEL)sel
// 對象方法專用
+ (BOOL)resolveInstanceMethod:(SEL)sel

在該方法中,需要給對象所屬類動態的添加一個方法,並返回YES,表明可以處理

+ (BOOL)resolveInstanceMethod:(SEL)sel
{
    NSString *method = NSStringFromSelector(sel);
    if ([@"playPiano" isEqualToString:method]) {
        /**
         添加方法
         
         @param self 調用該方法的對象
         @param sel 選擇子
         @param IMP 新添加的方法,是c語言實現的
         @param 新添加的方法的類型,包含函數的返回值以及參數內容類型,eg:void xxx(NSString *name, int size),類型為:v@i
         */
        class_addMethod(self, sel, (IMP)playPiano, "v");
        return YES;
    }
    return NO;
}

2.備援接受者

經歷了第一步后,如果該消息還是無法處理,那么就會調用下面的方法,查詢是否有其它對象能夠處理該消息

- (id)forwardingTargetForSelector:(SEL)aSelector

在這個方法里,我們需要返回一個能夠處理該消息的對象

- (id)forwardingTargetForSelector:(SEL)aSelector
{
    NSString *seletorString = NSStringFromSelector(aSelector);
    if ([@"playPiano" isEqualToString:seletorString]) {
        Student *s = [[Student alloc] init];
        return s;
    }
    // 繼續轉發
    return [super forwardingTargetForSelector:aSelector];
}

3.完整的消息轉發

經歷了前兩步,還是無法處理消息,那么就會做最后的嘗試,先調用methodSignatureForSelector:獲取方法簽名,然后再調用forwardInvocation:進行處理,這一步的處理可以直接轉發給其它對象,即和第二步的效果等效,但是很少有人這么干,因為消息處理越靠后,就表示處理消息的成本越大,性能的開銷就越大。所以,在這種方式下,會改變消息內容,比如增加參數,改變選擇子等等。

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
- (void)forwardInvocation:(NSInvocation *)anInvocation

下面是改變選擇子的例子,比如我們直接調用的是playPiano方法,最后轉發給了traval:方法,完整實例參考:MsgSendDemo

// 完整的消息轉發
- (void)travel:(NSString*)city
{
    NSLog(@"Teacher travel:%@", city);
}

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
    NSString *method = NSStringFromSelector(aSelector);
    if ([@"playPiano" isEqualToString:method]) {
        
        NSMethodSignature *signature = [NSMethodSignature signatureWithObjCTypes:"v@:@"];
        return signature;
    }
    return nil;
}

- (void)forwardInvocation:(NSInvocation *)anInvocation
{
    SEL sel = @selector(travel:);
    NSMethodSignature *signature = [NSMethodSignature signatureWithObjCTypes:"v@:@"];
    anInvocation = [NSInvocation invocationWithMethodSignature:signature];
    [anInvocation setTarget:self];
    [anInvocation setSelector:@selector(travel:)];
    NSString *city = @"北京";
    // 消息的第一個參數是self,第二個參數是選擇子,所以"北京"是第三個參數
    [anInvocation setArgument:&city atIndex:2];
    
    if ([self respondsToSelector:sel]) {
        [anInvocation invokeWithTarget:self];
        return;
    } else {
        Student *s = [[Student alloc] init];
        if ([s respondsToSelector:sel]) {
            [anInvocation invokeWithTarget:s];
            return;
        }
    }
    
    // 從繼承樹中查找
    [super forwardInvocation:anInvocation];
}

iOS的消息轉發機制給我們提供了更多的選擇,來保證消息的正常傳遞,而了解這些具體的實現方法,則可以讓我們的程序更加的健壯。

參考資料

iOS消息轉發機制詳解

iOS 消息轉發機制(VN的逃生之路)

NSMethodSignature和NSInvocation的用法

使用NSMethodSignature和NSInvocation實現消息轉發


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM