解釋一git
上一篇文章我們提到了Runtime的消息傳遞機制,主要圍繞三個C語言API來展開進行的。這篇文章我將從另外三個方法來描述Runtime中另外一個特性:消息轉發機制。github
當向某個對象發送一條消息時,若該對象的方法列表以及它相應繼承鏈上的方法列表都沒法找到以該消息選擇子做爲key的方法實現時,則會觸發消息轉發機制。緩存
+ (BOOL)resolveInstanceMethod:(SEL)sel;app
首先,當接受到未能識別的選擇子時,運行時系統會調用該函數用以給對象一次機會來添加相應的方法實現,若是用戶在該函數中動態添加了相應方法的實現,則跳轉到方法的實現部分,並將該實現存入緩存中,以供下次調用。函數
- (id)forwardingTargetForSelector:(SEL)aSelector;3d
若是運行時在消息轉發的第一步中未找到所調用方法的實現,那麼當前接收者還有第二次機會進行未知選擇子的處理。這時運行期系統會調用上述方法,並將未知選擇子做爲參數傳入,該方法能夠返回一個能處理該選擇子的對象,運行時系統會根據返回的對象進行查找,若找到則跳轉到相應方法的實現,則消息轉發結束。對象
- (void)forwardInvocation:(NSInvocation *)anInvocation;blog
當運行時系統檢測到第二步中用戶未返回能處理相應選擇子的對象時,那麼來到這一步就要啓動完整的消息轉發機制了。該方法能夠改變消息調用目標,運行時系統根據所改變的調用目標,向調用目標方法列表中查詢對應方法的實現並實現跳轉,這種方式和第二步的操做很是類似。固然你也能夠修改方法的選擇子,亦或者向所調用方法中追加一個參數等來跳轉到相關方法的實現。繼承
最後,若是消息轉發的第三步還未能處理該未知選擇子的話,那麼最終會調用NSObject類的以下方法用以異常的拋出,代表該選擇子最終未能處理。ip
- (void)doesNotRecognizeSelector:(SEL)aSelector;
下面附上完整的消息轉發流程圖:
好了,看了那麼多的理論知識,相比你們也已經累了,那咱們用一個實例來具體說明Runtime的消息轉發機制吧。我是傳送門~~~
首先新建一個工程,並在工程中添加Cat、Dog and Chicken三個類,並在每一個類的.h文件中聲明jump方法,在Cat.m文件中聲明消息轉發的第一步方法:resolveInstanceMethod: ,在該方法中動態添加jump方法的實現。
注:在第一步中動態添加方法實現用到了Runtime中的class_addMethod方法,該方法用以向該類的實例對象中添加相應的方法實現。
而後在main.m文件中調用Cat實例的jump方法,就會看到在控制檯打印出以下結果:
而後在Dog.m文件中驗證消息轉發第二步過程,爲了讓運行時系統可以運行到forwardingTargetForSelector:方法,咱們先在resolveInstanceMethod:中返回NO,代碼以下:
而後按照以前的樣子,在main.m文件中讓Dog也jump起來,運行以後打印結果以下:
最後咱們來驗證消息轉發第三步驟的過程。
在最後的Chicken.m文件中咱們讓前兩步的方法分別返回NO和nil值,用以快速觸發消息轉發機制中的完整消息轉發機制。在驗證這一步中咱們注意到,在調用forwardInvocation:方法以前咱們須要實現methodSignatureForSelector:方法,並將相應選擇子的描述返回。
這裏我用了改變調用目標這種方式進行消息轉發機制,至於改換選擇子,讀者能夠自行嘗試運行哈,具體我已在項目代碼中寫明,最終調用Chicken實例的jump方法,其打印結果以下:
明明分別調用了三個動物的jump,最後在控制檯只看到了Cat一直在jump。。。
解釋二
咱們要經過一個小例子來簡單、通俗的理解一下什麼是消息轉發以及如何消息轉發,但願看完這篇文章時你們會完全的明白OC的消息。
首先,你須要知道這兩個概念:
OC中調用方法就是向對象發送消息。
好比 :
[person run];
這實際上這是在給person這個對象發送run這個消息。
那麼問題來了,當run這個方法只有定義沒有實現會怎麼樣呢?
就是經典的報錯
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[Person run]: unrecognized selector sent to instance
ok,前提已經說完了,咱們就從找這個錯誤緣由講起。
首先,該方法在調用時,系統會查看這個對象可否接收這個消息(查看這個類有沒有這個方法,或者有沒有實現這個方法。),若是不能而且只在不能的狀況下,就會調用下面這幾個方法,給你「補救」的機會,你能夠先理解爲幾套防止程序crash的備選方案,咱們就是利用這幾個方案進行消息轉發,注意一點,前一套方案實現後一套方法就不會執行。若是這幾套方案你都沒有作處理,那麼程序就會報錯crash。
打個比方:比賽足球時,腳下有球的那名球員,若是他的位置不利於射門或者他的球即將被對方球員搶斷,這時最好是把球傳出去,這裏的球就至關於消息。
方案一:
+ (BOOL)resolveInstanceMethod:(SEL)sel
+ (BOOL)resolveClassMethod:(SEL)sel
方案二:
- (id)forwardingTargetForSelector:(SEL)aSelector
方案三:
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector;
- (void)forwardInvocation:(NSInvocation *)anInvocation;
到目前爲止你們已經知道什麼是消息轉發了。下面就說一下這幾套方案是怎樣調用的。
首先,系統會調用resolveInstanceMethod(固然,若是這個方法是一個類方法,就會調用resolveClassMethod)讓你本身爲這個方法增長實現。
我們來看一個例子:
首先,建立了一個Person類的對象p,而後調用p的run方法,注意,這個run方法是沒有寫實現的。
進入Person類的.m文件,我實現了resolveInstanceMethod這個方法爲個人Person類動態增長了一個run方法的實現。(什麼是動態增長?其實就是在程序運行的時候給某類的某個方法增長實現。具體實現內容就爲上面的void run 這個c函數。)
當外部調用[p run]時,因爲咱們沒有實現run對應的方法,那麼系統會調用resolveInstanceMethod讓你去作一些其餘操做。(固然,你也能夠不作操做,只是在這個例子中,我爲run方法動態增長了實現。)
繼續運行,程序走到了咱們C函數的部分,這樣程序沒有了崩潰。
下面講一下第二套方法,forwardingTargetForSelector,這個方法返回你須要轉發消息的對象。
咱們接着這個例子來說,爲了便於演示消息轉發,咱們新建了一個汽車類Car,而且實現了Car的run方法。
如今我不去對方案一的resolveInstanceMethod作任何處理,直接調用父類方法。能夠看到,系統已經來到了forwardingTargetForSelector方法,咱們如今返回一個Car類的實例對象。
繼續運行,程序就來到了Car類的run方法,這樣,咱們就實現了消息轉發。
繼續咱們的例子。若是咱們不實現forwardingTargetForSelector,系統就會調用方案三的兩個方法methodSignatureForSelector和forwardInvocation
methodSignatureForSelector用來生成方法簽名,這個簽名就是給forwardInvocation中的參數NSInvocation調用的。
開頭咱們要找的錯誤unrecognized selector sent to instance緣由,原來就是由於methodSignatureForSelector這個方法中,因爲沒有找到run對應的實現方法,因此返回了一個空的方法簽名,最終致使程序報錯崩潰。
因此咱們須要作的是本身新建方法簽名,再在forwardInvocation中用你要轉發的那個對象調用這個對應的簽名,這樣也實現了消息轉發。
關於生成簽名的類型"v@:"解釋一下。每個方法會默認隱藏兩個參數,self、_cmd,self表明方法調用者,_cmd表明這個方法的SEL,簽名類型就是用來描述這個方法的返回值、參數的,v表明返回值爲void,@表示self,:表示_cmd。
如今咱們回到最初,咱們調用的是Person類的run方法,最終方法被Car類的對象來接受。這就是OC的消息轉發機制。