相信大家對Objective-C的消息傳遞機制並不陌生(如果不熟悉,我後續會再寫一篇關於消息傳遞機制的文章),今天我來講解另外一個重要的問題,就是對象在收到無法解讀的消息之後會發生什麼情況。
若想令類能理解某條消息,我們必須以程序碼實現出對應的方法才行。但是,在編譯器向類發送了其無法解讀的消息並不會報錯,因為在運行期可以繼續向類中添加方法(動態添加),所以編譯器在編譯時還無法確知類中到底會不會有某個方法實現。當對象接收到無法解讀的消息後,就會啟動“消息轉發”(message forwarding)機制,程序員可經由此過程告訴對象應該如何處理未知消息。
你可能早就遇到過經由消息轉發流程所處理的消息了,只是未加留意。如果在控制台中看到下面這種提示信息,那就說明你曾向某個對象發送過一條無法解讀的消息,從而啟動了消息轉發機制,並將次消息轉發給了NSObject得默認實現。
-[__NSCFNumber lowercaseString]:unrecognized selector sent to instance 0x87 *** Terminating app due to uncaught exception 'NSInvalidArgumentException',reason: '-[__NSCFNumber lowercaseString]:unrecognized selector sent to instance ox87'
上面這段異常信息是由NSObject的“doesNotRecognizeSelector:”方法所拋出的,此異常表明:消息接收者的類型是__NSCFNumber,而該接受者無法理解名位lowercaseString的選擇子。本例所列舉的這種情況並不奇怪,因為NSNumber類裡本來就沒有名為lowercaseString的方法。在本例中,消息轉發過程以應用程序崩潰而告終,不過,開發者在編寫自己的類時,可於轉發過程中設置掛鉤,用以執行預定的邏輯,而不使應用程序崩潰。
消息的轉發分為兩大階段。第一階段先征詢接收者,所屬的類,看其是否能動態添加方法,以處理當前這個“未知的選擇子”(unknown selector),這叫做“動態方法解析”(dynamic method resolution)。第二階段涉及“完整的消息轉發機制”。如果運行期系統已經把第一階段執行完了,那麼接收者自己就無法再以動態新增方法的手段來響應包含該選擇子的消息了。此時,運行期系統會請求接受者以其他手段來處理與消息相關的方法調用。這又細分為兩小步。首先,請接受者看看有沒有其他對象處理這條消息。若有,則運行期系統會把消息轉給那個對象,於是消息轉發過程結束,一起如常。若沒有“備援的接收者”,則啟動完整的消息轉發機制,運行期系統會把於消息有關的全部細節都封裝到NSInvocation對象中,再給接收者最後一次機會,令其設法解決當前還未處理的這條消息。
動態方法解析
對象在收到無法解讀的消息後,首先將調用其所屬類的下列類方法:
+ (BOOL)resolveInstanceMethod:(SEL)sel
該方法的參數就是那個未知的選擇子,其返回值為Boolean類型,表示這個類是否能新增一個實例方法用以處理此選擇子。在繼續往下執行轉發機制之前,本類有機會新增一個處理此選擇子的方法。加入尚未實現的方法不是實例方法而是類方法,那麼運行期系統就會調用另外一個方法
+ (BOOL)resolveClassMethod:(SEL)sel
使用這種辦法的前提是:相關方法的實現代碼已經寫好,只等著運行的時候動態插在類裡面就可以了。此方案常用來實現@dynamic屬性。
備援接收者
當前接收者還有第二次機會能處理未知的選擇子,在這一步中,運行期系統會問它:能不能把這條消息轉給其他接收者來處理。與該步驟對應的處理方法如下:
- (id)forwardingTargetForSelector:(SEL)aSelector
方法參數代表未知的選擇子,若當前接收者能找到被援對象,則將其返回,若找不到就返回nil。在一個對象內部,可能還有一系列其他對象,該對象可經由此方法將能夠處理某選擇子的相關內部對象返回,這樣的話,在外界看來,好像是該對象親自處理了這些消息似的。
完整的消息轉發
如果轉發算法已經來到這一波的話,那麼唯一能做的就是啟用完整的消息轉發機制了。首先創建NSInvocation對象,把與尚未處理的那條消息有關的全部細節都封於其中。在觸發NSInvocation對象時,“消息派發系統”將親自出馬,把消息指派給目標對象。
此步驟會調用下列方法來轉發消息:
-(void)forwardInvocation:(NSInvocation *)invocation
這個方法實現很簡單:只需改變調用目標,使消息在新目標上得以調用即可。然而這樣實現出來的方法與“備援接收者”方案實現的方法等效,所以很少有人采用這麼簡單的實現方式。
以完整的例子演示動態方法解析
下面編寫一個類似於“字典”的對象。
在接口文件中隨便定義幾個屬性:
@interface GXZDictionary : NSObject @property (nonatomic,strong) NSString *name; @property (nonatomic,strong) NSNumber *number; @property (nonatomic,strong) NSDate *date;
本例的關鍵在於resolveInstanceMethod:方法的實現代碼:
@dynamic name,number,date;
+ (BOOL)resolveInstanceMethod:(SEL)selector { NSString *selectorString = NSStringFromSelector(selector); if ([selectorString hasPrefix:@"set"]) { class_addMethod(self, selector, (IMP)autoDictionarySetter, "v@:@"); } else { class_addMethod(self, selector, (IMP)autoDictionaryGetter, "@@:"); } return YES; }
然後就是Getter和Setter方法實現了:
getter:
id autoDictionaryGetter(id self,SEL _cmd) { GXZDictionary *typedSelf = (GXZDictionary *)self; NSMutableDictionary *backingStore = typedSelf.backingStore; NSString *key = NSStringFromSelector(_cmd); return [backingStore objectForKey:key]; }
其中的backingStore是實現文件中定義的可變的字典,用於存儲數據。
下面是Setter方法:
void autoDictionarySetter(id self,SEL _cmd,id value) { GXZDictionary *typedSelf = (GXZDictionary *)self; NSMutableDictionary *backingStore = typedSelf.backingStore; NSString *selectorString = NSStringFromSelector(_cmd); NSMutableString *key = [selectorString mutableCopy]; [key deleteCharactersInRange:NSMakeRange(key.length - 1, 1)]; [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]; } }
好了,主要工作已經完成,下面直接使用就可以了,調用很簡單:
GXZDictionary *dict = [GXZDictionary new]; dic.date = [NSDate dateWithTimeIntervalSince1970:475372800]; NSLog(@"%@",dict.date);
好了,是不是很神奇,很簡單,現在可以隨便在接口文件中添加屬性了。
小結
* 若對象無法響應某個選擇子,則進入消息轉發流程。
* 通過運行期的動態方法解析功能,我們可以在需要用到某個方法時再將其加入類中。
* 對象可以把其無法解讀的某些選擇子轉交給其他對象處理。
* 經過上述兩步之後,如果還是沒辦法處理選擇子,那就啟動完整的消息轉發機制。
> 原文鏈接:http://coderperson.com/2015/06/08/iOS-runtime-messageForwarding/