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的消息轉發機制給我們提供了更多的選擇,來保證消息的正常傳遞,而了解這些具體的實現方法,則可以讓我們的程序更加的健壯。
參考資料
