iOS消息轉發機制及避免崩潰的解決方案

最近研究了一下iOS的消息轉發機制,特此做記錄,用一個真實的例子讓自己理解的更深刻。這個例子會列舉方法沒有實現而導致崩潰的避免措施,。息的轉發分為三步,通過這個例子看一下在每一步的轉發中如何避免因為方法沒有實現而導致的程序崩潰。

先說一下消息轉發的整個流程吧,我們知道調用對象的某一個方法的時候其實就是再給這個對象發消息,來調用他的方法,假如我們有一個Dog類,我們調用他的testFun方法,調用如下:

Dog *testDog = [Dog new];
 [testDog performSelector:@selector(testFun)];

這個時候消息的轉發流程如下:

第一次轉發:方法解析
Dog嘗試自己解析這個方法,此時Dog類的
+(BOOL)resolveClassMethod:(SEL)sel (實例方法)
或者
+(BOOL)resolveInstanceMethod:(SEL)sel (類方法)
會執行,如果Dog類實現了testFun這個方法,那么會被調用,消息轉發也就結束。

第二次轉發:快速轉發
如果第一次轉發方法的實現沒有被找到,那么會調用這個類的
-(id)forwardingTargetForSelector:(SEL)aSelector
開始進行消息快速轉發,如果我們可以找到一個對象來實現調用的testFun方法,可以在這個方法里將這個對象返回,然后該對象就會執行我們調用的testFun方法

第三次轉發:慢速轉發
如果第二次轉發也沒有找到可以處理testFun方法的對象的話,那么Dog類的
-(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
方法就會被調用,這個方法會返回一個testFun的方法簽名,如果返回了testFun的方法簽名的話,Dog類的
-(void)forwardInvocation:(NSInvocation *)anInvocation
方法會被調用,在這個方法里處理我們調用的方法,如果這個方法里也處理不了的話,就會執行
doesNotRecognizeSelector方法,引起一個unrecognized selector sent to instance異常崩潰。

我們如何利用IOS的消息轉發機制來避免對象沒有實現某個方法的時候出現崩潰的情況呢?我們來看一下如果在每一步里進行避免:

在第一次轉發里攔截崩潰 — 消息處理

假如Dog類沒有實現testFun方法,那么我們需要在第一次消息轉發調用
+(BOOL)resolveClassMethod:(SEL)sel (實例方法)
或者
+(BOOL)resolveInstanceMethod:(SEL)sel (類方法)
這兩個方法的時候在里面加上方法的實現,代碼如下(以+(BOOL)resolveClassMethod:(SEL)sel 為例 ):

#pragma mark - 第一步 方法解析
void testFun(){
    NSLog(@"Dog test Fun");
}

+(BOOL)resolveInstanceMethod:(SEL)sel{
    if ([super resolveInstanceMethod:sel]) {
        return YES;
    }else{
        class_addMethod(self, sel, (IMP)testFun, "v@:");
        return YES;
    }
}

我們需要自己實現一個testFun方法,然后在resolveInstanceMethod方法里首先判斷自己的父類是否可以解析該方法,如果父類解析不了的話我們就利用iOS的runtime機制動態的將testFun添加到自己的類里,前提是我們自己有一個testFun的方法實現(c語音),程序運行后正常輸出:
Dog test Fun

這就是通過消息轉發機制的第一步避免崩潰的操作。

在第二次轉發里攔截崩潰 — 快速轉發

快速轉發階段調用Dog類的
-(id)forwardingTargetForSelector:(SEL)aSelector
方法,如果我們自己實現不了testFun方法的話,我們需要創建一個實現了這個方法的對象然后拋出去來作為方法的處理對象,假如我們有一個SubDog類,并且這個SubDog實現了testFun方法,此時的處理方法如下:

#pragma mark - 第二步快速轉發
-(id)forwardingTargetForSelector:(SEL)aSelector{
    if ([NSStringFromSelector(aSelector) isEqual:@"testFun"]) {
        return [SubDog new];
    }
    return [super forwardingTargetForSelector:aSelector];
}

其實很簡單,我們只需要創建一個SubDog的實例拋出去就可以了,該實例會處理testFun方法。
運行后,SubDog里的方法會執行,程序正常運行。
這就是通過消息轉發機制的第二步避免崩潰的操作。

在第三次轉發里攔截崩潰 — 快速轉發

此時進入第三次轉發階段了,在這里會調用
-(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
返回放方法簽名,如果返回了的話會調用
-(void)forwardInvocation:(NSInvocation *)anInvocation
方法尋找處理方法簽名的對象,我們先看一個比較普通的處理措施,其實跟第二步一樣,也是返回一個可以處理testFun方法的對象:

#pragma mark - 第三步 慢速轉發
-(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
    if ([super methodSignatureForSelector:aSelector] == nil) {
    //創建一個方法簽名然后返回
        NSMethodSignature *signature = [NSMethodSignature signatureWithObjCTypes:"v@:"];
        return signature;
    }
    return [super methodSignatureForSelector:aSelector];
}

-(void)forwardInvocation:(NSInvocation *)anInvocation{
    //創建一個對象來處理testFun方法
    SubDog *subDog = [SubDog new];
    SEL sel = anInvocation.selector;
    if ([subDog respondsToSelector:sel]) {
        [anInvocation invokeWithTarget:subDog];
    }else{
    //如果處理不了的話,調用doesNotRecognizeSelector方法返回崩潰
        [self doesNotRecognizeSelector:sel];
    }
}

運行后,SubDog里的方法會執行,程序正常運行。
這就是通過消息轉發機制的第三步避免崩潰的操作。

在第三次轉發的時候可以做的更多的東西

我們可以看到在第三次轉發的時候我們可以拿到anInvocation參數,這個參數里其實包含了方法調用的所有信息,我們其實可以對這個anInvocation進行一些修改,增加或者減少參數,然后找到適當的對象來進行處理,或者我們自己實現方法自己處理。
舉例如下:

#pragma mark - 第三步 慢速轉發
-(void)showMessage:(NSString*)message{
    NSLog(@"message = %@",message);
}

-(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
    if ([super methodSignatureForSelector:aSelector] == nil) {
        NSMethodSignature *signature = [NSMethodSignature signatureWithObjCTypes:"v@:"];
        return signature;
    }
    return [super methodSignatureForSelector:aSelector];
}

-(void)forwardInvocation:(NSInvocation *)anInvocation{
    SEL sel = @selector(showMessage:);
    NSMethodSignature *signature = [NSMethodSignature signatureWithObjCTypes:"v@:@"];
    anInvocation = [NSInvocation invocationWithMethodSignature:signature];
    [anInvocation setTarget:self];
    [anInvocation setSelector:sel];
    NSString *message = @"在第三步自己實現的方法,改了參數";
    [anInvocation setArgument:&message atIndex:2];
    if ([self respondsToSelector:sel]) {
        [anInvocation invokeWithTarget:self];
    }else{
        [super forwardInvocation:anInvocation];
    }
}

本來我們調用的方法只是一個testFun,不包含任何參數的,但是通過在-(void)forwardInvocation:(NSInvocation *)anInvocation方法里我們對anInvocation進行了改動,使其變成了一個對帶有一個NSString類型參數的調用,然后我們實現了這個方法,并且把自己設置為方法的實現對象,程序運行后輸出:
message = 在第三步自己實現的方法,改了參數

這樣就實現了在第三次調用里進行了偷梁換柱的操作,代碼很簡單,但很有意思,自己操作試試吧。

消息轉發機制的其他用途:
1.JSPatch --iOS動態化更新方案利用的就是消息轉發機制的第三次轉發
2.實現多重代理
3.間接實現多繼承

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。