標籤:style blog http io color os ar 使用 for
訊息轉寄
若想令類能理解某條訊息,我們必須以程式碼實現出對應的方法才行。但是,在編譯期向類發送了其無法解讀的訊息並不會報錯,因為在運行期可以繼續向類中添加方法,所以編譯器在編譯時間還無法確知類中到底會不會有某個方法實現。當對象接收到無法解讀的訊息後,就會啟動“訊息轉寄”(message forwarding)機制,程式員可經由此過程告訴對象應該如何處理未知訊息。
你可能早就遇到過經由訊息轉寄流程所處理的訊息了,只是未加留意。如果在控制台中看到下面這種提示資訊,那就說明你曾向某個對象發送過一條其無法解讀的訊息,從而啟動了訊息轉寄機制,並將此訊息轉寄給了NSObject的預設實現。
1 -[__NSCFNumber lowercaseString]: unrecognized selector sent to 2 instance 0x87 3 *** Terminating app due to uncaught exception 4 ‘NSInvalidArgumentException‘, reason: ‘-[__NSCFNumber 5 lowercaseString]: unrecognized selector sent to instance 0x87‘
上面這段異常資訊是由NSObject的“doesNotRecognizeSelector:”方法所拋出的,此異常表明:訊息接收者的類型是_ _NSCFNumber,而該接收者無法理解名為lowercaseString的選擇子。本例所列舉的這種情況並不奇怪,因為NSNumber類裡本來就沒有名為lowercaseString的方法。控制台中看到的那個_ _NSFCNumber是為了實現“無縫橋接”(toll-free bridging,第49條將會詳解此技術)而使用的內部類(internal class),配置NSNumber對象時也會一併建立此對象。在本例中,訊息轉寄過程以應用程式崩潰而告終,不過,開發人員在編寫自己的類時,可於轉寄過程中設定掛鈎,用以執行預定的邏輯,而不使應用程式崩潰。
訊息轉寄分為兩大階段。第一階段先徵詢接收者,所屬的類,看其是否能動態添加方法,以處理當前這個“未知的選擇子”(unknown selector),這叫做“動態方法解析”(dynamic method resolution)。第二階段涉及“完整的訊息轉寄機制”(full forwarding mechanism)。如果運行期系統已經把第一階段執行完了,那麼接收者自己就無法再以動態新增方法的手段來響應包含該選擇子的訊息了。此時,運行期系統會請求接收者以其他手段來處理與訊息相關的方法調用。這又細分為兩小步。首先,請接收者看看有沒有其他對象能處理這條訊息。若有,則運行期系統會把訊息轉給那個對象,於是訊息轉寄過程結束,一切如常。若沒有“備援的接收者”(replacement receiver),則啟動完整的訊息轉寄機制,運行期系統會把與訊息有關的全部細節都封裝到NSInvocation對象中,再給接收者最後一次機會,令其設法解決當前還未處理的這條訊息
動態方法解析
對象在收到無法解讀的訊息後,首先將調用其所屬類的下列類方法:
+ (BOOL)resolveInstanceMethod:(SEL)selector
該方法的參數就是那個未知的選擇子,其傳回值為Boolean類型,表示這個類是否能新增一個執行個體方法用以處理此選擇子。在繼續往下執行轉寄機制之前,本類有機會新增一個處理此選擇子的方法。假如尚未實現的方法不是執行個體方法而是類方法,那麼運行期系統就會調用另外一個方法,該方法與“resolveInstanceMethod:”類似,叫做“resolveClassMethod:”。
使用這種辦法的前提是:相關方法的實現代碼已經寫好,只等著啟動並執行時候動態插在類裡面就可以了。此方案常用來實現@dynamic屬性(參見第6條),比如說,要訪問CoreData架構中NSManagedObjects對象的屬性時就可以這麼做,因為實現這些屬性所需的存取方法在編譯期就能確定。
下列代碼示範了如何用“resolveInstanceMethod:”來實現@dynamic屬性:
1 id autoDictionaryGetter(id self, SEL _cmd); 2 void autoDictionarySetter(id self, SEL _cmd, id value); 3 4 + (BOOL)resolveInstanceMethod:(SEL)selector { 5 NSString *selectorString = NSStringFromSelector(selector); 6 if ( /* selector is from a @dynamic property */ ) { 7 if ([selectorString hasPrefix:@"set"]) { 8 class_addMethod(self, 9 selector, 10 (IMP)autoDictionarySetter, 11 "[email protected]:@"); 12 } else { 13 class_addMethod(self, 14 selector, 15 (IMP)autoDictionaryGetter, 16 "@@:"); 17 } 18 return YES; 19 } 20 return [super resolveInstanceMethod:selector]; 21 }
首先將選擇子化為字串,然後檢測其是否表示設定方法。若首碼為set,則表示設定方法,否則就是擷取方法。不管哪種情況,都會把處理該選擇子的方法加到類裡面,所添加的方法是用純C函數實現的。C函數可能會用代碼來操作相關的資料結構,類之中的屬性資料就存放在那些資料結構裡面。以CoreData為例,這些存取方法也許要和後端資料庫通訊,以便擷取或更新相應的值。
備援接收者
當前接收者還有第二次機會能處理未知的選擇子,在這一步中,運行期系統會問它:能不能把這條訊息轉給其他接收者來處理。與該步驟對應的處理方法如下:
- (id)forwardingTargetForSelector:(SEL)selector
方法參數代表未知的選擇子,若當前接收者能找到備援對象,則將其返回,若找不到,就返回nil。通過此方案,我們可以用“組合”(composition)來類比出“多重繼承”(multiple inheritance)的某些特性。在一個對象內部,可能還有一系列其他對象,該對象可經由此方法將能夠處理某選擇子的相關內部對象返回,這樣的話,在外界看來,好像是該對象親自處理了這些訊息似的。
請注意,我們無法操作經由這一步所轉寄的訊息。若是想在發送給備援接收者之前先修改訊息內容,那就得通過完整的訊息轉寄機制來做了。
完整的訊息轉寄
如果轉寄演算法已經來到這一步的話,那麼唯一能做的就是啟用完整的訊息轉寄機制了。首先建立NSInvocation對象,把與尚未處理的那條訊息有關的全部細節都封於其中。此對象包含選擇子、目標(target)及參數。在觸發NSInvocation對象時,“訊息派發系統”(message-dispatch system)將親自出馬,把訊息指派給目標對象。
此步驟會調用下列方法來轉寄訊息:
- (void)forwardInvocation:(NSInvocation*)invocation
這個方法可以實現得很簡單:只需改變調用目標,使訊息在新目標上得以調用即可。然而這樣實現出來的方法與“備援接收者”方案所實現的方法等效,所以很少有人採用這麼簡單的實現方式。比較有用的實現方式為:在觸發訊息前,先以某種方式改變訊息內容,比如追加另外一個參數,或是改換選擇子,等等。
實現此方法時,若發現某叫用作業不應由本類處理,則需調用超類的同名方法。這樣的話,繼承體系中的每個類都有機會處理此調用請求,直至NSObject。如果最後調用了NSObject類的方法,那麼該方法還會繼而調用“doesNotRecognizeSelector:”以拋出異常,此異常表明選擇子最終未能得到處理。
接收者在每一步中均有機會處理訊息。步驟越往後,處理訊息的代價就越大。最好能在第一步就處理完,這樣的話,運行期系統就可以將此方法緩衝起來了。如果這個類的執行個體稍後還收到同名選擇子,那麼根本無須啟動訊息轉寄流程。若想在第三步裡把訊息轉給備援的接收者,那還不如把轉寄操作提前到第二步。因為第三步只是修改了調用目標,這項改動放在第二步執行會更為簡單,不然的話,還得建立並處理完整的NSInvocation。
ios底層開發訊息機制(四)訊息轉寄