前言:
上面這段異常資訊是由NSObject的“doesNotRecognizeSelector:”方法所丟擲的,此異常表明:訊息接受者的型別是__NSCFNumber,而該接受者無法理解名為lowercaseString的選擇子。本例所列舉的這種情況並不奇怪,因為NSNumber類裡本來就麼有名為lowercaseString的方法。控制檯的中看到的那個__NSCFNumber是為了實現“無縫橋接”而使用的內部類,配置NSNumber物件時也會一併建立此物件。在本例中,訊息轉發過程以應用程式崩潰而告終,不過,開發者在編寫自己的類時,可於轉發過程中設定掛鉤,用以執行預定的邏輯,而不使應用程式崩潰。 訊息轉發分為兩大階段。第一階段先徵詢接收者,所屬的類,看其是否能動態新增方法,以處理當前這個“未知的選擇子”,這叫做“動態方法解析”。第二階段涉及“完整的訊息轉發機制”。如果執行期系統已經把第一階段執行完了,那麼接收者自己就無法再以動態新增方法的手段來響應包含該選擇子的訊息了。此時,執行期系統會請求接收者以其他手段來處理與訊息相關的方法呼叫。這又細分為兩小步。首先,請接收者看看有沒有其他物件能處理這條訊息。若有,則執行期系統會把訊息轉給那個物件,於是訊息轉發過程結束,一切如常。若沒有“備援的接收者”,則啟動完整的訊息的轉發機制,執行期系統會把與訊息有關的全部細節都封裝到NSInvocation物件中,再給接收者最後一次機會,令其設法解決當前還未處理的這條訊息。iOS進階之傳遞訊息 上篇講到訊息傳遞,其中有個問題:物件在收到無法解讀的訊息之後會發生什麼情況? 若想令類能理解某條訊息,我們必須以程式碼實現出對應的方法才行。但是,在編譯期向類傳送了其無法解讀的訊息並不會報錯,因為在執行期可以繼續向類中新增方法,所以編譯器在編譯時還無法確知類中到底會不會有某個方法實現。當物件接收到無法解讀的訊息後,就會啟動“訊息轉發”機制,程式設計師可經此過程告訴物件應該如何處理位置訊息。
動態方法解析
物件在收到無法解讀的訊息後,首先將呼叫其所類的下列類方法:
+ (BOOL)resolveInstanceMethod:(SEL)selector
該方法的引數就是那個未知的選擇子,其返回值為Boolean型別,表示這個類是否能新增一個實類用於處理此選擇子。在繼續往下執行轉發機制之前,本類有機會新增一個處理此選擇子的方法。加入尚未實現的方法不是例項方法而是類方法,那麼執行期就會呼叫另外一個方法,該方與“resolveInstanceMethod”類似,叫做“resolveClassMethod:”。 使用這種辦法的前提是:相關方法的實現程式碼已經寫好,只等著執行的時候動態插在類裡面就可以了。此方法常用來實現@dynamic屬性,比如說,要訪問CoreData框架中NSManagedObjects物件時就可以這麼做,因為實現這些屬性所需的儲存方法在編譯器就能確定。
備援接受者
當前接收者還有第二次機會能處理未知的選擇子,在這一步中,執行期系統會問它:能不能把這條訊息轉給其他接收者來處理。與該步驟對應的處理方法如下:
- (id)forwardingTargetForSelector:(SEL)aSelector
方法引數代表未知的選擇子,若當前接收者能找到備援物件,則將其返回,則將其返回,若找不到,就返回nil。通過此方案,我們可以用“組合”來模擬出“多重繼承”的某些特性。在一個物件內部,可能還有一系列其他物件,該物件可經由此方法將能夠處理某選擇子的相關內部物件返回,這樣的話,在外界看來,好像是該物件親自處理了這些訊息似的。 請注意,我們無法操作經由這一步所轉發的訊息。若是想在傳送給備援接收者之前先修改訊息內容,那就得通過完整的訊息轉發機制來做了。
完整的訊息轉發
如果轉發演算法已經來到這一步的話,那麼唯一能做的就是啟用完整的訊息轉發機制了。首先建立NSInvocation物件,把與尚未處理的那條訊息有關的全部細節都封於其中。此物件包含選擇子、目標及引數。在觸發NSInvocation物件時,“訊息派發系統”將親自出馬,把訊息指派給目標物件。 此步驟會呼叫下列方法來轉發訊息:
- (void)forwardInvocation:(NSInvocation *)anInvocation
這個方法可以實現得很簡單:只需改變呼叫目標,使訊息在新目標上得以呼叫即可。然而這樣實現出來的方法與“備援接受者”方案所實現的方法等效,所以很少有人採用這麼簡單的實現方法。比較有用的實現方式為:在觸發訊息前,先以某種改變訊息內容,比如追加一個引數,或是改變選擇子,等等。 實現此方法時,若發現某呼叫操作不應由本類處理,則需要呼叫超類的同名方法。這樣的話,繼承體系中的每個類都有機會處理此呼叫請求,直至NSObject。如果最後呼叫了NSObject類的方法,那麼該方法還會繼而呼叫“doesNotRecognizeSelector:”以丟擲異常,此異常表明選擇子最終未能得到處理。
訊息轉發全流程
接收者在每一步中均有機會處理訊息。步驟越往後,處理訊息的代價就越大。最好能夠在第一步就處理完,這樣的話,執行期系統就可以將此方法快取起來了。如果這個類的例項稍後還收到同名選擇子,那麼根本無須啟動訊息轉發流程。若想在第三步裡把訊息轉給備援的接收者,那還不如把轉發操作提前到第二步。因為第三步只是修改了呼叫目標,這項改動放在第二步執行會更為簡單,不然的話,還得建立並處理完整的NSInvocation。
要點
1️⃣若物件無法響應某個選擇子,則進入訊息轉發流程。
2️⃣通過執行期的動態方法解析功能,我們可以在需要用到某個方法時再將其加入類中。
3️⃣物件可以把其無法解讀的某些選擇子轉交給其他物件來處理。
4️⃣經過上述兩步後,如果還是沒法處理選擇子,那就啟動完整的訊息轉發機制。