消息發送(Messaging)
8、以上便是runtime相關的一些數據結構,接下來我們回看一開始的疑問:
objc_msgSend()函數在執行的過程中是如何找到對應的類,找到對應的方法實現的呢?
這就是消息發送(messaging)的處理過程了:
(1)、對于上文的Class的數據結構的描述,官方文檔只簡略了把它們歸納成了兩部分:一個指向其父類的指針和一個方法調用表(這個Class的所有方法的selector和實現代碼所在地址的關聯表);
(2)、當某個消息被發送到一個對象之后(即對象執行某個方法),runtime會根據這個對象的isa指針找到它所屬的類,在類的方法調用表里查找對應的selector。如果沒有找到的話,它會繼續沿著類的super_class指針找到它的父類,在父類的方法調用表里查找對應的selector;
(3)、找到了對應的selector之后,就根據selector找到方法的實現代碼的地址,執行這些實現的代碼。如果沒有找到,則會啟用消息轉發(message forwarding)機制,這個機制在后文會詳談;
(4)、所以一個方法的實現代碼,并不是在編譯的時候就確定好的,它是直到調用這個方法的時候,才通過消息發送機制,定位到方法的實現代碼處執行,所以方法的調用和實現是動態綁定(dynamically bound)的;
(5)、當執行方法的實現代碼的時候,objc_msgSend()函數不止會把實現代碼需要的參數傳給它,同時還會多傳兩個隱藏參數:self和_cmd。這兩個參數其實就是objc_msgSend(receiver, selector)的receiver和selector,表面上objc_msgSend()函數只是把receiver和selector之后的那些參數傳給了方法的實現代碼(如果后面還有參數的話),實際上它偷偷地把receiver和selector也給傳進去了,方法的實現代碼里使用self和_cmd這兩個形參就能調用到receiver和selector。
所以為什么當我們在編寫一個方法的代碼的時候,使用“self”就能直接調用到這個方法調用的對象,就是通過這個過程傳遞進來的;
(6)、為了提高消息發送的速度,每次在查找方法調用表前,會先查找一個類的cache(見前文7(7)),cache里存放了常用的方法的selector和實現代碼地址的對應關系,如果在cache里能夠找到對應的selector,那就可以直接跳到方法的實現代碼處做執行,不需要再去跑剩下的消息發送流程。
判斷方法是否“常用”依照了這樣一個原則:如果一個方法被調用了一次,那么它就很有可能會被調用第二次,這個方法就會被加入cache。如果程序運行了足夠久,讓cache做了足夠的熱身(warn up),那么程序的運行會比一開始的時候更快,此時幾乎所有需要調用的方法都能在cache里找到。
(7)、官方提供的消息發送的流程圖如下:
動態方法解析(Dynamic Method Resolution)和消息轉發(Message Forwarding)
9、那么還有一個疑問沒有討論,就是如果在消息發送的過程中發生了意外的話,它又會怎么處理呢?其實也就是8(3)中所提到的:如果消息發送沒能找到對應的方法,那么runtime就會啟用消息轉發(message forwarding)機制來進行處理。
首先我們知道,正常情況下我們會在類的@implementation寫好方法的實現代碼,當執行這個方法的時候,runtime最終會綁定到這段實現代碼并執行它,這是正常的流程。如果沒有找到對應的實現代碼,那么runtime會依次按照下面三個步驟來處理這個消息:
(1)、其實runtime并不會立刻就啟動消息轉發,首先runtime會做的是動態方法解析(Dynamic Method Resolution)。它調用當前類的類方法+resolveInstanceMethod:(處理實例方法)或+resolveClassMethod:(處理類方法),看看是否在方法中有動態添消息的方法實現,有則執行,無則繼續下一步處理;
(2)、如果來到這一步,才是真正地開始消息轉發了。runtime首先會進行快速轉發(Fast Forwarding),它會調用當前類的- (id)forwardingTargetForSelector:方法,看看方法中是否有將此消息轉發給其他類的處理,有則將消息轉發給對應處理的類,無則繼續下一步處理;
(3)、最后runtime會進行完整的消息轉發(Normal Forwarding),它首先會調用- (NSMethodSignature *)methodSignatureForSelector:方法,如果方法能正常返回一個NSMethodSignature對象,那么它就會創建一個表示消息的NSInvocation對象,這個對象包含了消息相關的所有細節,然后調用- (void)forwardInvocation:方法進行完整的轉發,如果- (void)forwardInvocation:方法中有對這個消息的相關轉發處理,就將消息轉發給對應的另一類進行處理處理,如果沒有,則拋出unrecognized selector sent to instance或者unrecognized selector sent to class的異常信息。
這就是一個完整的消息轉發處理流程。
10、我們可以通過@samlaudev的一個demo驗證整個轉發過程:
(1)、首先定義了一個Message類,并在類中定義了一個實例方法:
當調用了這個方法的時候:
會有如下輸出:
這是一個正常的方法執行;
(2)、然后我們首先來驗證第一步:動態方法解析。
將-(void)sendMessage:方法的實現代碼注掉,同時添加以下方法:
這對應處理的第一步,此時-(void)sendMessage:方法已經沒有正常的實現代碼了,根據第一步,runtime會在+resolveInstanceMethod:方法中看看是否有動態添加-(void)sendMessage:方法實現,此時運行后輸出:
說明runtime確實執行了動態方法解析;
(3)、然后我們來驗證第二步,即是消息轉發的第一步:快速轉發給其他類處理。
此時需要新建一個其他類MessageForwarding,然后在MessageForwarding類中也定義一個-(void)message:方法:
然后回到Message類,把上一步的+resolveInstanceMethod:方法注掉,添加以下快速轉發的方法:
意思即是將這個消息快速轉發給MessageForwarding對象去處理,運行輸出如下:
說明runtime執行了消息轉發的第一步;
(4)、最后我們來驗證處理的第三步,即是消息轉發的第二步:將消息完整轉發給其他類處理。
此時我們再新建一個類MessageNormalForwading,并在MessageNormalForwading類中也定義一個-(void)message:方法:
回到Message類,將第二步的-(id) forwardingTargetForSelector:方法注掉,然后添加以下兩個方法:
將消息封裝成一個NSIncocation對象,然后將它完整轉發給MessageNormalForwading類去處理。執行后輸出:
說明runtime完整地執行了消息轉發的第二步。
由此我們驗證了這三個步驟。
動態解析類方法和類型編碼
11、在10(2)所處理的第一步中,如果需要動態解析的方法是類方法,應該怎么處理呢?
我們給Message類聲明一個類方法+(void)classSendMessage:并且不做任何實現,然后需要在Message類中添加這樣一個方法來處理:
執行以下代碼后:
輸出如下:
需要注意的地方是,在classSendMessage:方法內執行class_addMethod()函數時的第一個參數。
當我們添加實例方法的時候,class_addMethod()函數第一個參數傳的是[self class],傳當前的類;而添加類方法的時候,就需要傳[self class]所屬的類,當前類所屬的類,即是元類(Meta Class)。
這正是我們在前文討論過的,在類的method_list里添加方法,會成為它的實例可調用的方法,即是這個類的實例方法;在類所屬的元類的method_list里添加方法,會成為元類的實例可調用的方法,元類的實例即是當前類,于是成為了這個類的類方法。
12、消息轉發能讓一個類通過把消息傳遞給其他類處理,來處理一些它本來不能處理的方法,看起來似乎能模擬“多重繼承”的效果,通過把不同消息轉發給其他類處理模擬了繼承自其他類的效果。不過消息轉發雖然類似于繼承,但NSObject的一些方法還是能區分兩者,如respondsToSelector:和isKindOfClass:只能用于繼承體系,而不能用于轉發鏈。
13、還有一個地方可以注意一下:在動態方法解析和完整消息轉發中的相關方法中,都出現了這么一個字符串:"v@*",這個字符串是類型編碼,它將消息中的方法歸納成幾個字符串來表示。
比如上文消息轉發的例子中,消息里的方法是-(void)message:,于是"v@*"中的v表示方法返回值為void,*表示方法的參數是NSString類型的,@則表示隱藏參數self。
隱藏參數在類型編碼中是可寫可不寫的,所以考慮到還有另外一個隱藏參數_cmd,這個類型編碼寫成"v@:*"也是可以的。當然直接寫成"v*"也沒問題。
參考文檔:
官方文檔
https://github.com/samlaudev/RuntimeDemo
http://www.lxweimin.com/p/25a319aee33d
http://www.cocoachina.com/ios/20141105/10134.html
http://www.cocoachina.com/ios/20141106/10150.html