若想令類能理解某條消息,我們必須實現對應的方法才行,但是在編譯期向類發送了其無法理解的消息並不會報錯,因為在運行期可以繼續向類中添加方法,所以編譯器在編譯時還不確定類中到底會不會有某個方法的實現。當對象接收到無法解讀的消息后,就會啟動“消息轉發”機制,程序可由此過程告訴對象應該如何處理未知消息。
消息轉發分為兩大階段,第一階段先征詢接收者所屬的類,看其是否能動態添加方法,以處理當前這個“未知的選擇子”,這叫做“動態方法解析”。如果運行期系統已經把第一階段執行完了,那么接收者自己就無法再以動態新增方法的手段來響應包含該選擇子的消息了。此時運行期系統會請求接收者以其他手段來處理與消息相關的方法調用。這又細分為兩小步。首先請接收者看看有沒有其他對象可以處理這條消息,若有,則運行期會把消息轉給那個對象,於是消息轉發過程結束,一切如常。若沒有“備援的接收者”,則啟動完整的消息轉發機制,運行期會把與消息有關的全部細節都封裝到NSInvocation對象中,再給接收者最后一次機會,令其設法解決當前還未處理的這條消息。
1.動態方法解析
對象在無法解讀消息會首先調用所屬類的下列類方法:
+ (BOOL) resolveInstanceMethod:(SEL)selector
參數為那個未知的選擇子,返回值表示這個類能否新增一個實例方法處理此選擇子。假如尚未實現的方法不是實例方法而是類方法則運行期會調用另一個方法:+ (BOOL) resolveClassMethod:(SEL)selector。使用這種方法的前提是:相關方法的實現代碼已經寫好,只等着運行的時候動態插入到類里面就可以了。此方案常用來實現@dynamic屬性。
2.備援接收者
當前接收者還有第二次機會能處理未知的選擇子,這一步中,運行期會問它:能不能把這條消息轉發給其他接收者來處理。與該步驟對應的處理方法:
- (id) forwardingTargetForSelector:(SEL)selector
方法參數代表未知的選擇子,若當前接收者能找到備援對象,則將其返回,若找不到就返回nil。通過此方案我們可以用“組合”來模擬出“多重繼承”的某些特性(因為OC屬於單繼承,一個字類只能繼承一個基類)。在一個對象內部,可能還有一系列其他對象,該對象可能由此方法將能夠處理某選擇子的相關內部對象返回,這樣的話,在外界看來,好像該對象親自處理了這些消息。
3.完整的消息轉發
如果轉發算法已經到了這一步的話,那么唯一能做的就是啟用完整的消息轉發機制了。首先創建NSInvocation對象,把與尚未處理的那條消息有關的全部細節都封裝與其中。此對象包含選擇子、目標(target)、參數。在出發NSInvocation對象時“消息派發系統”將親自出馬,把消息指派給目標對象。
- (void) forwardInvocation:(NSInvocation *)invocation
這個方法可以實現很簡單:只需改變調用目標,使消息在新目標上得以調用即可,然而這樣實現出來的方法與“備援接收者”方案所實現的方法等效,所以很少有人采用這么簡單的實現方式。比較有用的實現方式為:在出發消息前,現已某種方式改變消息內容,比如追加另外一個參數,或者改換選擇子等等。
實現了此方法若發現某調用操作不應由本類處理,則需調用超類的同名方法。這樣的話繼承體系中的每個類都有機會處理此調用請求,直至NSObject。如果最后調用了NSObject類的方法,那么該方法還會繼續調用“doesNotRecognizeSelector:”以拋出異常,此異常表明選擇子最終未能得道處理。
例子1:
@interface WKAutoDictionary : NSObject @property (nonatomic, copy) NSString *string; @property (nonatomic, strong) NSNumber *number; @property (nonatomic, strong) NSDate *date; @property (nonatomic, strong) id opaqueObject; @end
#import "WKAutoDictionary.h" #import <objc/runtime.h> @interface WKAutoDictionary () @property (nonatomic, strong) NSMutableDictionary *backingStore; @end @implementation WKAutoDictionary @dynamic string, number, date, opaqueObject; - (instancetype)init { if (self = [super init]) { _backingStore = [NSMutableDictionary dictionary]; } return self; } /* * 找不到sel首先會進入 */ + (BOOL)resolveInstanceMethod:(SEL)sel { NSString *selString = NSStringFromSelector(sel); if ([selString hasPrefix:@"set"]) {//說明調用的時set方法 class_addMethod(self, //class_addMethod 可以向類中動態添加方法 sel, (IMP)autoDictionarySetter,//函數指針類型,指向待添加的方法 "v@:@");// 表示待添加方法的“類型編碼” }else { class_addMethod(self, sel, (IMP)autoDictionaryGetter, "@@:"); } return YES; } void autoDictionarySetter (id self, SEL _cmd, id value) { //獲取當前self的字典 WKAutoDictionary *typedSelf = (WKAutoDictionary *)self; NSMutableDictionary *backingStore = typedSelf.backingStore; NSString *selString = NSStringFromSelector(_cmd); NSMutableString *key = [selString mutableCopy]; //刪除set方法的“:” [key deleteCharactersInRange:NSMakeRange(key.length-1, 1)]; //刪除 “set” [key deleteCharactersInRange:NSMakeRange(0, 3)]; //讓第一個字符小寫 NSString *lowercaseFirstChar = [[key substringToIndex:1] lowercaseString]; [key replaceCharactersInRange:NSMakeRange(0, 1) withString:lowercaseFirstChar]; if (value) { [backingStore setObject:value forKey:key]; }else { [backingStore removeObjectForKey:key]; } } id autoDictionaryGetter (id self, SEL _cmd){ //獲取當前self的字典 WKAutoDictionary *typedSelf = (WKAutoDictionary *)self; NSMutableDictionary *backingStore = typedSelf.backingStore; NSString *key = NSStringFromSelector(_cmd); return [backingStore objectForKey:key]; }
例子2:
#import "Developer.h" #import "Finance.h" #import <objc/runtime.h> @implementation Developer - (void)doDeveloper { NSLog(@"Developer doWork!"); } + (BOOL)resolveInstanceMethod:(SEL)sel { /* 如果當前對象調用了一個不存在的方法 Runtime會調用resolveInstanceMethod:來進行動態方法解析 我們需要用class_addMethod函數完成向特定類添加特定方法實現的操作 返回NO,則進入下一步forwardingTargetForSelector: */ /* class_addMethod(self, sel, class_getMethodImplementation(self, sel_registerName("doDeveloper")), "v@:"); return [super resolveInstanceMethod:sel]; */ return NO; } - (id)forwardingTargetForSelector:(SEL)aSelector { /* 在消息轉發機制執行前,Runtime 系統會再給我們一次重定向的機會 通過重載forwardingTargetForSelector:方法來替換消息的接受者為其他對象 返回nil則進步下一步forwardInvocation: */ /* return [[Finance alloc] init]; */ return nil; } -(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{ /* 獲取方法簽名進入下一步,進行消息轉發 */ return [NSMethodSignature signatureWithObjCTypes:"v@:"]; } - (void)forwardInvocation:(NSInvocation *)anInvocation { /* 消息轉發 */ return [anInvocation invokeWithTarget:[[Finance alloc] init]]; }
總結:
1.若對象無法響應某個選擇子,則進入消息轉發流程。
2.通過運行期的動態方法解析功能,我們可以在需要某個方法時再將其加入類中。
3.對象可以把其無法解讀的某些選擇子轉交給其他對象來處理。
4.經過上述兩步后,如果還是沒辦法處理選擇子,那就啟動完整的消息轉發機制。