RunTime運(yùn)行時(shí)(二)

轉(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í)例變量的途徑。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 轉(zhuǎn)至元數(shù)據(jù)結(jié)尾創(chuàng)建: 董瀟偉,最新修改于: 十二月 23, 2016 轉(zhuǎn)至元數(shù)據(jù)起始第一章:isa和Class一....
    40c0490e5268閱讀 2,077評(píng)論 0 9
  • 參考鏈接: http://www.cnblogs.com/ioshe/p/5489086.html 簡(jiǎn)介 Runt...
    樂(lè)樂(lè)的簡(jiǎn)書閱讀 2,249評(píng)論 0 9
  • 參數(shù)自一個(gè)指針,指向類的要接收消息的實(shí)例。 OP在處理該信息的方法的選擇。 ......可變參數(shù)列表包含參數(shù)的方法...
    reallychao閱讀 897評(píng)論 0 0
  • 轉(zhuǎn)載:http://yulingtianxia.com/blog/2014/11/05/objective-c-r...
    F麥子閱讀 839評(píng)論 0 2
  • 這篇文章完全是基于南峰子老師博客的轉(zhuǎn)載 這篇文章完全是基于南峰子老師博客的轉(zhuǎn)載 這篇文章完全是基于南峰子老師博客的...
    西木閱讀 30,893評(píng)論 33 466

友情鏈接更多精彩內(nèi)容