消息發(fā)送(Messaging)
8臭蚁、以上便是runtime相關(guān)的一些數(shù)據(jù)結(jié)構(gòu)最铁,接下來我們回看一開始的疑問:
objc_msgSend()函數(shù)在執(zhí)行的過程中是如何找到對應的類,找到對應的方法實現(xiàn)的呢垮兑?
這就是消息發(fā)送(messaging)的處理過程了:
(1)冷尉、對于上文的Class的數(shù)據(jù)結(jié)構(gòu)的描述,官方文檔只簡略了把它們歸納成了兩部分:一個指向其父類的指針和一個方法調(diào)用表(這個Class的所有方法的selector和實現(xiàn)代碼所在地址的關(guān)聯(lián)表)系枪;
(2)雀哨、當某個消息被發(fā)送到一個對象之后(即對象執(zhí)行某個方法),runtime會根據(jù)這個對象的isa指針找到它所屬的類私爷,在類的方法調(diào)用表里查找對應的selector雾棺。如果沒有找到的話,它會繼續(xù)沿著類的super_class指針找到它的父類衬浑,在父類的方法調(diào)用表里查找對應的selector捌浩;
(3)、找到了對應的selector之后工秩,就根據(jù)selector找到方法的實現(xiàn)代碼的地址尸饺,執(zhí)行這些實現(xiàn)的代碼。如果沒有找到助币,則會啟用消息轉(zhuǎn)發(fā)(message forwarding)機制浪听,這個機制在后文會詳談;
(4)奠支、所以一個方法的實現(xiàn)代碼馋辈,并不是在編譯的時候就確定好的抚芦,它是直到調(diào)用這個方法的時候倍谜,才通過消息發(fā)送機制迈螟,定位到方法的實現(xiàn)代碼處執(zhí)行,所以方法的調(diào)用和實現(xiàn)是動態(tài)綁定(dynamically bound)的尔崔;
(5)答毫、當執(zhí)行方法的實現(xiàn)代碼的時候,objc_msgSend()函數(shù)不止會把實現(xiàn)代碼需要的參數(shù)傳給它季春,同時還會多傳兩個隱藏參數(shù):self和_cmd洗搂。這兩個參數(shù)其實就是objc_msgSend(receiver, selector)的receiver和selector,表面上objc_msgSend()函數(shù)只是把receiver和selector之后的那些參數(shù)傳給了方法的實現(xiàn)代碼(如果后面還有參數(shù)的話)载弄,實際上它偷偷地把receiver和selector也給傳進去了耘拇,方法的實現(xiàn)代碼里使用self和_cmd這兩個形參就能調(diào)用到receiver和selector。
所以為什么當我們在編寫一個方法的代碼的時候宇攻,使用“self”就能直接調(diào)用到這個方法調(diào)用的對象惫叛,就是通過這個過程傳遞進來的;
(6)逞刷、為了提高消息發(fā)送的速度嘉涌,每次在查找方法調(diào)用表前,會先查找一個類的cache(見前文7(7))夸浅,cache里存放了常用的方法的selector和實現(xiàn)代碼地址的對應關(guān)系仑最,如果在cache里能夠找到對應的selector,那就可以直接跳到方法的實現(xiàn)代碼處做執(zhí)行帆喇,不需要再去跑剩下的消息發(fā)送流程警医。
判斷方法是否“常用”依照了這樣一個原則:如果一個方法被調(diào)用了一次,那么它就很有可能會被調(diào)用第二次坯钦,這個方法就會被加入cache法严。如果程序運行了足夠久,讓cache做了足夠的熱身(warn up)葫笼,那么程序的運行會比一開始的時候更快深啤,此時幾乎所有需要調(diào)用的方法都能在cache里找到。
(7)路星、官方提供的消息發(fā)送的流程圖如下:
動態(tài)方法解析(Dynamic Method Resolution)和消息轉(zhuǎn)發(fā)(Message Forwarding)
9溯街、那么還有一個疑問沒有討論,就是如果在消息發(fā)送的過程中發(fā)生了意外的話洋丐,它又會怎么處理呢呈昔?其實也就是8(3)中所提到的:如果消息發(fā)送沒能找到對應的方法,那么runtime就會啟用消息轉(zhuǎn)發(fā)(message forwarding)機制來進行處理友绝。
首先我們知道堤尾,正常情況下我們會在類的@implementation寫好方法的實現(xiàn)代碼,當執(zhí)行這個方法的時候迁客,runtime最終會綁定到這段實現(xiàn)代碼并執(zhí)行它郭宝,這是正常的流程辞槐。如果沒有找到對應的實現(xiàn)代碼,那么runtime會依次按照下面三個步驟來處理這個消息:
(1)粘室、其實runtime并不會立刻就啟動消息轉(zhuǎn)發(fā)榄檬,首先runtime會做的是動態(tài)方法解析(Dynamic Method Resolution)。它調(diào)用當前類的類方法+resolveInstanceMethod:(處理實例方法)或+resolveClassMethod:(處理類方法)衔统,看看是否在方法中有動態(tài)添消息的方法實現(xiàn)鹿榜,有則執(zhí)行,無則繼續(xù)下一步處理锦爵;
(2)舱殿、如果來到這一步,才是真正地開始消息轉(zhuǎn)發(fā)了险掀。runtime首先會進行快速轉(zhuǎn)發(fā)(Fast Forwarding)怀薛,它會調(diào)用當前類的- (id)forwardingTargetForSelector:方法,看看方法中是否有將此消息轉(zhuǎn)發(fā)給其他類的處理迷郑,有則將消息轉(zhuǎn)發(fā)給對應處理的類枝恋,無則繼續(xù)下一步處理;
(3)嗡害、最后runtime會進行完整的消息轉(zhuǎn)發(fā)(Normal Forwarding)焚碌,它首先會調(diào)用- (NSMethodSignature *)methodSignatureForSelector:方法,如果方法能正常返回一個NSMethodSignature對象霸妹,那么它就會創(chuàng)建一個表示消息的NSInvocation對象十电,這個對象包含了消息相關(guān)的所有細節(jié)阀圾,然后調(diào)用- (void)forwardInvocation:方法進行完整的轉(zhuǎn)發(fā)睁搭,如果- (void)forwardInvocation:方法中有對這個消息的相關(guān)轉(zhuǎn)發(fā)處理,就將消息轉(zhuǎn)發(fā)給對應的另一類進行處理處理存筏,如果沒有罢绽,則拋出unrecognized selector sent to instance或者unrecognized selector sent to class的異常信息畏线。
這就是一個完整的消息轉(zhuǎn)發(fā)處理流程。
10良价、我們可以通過@samlaudev的一個demo驗證整個轉(zhuǎn)發(fā)過程:
(1)寝殴、首先定義了一個Message類,并在類中定義了一個實例方法:
當調(diào)用了這個方法的時候:
會有如下輸出:
這是一個正常的方法執(zhí)行明垢;
(2)蚣常、然后我們首先來驗證第一步:動態(tài)方法解析。
將-(void)sendMessage:方法的實現(xiàn)代碼注掉痊银,同時添加以下方法:
這對應處理的第一步抵蚊,此時-(void)sendMessage:方法已經(jīng)沒有正常的實現(xiàn)代碼了,根據(jù)第一步,runtime會在+resolveInstanceMethod:方法中看看是否有動態(tài)添加-(void)sendMessage:方法實現(xiàn)贞绳,此時運行后輸出:
說明runtime確實執(zhí)行了動態(tài)方法解析谷醉;
(3)、然后我們來驗證第二步熔酷,即是消息轉(zhuǎn)發(fā)的第一步:快速轉(zhuǎn)發(fā)給其他類處理。
此時需要新建一個其他類MessageForwarding豺裆,然后在MessageForwarding類中也定義一個-(void)message:方法:
然后回到Message類拒秘,把上一步的+resolveInstanceMethod:方法注掉,添加以下快速轉(zhuǎn)發(fā)的方法:
意思即是將這個消息快速轉(zhuǎn)發(fā)給MessageForwarding對象去處理臭猜,運行輸出如下:
說明runtime執(zhí)行了消息轉(zhuǎn)發(fā)的第一步躺酒;
(4)、最后我們來驗證處理的第三步蔑歌,即是消息轉(zhuǎn)發(fā)的第二步:將消息完整轉(zhuǎn)發(fā)給其他類處理羹应。
此時我們再新建一個類MessageNormalForwading,并在MessageNormalForwading類中也定義一個-(void)message:方法:
回到Message類次屠,將第二步的-(id) forwardingTargetForSelector:方法注掉园匹,然后添加以下兩個方法:
將消息封裝成一個NSIncocation對象,然后將它完整轉(zhuǎn)發(fā)給MessageNormalForwading類去處理劫灶。執(zhí)行后輸出:
說明runtime完整地執(zhí)行了消息轉(zhuǎn)發(fā)的第二步裸违。
由此我們驗證了這三個步驟。
動態(tài)解析類方法和類型編碼
11本昏、在10(2)所處理的第一步中供汛,如果需要動態(tài)解析的方法是類方法,應該怎么處理呢涌穆?
我們給Message類聲明一個類方法+(void)classSendMessage:并且不做任何實現(xiàn)怔昨,然后需要在Message類中添加這樣一個方法來處理:
執(zhí)行以下代碼后:
輸出如下:
需要注意的地方是,在classSendMessage:方法內(nèi)執(zhí)行class_addMethod()函數(shù)時的第一個參數(shù)宿稀。
當我們添加實例方法的時候趁舀,class_addMethod()函數(shù)第一個參數(shù)傳的是[self class],傳當前的類祝沸;而添加類方法的時候赫编,就需要傳[self class]所屬的類,當前類所屬的類奋隶,即是元類(Meta Class)擂送。
這正是我們在前文討論過的,在類的method_list里添加方法唯欣,會成為它的實例可調(diào)用的方法嘹吨,即是這個類的實例方法;在類所屬的元類的method_list里添加方法境氢,會成為元類的實例可調(diào)用的方法蟀拷,元類的實例即是當前類碰纬,于是成為了這個類的類方法。
12问芬、消息轉(zhuǎn)發(fā)能讓一個類通過把消息傳遞給其他類處理悦析,來處理一些它本來不能處理的方法,看起來似乎能模擬“多重繼承”的效果此衅,通過把不同消息轉(zhuǎn)發(fā)給其他類處理模擬了繼承自其他類的效果强戴。不過消息轉(zhuǎn)發(fā)雖然類似于繼承,但NSObject的一些方法還是能區(qū)分兩者挡鞍,如respondsToSelector:和isKindOfClass:只能用于繼承體系骑歹,而不能用于轉(zhuǎn)發(fā)鏈。
13墨微、還有一個地方可以注意一下:在動態(tài)方法解析和完整消息轉(zhuǎn)發(fā)中的相關(guān)方法中道媚,都出現(xiàn)了這么一個字符串:"v@*",這個字符串是類型編碼翘县,它將消息中的方法歸納成幾個字符串來表示最域。
比如上文消息轉(zhuǎn)發(fā)的例子中,消息里的方法是-(void)message:锈麸,于是"v@*"中的v表示方法返回值為void羡宙,*表示方法的參數(shù)是NSString類型的,@則表示隱藏參數(shù)self掐隐。
隱藏參數(shù)在類型編碼中是可寫可不寫的狗热,所以考慮到還有另外一個隱藏參數(shù)_cmd,這個類型編碼寫成"v@:*"也是可以的虑省。當然直接寫成"v*"也沒問題匿刮。
參考文檔:
官方文檔
https://github.com/samlaudev/RuntimeDemo
http://www.reibang.com/p/25a319aee33d
http://www.cocoachina.com/ios/20141105/10134.html
http://www.cocoachina.com/ios/20141106/10150.html