轉(zhuǎn)自原文地址:http://blog.csdn.net/wzzvictory/article/details/8624057
今天開(kāi)始說(shuō)說(shuō)runtime system中最關(guān)鍵的消息相關(guān)內(nèi)容。
一、runtime中的消息
-
1、什么是消息
進(jìn)入今天的正題之前,先來(lái)說(shuō)說(shuō)跟message息息相關(guān)的幾個(gè)概念
①message(消息)
message的具體定義很難說(shuō),因?yàn)椴](méi)有真正的代碼描述,簡(jiǎn)單的講message 是一種抽象,包括了函數(shù)名+參數(shù)列表,他并沒(méi)有實(shí)際的實(shí)體存在。
②method(方法)
method是真正的存在的代碼。如:- (int)meaning { return 42; }
③selector(方法選擇器)
selector通過(guò)SEL類型存在,描述一個(gè)特定的method 或者說(shuō) message。在實(shí)際編程中,可以通過(guò)selector進(jìn)行檢索方法等操作。
-
2、兩個(gè)跟消息相關(guān)的概念
①SEL
SEL又叫方法選擇器,這到底是個(gè)什么玩意呢?在objc.h中是這樣定義的:
typedef struct objc_selector *SEL;
這個(gè)SEL表示什么?首先,說(shuō)白了,方法選擇器僅僅是一個(gè)char *指針,僅僅表示它所代表的方法名字罷了,有如下證據(jù):
SEL selector = @selector(message); //@selector不是函數(shù)調(diào)用,只是給這個(gè)坑爹的編譯器的一個(gè)提示
NSLog (@"%s", (char *)selector); //print message
這時(shí)打印的結(jié)果就是:message
Objective-C在編譯的時(shí)候,會(huì)根據(jù)方法的名字,生成一個(gè)用 來(lái)區(qū)分這個(gè)方法的唯一的一個(gè)ID,這個(gè)ID就是SEL類型的。我們需要注意的是,只要方法的名字相同,那么它們的ID都是相同的。就是說(shuō),不管是超類還是子類,不管是有沒(méi)有超類和子類的關(guān)系,只要名字相同那么ID就是一樣的。而這也就導(dǎo)致了Objective-C在處理有相同函數(shù)名和參數(shù)個(gè)數(shù)但參數(shù)類型不同的函數(shù)的能力非常的弱,比如當(dāng)你想在程序中實(shí)現(xiàn)下面兩個(gè)方法:
-(void)setWidth:(int)width;
-(void)setWidth:(double)width;
這樣的函數(shù)則被認(rèn)為是一種編譯錯(cuò)誤,而這最終導(dǎo)致了一個(gè)非常非常奇怪的Objective-C特色的函數(shù)命名:
-(void)setWidthIntValue:(int)width;
-(void)setWidthDoubleValue:(double)width;
可能有人會(huì)問(wèn),runtime費(fèi)了那么老半天勁,究竟想做什么?GC來(lái)了。
剛才我們說(shuō)道,編譯器會(huì)根據(jù)每個(gè)方法的方法名為那個(gè)方法生成唯一的SEL,這些SEL組成了一個(gè)Set集合,這個(gè)Set簡(jiǎn)單的說(shuō)就是一個(gè)經(jīng)過(guò)了優(yōu)化過(guò)的hash表。而Set的特點(diǎn)就是唯一,也就是SEL是唯一的,因此,如果我們想到這個(gè)方法集合中查找某個(gè)方法時(shí),只需要去找到這個(gè)方法對(duì)應(yīng)的SEL就行了,SEL實(shí)際上就是根據(jù)方法名hash化了的一個(gè)字符串,而對(duì)于字符串的比較僅僅需要比較他們的地址就可以了,犀利,速度上無(wú)語(yǔ)倫比?。〉?,有一個(gè)問(wèn)題,就是數(shù)量增多會(huì)增大hash沖突而導(dǎo)致的性能下降(或是沒(méi)有沖突,因?yàn)橐部赡苡玫氖莗erfect hash)。但是不管使用什么樣的方法加速,如果能夠?qū)⒖偭繙p少(多個(gè)方法可能對(duì)應(yīng)同一個(gè)SEL),那將是最犀利的方法。那么,我們就不難理解,為什么SEL僅僅是函數(shù)名了。
到這里,我們明白了,本質(zhì)上,SEL只是一個(gè)指向方法的指針(準(zhǔn)確的說(shuō),只是一個(gè)根據(jù)方法名hash化了的KEY值,能唯一代表一個(gè)方法),它的存在只是為了加快方法的查詢速度?。。?!
-
②IMP
IMP在objc.h中是如此定義的:
typedef id (*IMP)(id, SEL, ...);
這個(gè)比SEL要好理解多了,熟悉C語(yǔ)言的同學(xué)都知道,這其實(shí)是一個(gè)函數(shù)指針。前面介紹過(guò)的SEL,就是為IMP服務(wù)的。由于每個(gè)方法都對(duì)應(yīng)唯一的SEL,因此我們可以通過(guò)SEL方便、快速、準(zhǔn)確的獲得它所對(duì)應(yīng)的IMP(也就是函數(shù)指針),而在取得了函數(shù)指針之后,也就意味著我們?nèi)〉昧藞?zhí)行的時(shí)候的這段方法的代碼的入口,這樣我們就可以像普通的C語(yǔ)言函數(shù)調(diào)用一樣使用這個(gè)函數(shù)指針。當(dāng)然我們可以把函數(shù)指針作為參數(shù)傳遞到其他的方法,或者實(shí)例變量里面,從而獲得極大的動(dòng)態(tài)性。
下面的例子,介紹了取得函數(shù)指針,即函數(shù)指針的用法:
void (* performMessage)(id,SEL);//定義一個(gè)IMP(函數(shù)指針)
performMessage = (void (*)(id,SEL))[self methodForSelector:@selector(message)];//通過(guò)methodForSelector方法根據(jù)SEL獲取對(duì)應(yīng)的函數(shù)指針
performMessage(self,@selector(message));//通過(guò)取到的IMP(函數(shù)指針)跳過(guò)runtime消息傳遞機(jī)制,直接執(zhí)行message方法
用IMP 的方式,省去了runtime消息傳遞過(guò)程中所做的一系列動(dòng)作,比直接向?qū)ο蟀l(fā)送消息高效一些。
-
3、傳遞消息所用的幾個(gè)runtime方法
上篇文章中我們說(shuō)過(guò),下面的方法:
[receiver message]
在編譯后會(huì)變成:
[cpp] view plain copy
objc_msgSend(receiver, selector)
實(shí)際上,同objc_msgSend方法類似的還有幾個(gè):
[cpp] view plain copy
objc_msgSend_stret(返回值是結(jié)構(gòu)體)
objc_msgSend_fpret(返回值是浮點(diǎn)型)
objc_msgSendSuper(調(diào)用父類方法)
objc_msgSendSuper_stret(調(diào)用父類方法,返回值是結(jié)構(gòu)體)
它們的作用都是類似的,為了簡(jiǎn)單起見(jiàn),后續(xù)介紹消息和消息傳遞機(jī)制都以objc_msgSend方法為例。
二、消息調(diào)用流程
一切還是從消息表達(dá)式[receiver message]開(kāi)始,在被轉(zhuǎn)換成objc_msgSend(receiver, SEL)后,在運(yùn)行時(shí),runtime system會(huì)做以下事情:
-
1、檢查忽略的Selector,比如當(dāng)我們運(yùn)行在有垃圾回收機(jī)制的環(huán)境中,將會(huì)忽略retain和release消息。
-
2、檢查receiver是否為nil。不像其他語(yǔ)言,nil在objective-C中是完全合法的,并且這里有很多原因你也愿意這樣,比如,至少我們省去了給一個(gè)對(duì)象發(fā)送消息前檢查對(duì)象是否為空的操作。如果receiver為空,則會(huì)將 selector也設(shè)置為空,并且直接返回到消息調(diào)用的地方。如果對(duì)象非空,就繼續(xù)下一步。
-
3、接下來(lái)會(huì)根據(jù)SEL到當(dāng)前類中查找對(duì)應(yīng)的IMP,首先會(huì)在cache中檢索它,如果找到了就根據(jù)函數(shù)指針跳轉(zhuǎn)到這個(gè)函數(shù)執(zhí)行,否則進(jìn)行下一步。
-
4、檢索當(dāng)前類對(duì)象中的方法表(method list),如果找到了,加入cache中,并且就跳轉(zhuǎn)到這個(gè)函數(shù)之行,否則進(jìn)行下一步。
-
5、從父類中尋找,直到根類:NSObject類。找到了就將方法加入對(duì)應(yīng)類的cache表中,如果仍為找到,則要進(jìn)入后文介紹的內(nèi)容:動(dòng)態(tài)方法決議。
-
6、如果動(dòng)態(tài)方法決議仍不能解決問(wèn)題,只能進(jìn)行最后一次嘗試,進(jìn)入消息轉(zhuǎn)發(fā)流程。
-
7、如果還不行,去死吧。
下面的圖部分展示了這個(gè)調(diào)用過(guò)程:

寫到這大家肯定會(huì)發(fā)出這樣的疑問(wèn):我僅僅想調(diào)用一個(gè)方法而已,卻不得不經(jīng)歷那么多步驟,效率上怎么保證??蘋果也做了一些優(yōu)化上的工作。
三、函數(shù)檢索優(yōu)化措施
主要從下面兩個(gè)方面著手:
-
1、通過(guò)SEL進(jìn)行IMP匹配
先來(lái)看看類對(duì)象中保存的方法列表和方法的數(shù)據(jù)結(jié)構(gòu):
typedef struct method_list_t {
uint32_t entsize_NEVER_USE;
uint32_t count;
struct method_t first;
} method_list_t;
typedef struct method_t {
SEL name;
const char *types;//參數(shù)類型和返回值類型
IMP imp;
} method_t;
在前一篇文章介紹SEL的時(shí)候,我們已經(jīng)說(shuō)過(guò)了蘋果在通過(guò)SEL檢索IMP時(shí)做的努力,這里不再累述。
-
2、cache緩存
cache的原則就是緩存那些可能要執(zhí)行的函數(shù)地址,那么下次調(diào)用的時(shí)候,速度就可以快速很多。這個(gè)和CPU的各種緩存原理相通。好吧,說(shuō)了這么多了,再來(lái)認(rèn)識(shí)幾個(gè)名詞:
struct objc_cache {
uintptr_t mask;
uintptr_t occupied;
cache_entry *buckets[1];
};
typedef struct {
SEL name;
void *unused;
IMP imp;
} cache_entry;
看這個(gè)結(jié)構(gòu),有沒(méi)有搞錯(cuò)又是hash table。
objc_msgSend 首先在cache list 中找SEL,沒(méi)有找到就在class method中找,super class method中找(當(dāng)然super class 也有cache list)。而cache的機(jī)制則非常復(fù)雜了,由于Objective-C是動(dòng)態(tài)語(yǔ)言。所以,這里面還有很多的多線程同步問(wèn)題,而這些鎖又是效率的大敵,相關(guān)的內(nèi)容已經(jīng)遠(yuǎn)遠(yuǎn)超過(guò)本文討論的范圍。如果在緩存中已經(jīng)有了需要的方法選標(biāo),則消息僅僅比函數(shù)調(diào)用慢一點(diǎn)點(diǎn)。如果程序運(yùn)行了足夠長(zhǎng)的時(shí)間,幾乎每個(gè)消息都能在緩存中找到方法實(shí)現(xiàn)。程序運(yùn)行時(shí),緩存也將隨著新的消息的增加而增加。據(jù)牛人說(shuō)(沒(méi)有親測(cè)過(guò)),蘋果通過(guò)這些優(yōu)化,使消息傳遞和直接的函數(shù)調(diào)用效率上的差距已經(jīng)相當(dāng)?shù)男 ?/h3>
四、方法調(diào)用中的隱藏參數(shù)
親愛(ài)的Objective-C程序員們,你們?cè)谶M(jìn)行面向?qū)ο缶幊痰臅r(shí)候,在實(shí)例方法中都是用過(guò)self關(guān)鍵字吧,可是你有沒(méi)有想過(guò),為什么在一個(gè)實(shí)例方法中,通過(guò)self關(guān)鍵字就能取到調(diào)用當(dāng)前方法的對(duì)象呢?這就要?dú)w功與runtime system消息的隱藏參數(shù)了。(注:在此修正,類方法和實(shí)例方法中,都可以訪問(wèn)self和_cmd這兩個(gè)屬性,因?yàn)樗鼈兌疾粚儆陬惖膶?shí)例變量,而是形參?。。。≌`導(dǎo)大家了,深表歉意!?。。。?/h3>
當(dāng)objc_msgSend找到方法對(duì)應(yīng)的實(shí)現(xiàn)時(shí),它將直接調(diào)用該方法實(shí)現(xiàn),并將消息中所有的參數(shù)都傳遞給方法實(shí)現(xiàn),同時(shí),它還將傳遞兩個(gè)隱藏的參數(shù):
接收消息的對(duì)象(也就是self指向的內(nèi)容)
方法選標(biāo)(_cmd指向的內(nèi)容)
這些參數(shù)幫助方法實(shí)現(xiàn)獲得了消息表達(dá)式的信息。它們被認(rèn)為是”隱藏“的是因?yàn)樗鼈儾](méi)有在定義方法的源代碼中聲明,而是在代碼編譯時(shí)是插入方法的實(shí)現(xiàn)中的。盡管這些參數(shù)沒(méi)有被顯示聲明,但在源代碼中仍然可以引用它們(就象可以引用消息接收者對(duì)象的實(shí)例變 量一樣)。在方法中可以通過(guò) self 來(lái)引用消息接收者對(duì)象,通過(guò)選標(biāo)_cmd 來(lái)引用方法本身。下面的例子很好的說(shuō)明了這個(gè)問(wèn)題:
- (void)message
{
self.name = @"James";//通過(guò)self關(guān)鍵字給當(dāng)前對(duì)象的屬性賦值
SEL currentSel = _cmd;//通過(guò)_cmd關(guān)鍵字取到當(dāng)前函數(shù)對(duì)應(yīng)的SEL
NSLog(@"currentSel is :%s",(char *)currentSel);
}
打印結(jié)果:
ObjcRunTime[693:403] currentSel is :message
當(dāng)然,在這兩個(gè)參數(shù)中,self 更有用,更常用一些。實(shí)際上,它是在方法實(shí)現(xiàn)中訪問(wèn)消息接收者對(duì)象的實(shí)例變量的途徑。
四、方法調(diào)用中的隱藏參數(shù)
當(dāng)objc_msgSend找到方法對(duì)應(yīng)的實(shí)現(xiàn)時(shí),它將直接調(diào)用該方法實(shí)現(xiàn),并將消息中所有的參數(shù)都傳遞給方法實(shí)現(xiàn),同時(shí),它還將傳遞兩個(gè)隱藏的參數(shù):
接收消息的對(duì)象(也就是self指向的內(nèi)容)
方法選標(biāo)(_cmd指向的內(nèi)容)
這些參數(shù)幫助方法實(shí)現(xiàn)獲得了消息表達(dá)式的信息。它們被認(rèn)為是”隱藏“的是因?yàn)樗鼈儾](méi)有在定義方法的源代碼中聲明,而是在代碼編譯時(shí)是插入方法的實(shí)現(xiàn)中的。盡管這些參數(shù)沒(méi)有被顯示聲明,但在源代碼中仍然可以引用它們(就象可以引用消息接收者對(duì)象的實(shí)例變 量一樣)。在方法中可以通過(guò) self 來(lái)引用消息接收者對(duì)象,通過(guò)選標(biāo)_cmd 來(lái)引用方法本身。下面的例子很好的說(shuō)明了這個(gè)問(wèn)題:
- (void)message
{
self.name = @"James";//通過(guò)self關(guān)鍵字給當(dāng)前對(duì)象的屬性賦值
SEL currentSel = _cmd;//通過(guò)_cmd關(guān)鍵字取到當(dāng)前函數(shù)對(duì)應(yīng)的SEL
NSLog(@"currentSel is :%s",(char *)currentSel);
}
打印結(jié)果:
ObjcRunTime[693:403] currentSel is :message