深入解構(gòu)objc_msgSend函數(shù)的實(shí)現(xiàn)

objc_class(Class對(duì)象)結(jié)構(gòu)簡(jiǎn)介

熟悉OC語(yǔ)言的Runtime(運(yùn)行時(shí))機(jī)制以及對(duì)象方法調(diào)用機(jī)制的開(kāi)發(fā)者都知道,所有OC方法調(diào)用在編譯時(shí)都會(huì)轉(zhuǎn)化為對(duì)C函數(shù)objc_msgSend的調(diào)用舆吮。

/*下面的例子是在arm64體系下的函數(shù)調(diào)用實(shí)現(xiàn)揭朝,本文中如果沒(méi)有特殊說(shuō)明都是指在arm64體系下的結(jié)論*/// [view1 addSubview:view2];objc_msgSend(view1,"addSubview:",view2);// CGSize size = [view1 sizeThatFits:CGSizeZero];CGSize size=objc_msgSend(view1,"sizeThatFits:",CGSizeZero);//? CGFloat alpha = view1.alpha; CGFloat alpha=objc_msgSend(view1,"alpha");

系統(tǒng)的Runtime庫(kù)通過(guò)函數(shù)objc_msgSend以及OC對(duì)象中隱藏的isa數(shù)據(jù)成員來(lái)實(shí)現(xiàn)多態(tài)和運(yùn)行時(shí)方法查找以及執(zhí)行。每個(gè)對(duì)象的isa中保存著這個(gè)對(duì)象的類對(duì)象指針色冀,類對(duì)象是一個(gè)Class類型的數(shù)據(jù)潭袱,而Class則是一個(gè)objc_class結(jié)構(gòu)體指針類型的別名,它被定義如下:

typedefstructobjc_class*Class;

雖然在對(duì)外公開(kāi)暴露的頭文件#import <objc/runtime.h>中可以看到關(guān)于struct objc_class的定義锋恬,但可惜的是那只是objc1.0版本的定義屯换,而目前所運(yùn)行的objc2.0版本運(yùn)行時(shí)庫(kù)并沒(méi)有暴露出struct objc_class所定義的詳細(xì)內(nèi)容。

你可以在https://opensource.apple.com/source/objc4/objc4-723/中下載和查看開(kāi)源的最新版本的Runtime庫(kù)源代碼与学。Runtime庫(kù)的源代碼是用匯編和C++混合實(shí)現(xiàn)的彤悔,你可以在頭文件objc-runtime-new.h中看到關(guān)于struct objc_class結(jié)構(gòu)的詳細(xì)定義。objc_class結(jié)構(gòu)體用來(lái)描述一個(gè)OC類的類信息:包括類的名字索守、所繼承的基類晕窑、類中定義的方法列表描述、屬性列表描述卵佛、實(shí)現(xiàn)的協(xié)議描述杨赤、定義的成員變量描述等等信息蓝丙。在OC中類信息也是一個(gè)對(duì)象,所以又稱類信息為Class對(duì)象望拖。? 下面是一張objc_class結(jié)構(gòu)體定義的靜態(tài)類圖:

objc_class結(jié)構(gòu)

圖片最左邊顯示的內(nèi)容有一個(gè)編輯錯(cuò)誤渺尘,不應(yīng)該是NSObject而應(yīng)該是objc_class说敏。

objc_class結(jié)構(gòu)體中的數(shù)據(jù)成員非常的多也非常的復(fù)雜鸥跟,這里并不打算深入的去介紹它,本文主要介紹的是objc_msgSend函數(shù)內(nèi)部的實(shí)現(xiàn)盔沫,因此在下面的代碼中將會(huì)隱藏大部分?jǐn)?shù)據(jù)成員的定義医咨,并在不改變真實(shí)結(jié)構(gòu)體定義的基礎(chǔ)上只列出objc_msgSend方法內(nèi)部會(huì)訪問(wèn)和使用到的數(shù)據(jù)成員。

objc_msgSend函數(shù)的內(nèi)部實(shí)現(xiàn)

objc_msgSend函數(shù)是所有OC方法調(diào)用的核心引擎架诞,它負(fù)責(zé)查找真實(shí)的類或者對(duì)象方法的實(shí)現(xiàn)拟淮,并去執(zhí)行這些方法函數(shù)。因調(diào)用頻率是如此之高谴忧,所以要求其內(nèi)部實(shí)現(xiàn)近可能達(dá)到最高的性能很泊。這個(gè)函數(shù)的內(nèi)部代碼實(shí)現(xiàn)是用匯編語(yǔ)言來(lái)編寫(xiě)的,并且其中并沒(méi)有涉及任何需要線程同步和鎖相關(guān)的代碼沾谓。你可以在上面說(shuō)到的開(kāi)源URL鏈接中的Messengers文件夾下查看各種體系架構(gòu)下的匯編語(yǔ)言的實(shí)現(xiàn)委造。

;這里列出的是在arm64位真機(jī)模式下的匯編代碼實(shí)現(xiàn)。? ? 0x18378c420<+0>:? cmp? ? x0, #0x0? ? ? ? ? ? ? ? ? ; =0x0? ? 0x18378c424<+4>:? b.le? 0x18378c48c? ? ? ? ? ? ? ;<+108>0x18378c428<+8>:? ldr? ? x13, [x0]? ? 0x18378c42c<+12>:? and? ? x16, x13, #0xffffffff8? ? 0x18378c430<+16>:? ldp? ? x10, x11, [x16, #0x10]? ? 0x18378c434<+20>:? and? ? w12, w1, w11? ? 0x18378c438<+24>:? add? ? x12, x10, x12, lsl #4? ? 0x18378c43c<+28>:? ldp? ? x9, x17, [x12]? ? 0x18378c440<+32>:? cmp? ? x9, x1? ? 0x18378c444<+36>:? b.ne? 0x18378c44c? ? ? ? ? ? ? ;<+44>0x18378c448<+40>:? br? ? x17? ? 0x18378c44c<+44>:? cbz? ? x9, 0x18378c720? ? ? ? ? ; _objc_msgSend_uncached? ? 0x18378c450<+48>:? cmp? ? x12, x10? ? 0x18378c454<+52>:? b.eq? 0x18378c460? ? ? ? ? ? ? ;<+64>0x18378c458<+56>:? ldp? ? x9, x17, [x12, #-0x10]!? ? 0x18378c45c<+60>:? b? ? ? 0x18378c440? ? ? ? ? ? ? ;<+32>0x18378c460<+64>:? add? ? x12, x12, w11, uxtw #4? ? 0x18378c464<+68>:? ldp? ? x9, x17, [x12]? ? 0x18378c468<+72>:? cmp? ? x9, x1? ? 0x18378c46c<+76>:? b.ne? 0x18378c474? ? ? ? ? ? ? ;<+84>0x18378c470<+80>:? br? ? x17? ? 0x18378c474<+84>:? cbz? ? x9, 0x18378c720? ? ? ? ? ; _objc_msgSend_uncached? ? 0x18378c478<+88>:? cmp? ? x12, x10? ? 0x18378c47c<+92>:? b.eq? 0x18378c488? ? ? ? ? ? ? ;<+104>0x18378c480<+96>:? ldp? ? x9, x17, [x12, #-0x10]!? ? 0x18378c484<+100>: b? ? ? 0x18378c468? ? ? ? ? ? ? ;<+72>0x18378c488<+104>: b? ? ? 0x18378c720? ? ? ? ? ? ? ; _objc_msgSend_uncached? ? 0x18378c48c<+108>: b.eq? 0x18378c4c4? ? ? ? ? ? ? ;<+164>0x18378c490<+112>: mov? ? x10, #-0x1000000000000000? ? 0x18378c494<+116>: cmp? ? x0, x10? ? 0x18378c498<+120>: b.hs? 0x18378c4b0? ? ? ? ? ? ? ;<+144>0x18378c49c<+124>: adrp? x10, 202775? ? 0x18378c4a0<+128>: add? ? x10, x10, #0x220? ? ? ? ? ; =0x220? ? 0x18378c4a4<+132>: lsr? ? x11, x0, #60? ? 0x18378c4a8<+136>: ldr? ? x16, [x10, x11, lsl #3]? ? 0x18378c4ac<+140>: b? ? ? 0x18378c430? ? ? ? ? ? ? ;<+16>0x18378c4b0<+144>: adrp? x10, 202775? ? 0x18378c4b4<+148>: add? ? x10, x10, #0x2a0? ? ? ? ? ; =0x2a0? ? 0x18378c4b8<+152>: ubfx? x11, x0, #52, #8? ? 0x18378c4bc<+156>: ldr? ? x16, [x10, x11, lsl #3]? ? 0x18378c4c0<+160>: b? ? ? 0x18378c430? ? ? ? ? ? ? ;<+16>0x18378c4c4<+164>: mov? ? x1, #0x0? ? 0x18378c4c8<+168>: movi? d0, #0000000000000000? ? 0x18378c4cc<+172>: movi? d1, #0000000000000000? ? 0x18378c4d0<+176>: movi? d2, #0000000000000000? ? 0x18378c4d4<+180>: movi? d3, #0000000000000000? ? 0x18378c4d8<+184>: ret? ? ? ? 0x18378c4dc<+188>: nop

畢竟匯編語(yǔ)言代碼比較晦澀難懂均驶,因此這里將函數(shù)的實(shí)現(xiàn)反匯編成C語(yǔ)言的偽代碼:

//下面的結(jié)構(gòu)體中只列出objc_msgSend函數(shù)內(nèi)部訪問(wèn)用到的那些數(shù)據(jù)結(jié)構(gòu)和成員昏兆。/*

其實(shí)SEL類型就是一個(gè)字符串指針類型,所描述的就是方法字符串指針

*/typedefchar*SEL;/*

IMP類型就是所有OC方法的函數(shù)原型類型妇穴。

*/typedefid(*IMP)(idself,SEL _cmd,...);/*

? 方法名和方法實(shí)現(xiàn)桶結(jié)構(gòu)體

*/structbucket_t{SEL? key;//方法名稱IMP imp;//方法的實(shí)現(xiàn)爬虱,imp是一個(gè)函數(shù)指針類型};/*

? 用于加快方法執(zhí)行的緩存結(jié)構(gòu)體。這個(gè)結(jié)構(gòu)體其實(shí)就是一個(gè)基于開(kāi)地址沖突解決法的哈希桶腾它。

*/structcache_t{structbucket_t*buckets;//緩存方法的哈希桶數(shù)組指針跑筝,桶的數(shù)量 = mask + 1intmask;//桶的數(shù)量 - 1intoccupied;//桶中已經(jīng)緩存的方法數(shù)量。};/*

? ? OC對(duì)象的類結(jié)構(gòu)體描述表示携狭,所有OC對(duì)象的第一個(gè)參數(shù)保存是的一個(gè)isa指針继蜡。

*/structobjc_object{void*isa;};/*

? OC類信息結(jié)構(gòu)體,這里只展示出了必要的數(shù)據(jù)成員逛腿。

*/structobjc_class:objc_object{structobjc_class*superclass;//基類信息結(jié)構(gòu)體稀并。cache_t cache;//方法緩存哈希表//... 其他數(shù)據(jù)成員忽略。};/*

objc_msgSend的C語(yǔ)言版本偽代碼實(shí)現(xiàn).

receiver: 是調(diào)用方法的對(duì)象

op: 是要調(diào)用的方法名稱字符串

*/idobjc_msgSend(id receiver,SEL op,...){//1............................ 對(duì)象空值判斷单默。//如果傳入的對(duì)象是nil則直接返回nilif(receiver==nil)returnnil;//2............................ 獲取或者構(gòu)造對(duì)象的isa數(shù)據(jù)碘举。void*isa=NULL;//如果對(duì)象的地址最高位為0則表明是普通的OC對(duì)象,否則就是Tagged Pointer類型的對(duì)象if((receiver&0x8000000000000000)==0){structobjc_object*ocobj=(structobjc_object*)receiver;isa=ocobj->isa;}else{//Tagged Pointer類型的對(duì)象中沒(méi)有直接保存isa數(shù)據(jù)搁廓,所以需要特殊處理來(lái)查找對(duì)應(yīng)的isa數(shù)據(jù)引颈。//如果對(duì)象地址的最高4位為0xF, 那么表示是一個(gè)用戶自定義擴(kuò)展的Tagged Pointer類型對(duì)象if(((NSUInteger)receiver)>=0xf000000000000000){//自定義擴(kuò)展的Tagged Pointer類型對(duì)象中的52-59位保存的是一個(gè)全局?jǐn)U展Tagged Pointer類數(shù)組的索引值耕皮。intclassidx=(receiver&0xFF0000000000000)>>52isa=objc_debug_taggedpointer_ext_classes[classidx];}else{//系統(tǒng)自帶的Tagged Pointer類型對(duì)象中的60-63位保存的是一個(gè)全局Tagged Pointer類數(shù)組的索引值。intclassidx=((NSUInteger)receiver)>>60;isa=objc_debug_taggedpointer_classes[classidx];}}//因?yàn)閮?nèi)存地址對(duì)齊的原因和虛擬內(nèi)存空間的約束原因蝙场,//以及isa定義的原因需要將isa與上0xffffffff8才能得到對(duì)象所屬的Class對(duì)象凌停。structobjc_class*cls=(structobjc_class*)(isa&0xffffffff8);//3............................ 遍歷緩存哈希桶并查找緩存中的方法實(shí)現(xiàn)。IMP? imp=NULL;//cmd與cache中的mask進(jìn)行與計(jì)算得到哈希桶中的索引售滤,來(lái)查找方法是否已經(jīng)放入緩存cache哈希桶中罚拟。intindex=cls->cache.mask&op;while(true){//如果緩存哈希桶中命中了對(duì)應(yīng)的方法實(shí)現(xiàn),則保存到imp中并退出循環(huán)完箩。if(cls->cache.buckets[index].key==op){imp=cls->cache.buckets[index].imp;break;}//方法實(shí)現(xiàn)并沒(méi)有被緩存赐俗,并且對(duì)應(yīng)的桶的數(shù)據(jù)是空的就退出循環(huán)if(cls->cache.buckets[index].key==NULL){break;}//如果哈希桶中對(duì)應(yīng)的項(xiàng)已經(jīng)被占用但是又不是要執(zhí)行的方法,則通過(guò)開(kāi)地址法來(lái)繼續(xù)尋找緩存該方法的桶弊知。if(index==0){index=cls->cache.mask;//從尾部尋找}else{index--;//索引減1繼續(xù)尋找阻逮。}}/*end while*///4............................ 執(zhí)行方法實(shí)現(xiàn)或方法未命中緩存處理函數(shù)if(imp!=NULL)returnimp(receiver,op,...);//這里的... 是指?jìng)鬟f給objc_msgSend的OC方法中的參數(shù)。elsereturnobjc_msgSend_uncached(receiver,op,cls,...);}/*

? 方法未命中緩存處理函數(shù):objc_msgSend_uncached的C語(yǔ)言版本偽代碼實(shí)現(xiàn)秩彤,這個(gè)函數(shù)也是用匯編語(yǔ)言編寫(xiě)叔扼。

*/idobjc_msgSend_uncached(id receiver,SEL op,structobjc_class*cls){//這個(gè)函數(shù)很簡(jiǎn)單就是直接調(diào)用了_class_lookupMethodAndLoadCache3 來(lái)查找方法并緩存到struct objc_class中的cache中,最后再返回IMP類型呐舔。IMP? imp=_class_lookupMethodAndLoadCache3(receiver,op,cls);returnimp(receiver,op,....);}

可以看出objc_msgSend函數(shù)的實(shí)現(xiàn)邏輯主要分為4個(gè)部分:

1. 對(duì)象空值判斷

首先對(duì)傳進(jìn)來(lái)的方法接收者receiver進(jìn)行是否為空判斷币励,如果是nil則函數(shù)直接返回,這也就說(shuō)明了當(dāng)對(duì)一個(gè)nil對(duì)象調(diào)用方法時(shí)珊拼,不會(huì)產(chǎn)生崩潰,也不會(huì)進(jìn)入到對(duì)應(yīng)的方法實(shí)現(xiàn)中去流炕,整個(gè)過(guò)程其實(shí)什么也不會(huì)發(fā)生而是直接返回nil澎现。

2. 獲取或者構(gòu)造對(duì)象的isa數(shù)據(jù)

通常情況下每個(gè)OC對(duì)象的最開(kāi)始處都有一個(gè)隱藏的數(shù)據(jù)成員isa,isa保存有類的描述信息每辟,所以在執(zhí)行方法前就需要從對(duì)象處獲取到這個(gè)指針值剑辫。為了減少內(nèi)存資源的浪費(fèi),蘋(píng)果提出了Tagged Pointer類型對(duì)象的概念渠欺。比如一些NSString和NSNumber類型的實(shí)例對(duì)象就會(huì)被定義為T(mén)agged Pointer類型的對(duì)象妹蔽。Tagged Pointer類型的對(duì)象采用一個(gè)跟機(jī)器字長(zhǎng)一樣長(zhǎng)度的整數(shù)來(lái)表示一個(gè)OC對(duì)象,而為了跟普通OC對(duì)象區(qū)分開(kāi)來(lái)挠将,每個(gè)Tagged Pointer類型對(duì)象的最高位為1而普通的OC對(duì)象的最高位為0。因此上面的代碼中如果對(duì)象receiver地址的最高位為1則會(huì)將對(duì)象當(dāng)做Tagged Pointer對(duì)象來(lái)處理。從代碼實(shí)現(xiàn)中還可以看出系統(tǒng)中存在兩種類型的Tagged Pointer對(duì)象:如果是高四位全為1則是用戶自定義擴(kuò)展的Tagged Pointer對(duì)象锦秒,否則就是系統(tǒng)內(nèi)置的Tagged Pointer對(duì)象绍填。因?yàn)門(mén)agged Pointer對(duì)象中是不可能保存一個(gè)isa的信息的,而是用Tagged Pointer類型的對(duì)象中的某些bit位來(lái)保存所屬的類信息的索引值内贮。系統(tǒng)分別定義了兩個(gè)全局?jǐn)?shù)組變量:

extern"C"{externClass objc_debug_taggedpointer_classes[16*2];externClass objc_debug_taggedpointer_ext_classes[256];}

來(lái)保存所有的Tagged Pointer類型的類信息产园。對(duì)于內(nèi)置Tagged Pointer類型的對(duì)象來(lái)說(shuō)汞斧,其中的高四位保存的是一個(gè)索引值,通過(guò)這個(gè)索引值可以在objc_debug_taggedpointer_classes數(shù)組中查找到對(duì)象所屬的Class對(duì)象什燕;對(duì)于自定義擴(kuò)展Tagged Pointer類型的對(duì)象來(lái)說(shuō)粘勒,其中的高52位到59位這8位bit保存的是一個(gè)索引值,通過(guò)這個(gè)索引值可以在objc_debug_taggedpointer_ext_classes數(shù)組中查找到對(duì)象所屬的Class對(duì)象屎即。

思考和實(shí)踐: Tagged Pointer類型的對(duì)象中獲取isa數(shù)據(jù)的方式采用的是享元設(shè)計(jì)模式庙睡,這種設(shè)計(jì)模式在一定程度上還可以縮小一個(gè)對(duì)象占用的內(nèi)存尺寸。還有比如256色的位圖中每個(gè)像素位置中保存的是顏色索引值而非顏色的RGB值剑勾,從而減少了低色彩位圖的文件存儲(chǔ)空間埃撵。保存一個(gè)對(duì)象引用可能需要占用8個(gè)字節(jié),而保存一個(gè)索引值時(shí)可能只需要占用1個(gè)字節(jié)虽另。

在第二步中不管是普通的OC對(duì)象還是Tagged Pointer類型的對(duì)象都需要找到對(duì)象所屬的isa信息暂刘,并進(jìn)一步找到所屬的類對(duì)象,只有找到了類對(duì)象才能查找到對(duì)應(yīng)的方法的實(shí)現(xiàn)捂刺。

isa的內(nèi)部結(jié)構(gòu)

上面的代碼實(shí)現(xiàn)中谣拣,在將isa轉(zhuǎn)化為struct objc_class 時(shí)發(fā)現(xiàn)還進(jìn)行一次和0xffffffff8的與操作。雖然isa是一個(gè)長(zhǎng)度為8字節(jié)的指針值族展, 但是它保存的值并不一定是一個(gè)struct objc_class 對(duì)象的指針森缠。在arm64位體系架構(gòu)下的用戶進(jìn)程最大可訪問(wèn)的虛擬內(nèi)存地址范圍是0x0000000000 - 0x1000000000,也就是每個(gè)用戶進(jìn)程的可用虛擬內(nèi)存空間是64GB仪缸。同時(shí)因?yàn)橐粋€(gè)指針類型的變量存在著內(nèi)存地址對(duì)齊的因素所以指針變量的最低3位一定是0贵涵。所以將isa中保存的內(nèi)容和0xffffffff8進(jìn)行與操作得到的值才是真正的對(duì)象的Class對(duì)象指針。 arm64體系架構(gòu)對(duì)isa中的內(nèi)容進(jìn)行了優(yōu)化設(shè)計(jì)恰画,它除了保存著Class對(duì)象的指針外宾茂,還保存著諸如OC對(duì)象自身的引用計(jì)數(shù)值,對(duì)象是否被弱引用標(biāo)志拴还,對(duì)象是否建立了關(guān)聯(lián)對(duì)象標(biāo)志跨晴,對(duì)象是否正在銷毀中等等信息。如果要想更加詳細(xì)的了解isa的內(nèi)部結(jié)構(gòu)請(qǐng)參考文章:https://blog.csdn.net/u012581760/article/details/81230721中的介紹片林。

思考和實(shí)踐:對(duì)于所有指針類型的數(shù)據(jù)端盆,我們也可以利用其中的特性來(lái)使用0-2以及36-63這兩個(gè)區(qū)段的bit位進(jìn)行一些特定數(shù)據(jù)的存儲(chǔ)和設(shè)置,從而減少一些內(nèi)存的浪費(fèi)和開(kāi)銷费封。

3. 遍歷緩存哈希桶并查找緩存中的方法實(shí)現(xiàn)

一個(gè)Class對(duì)象的數(shù)據(jù)成員中有一個(gè)方法列表數(shù)組保存著這個(gè)類的所有方法的描述和實(shí)現(xiàn)的函數(shù)地址入口焕妙。如果每次方法調(diào)用時(shí)都要進(jìn)行一次這樣的查找,而且當(dāng)調(diào)用基類方法時(shí)孝偎,還需要遍歷基類進(jìn)行方法查找访敌,這樣勢(shì)必會(huì)對(duì)性能造成非常大的損耗。為了解決這個(gè)問(wèn)題系統(tǒng)為每個(gè)類建立了一個(gè)哈希表進(jìn)行方法緩存(objc_class 中的數(shù)據(jù)成員cache是一個(gè)cache_t類型的對(duì)象)衣盾。這個(gè)哈希表緩存由哈希桶來(lái)實(shí)現(xiàn)寺旺,每次當(dāng)執(zhí)行一個(gè)方法調(diào)用時(shí)爷抓,總是優(yōu)先從這個(gè)緩存中進(jìn)行方法查找,如果找到則執(zhí)行緩存中保存的方法函數(shù)阻塑,如果不在緩存中才到Class對(duì)象中的方法列表數(shù)組或者基類的方法列表數(shù)組中去查找蓝撇,當(dāng)找到后將方法名和方法函數(shù)地址保存到緩存中以便下次加速執(zhí)行。所以objc_msgSend函數(shù)第3部分的內(nèi)容主要實(shí)現(xiàn)的就是在Class對(duì)象的緩存哈希表中進(jìn)行對(duì)應(yīng)方法的查找:

? 3.1 函數(shù)首先將方法名op與cache中的mask進(jìn)行與操作陈莽。這個(gè)mask的值是緩存中桶的數(shù)量減1渤昌,一個(gè)類初始緩存中的桶的數(shù)量是4,每次桶數(shù)量擴(kuò)容時(shí)都乘2走搁。也就是說(shuō)mask的值的二進(jìn)制的所有bit位數(shù)全都是1独柑,這樣當(dāng)op和mask進(jìn)行與操作時(shí)也就是取op中的低mask位數(shù)來(lái)命中哈希桶中的元素。因此這個(gè)哈希算法所得到的index索引值一定是小于緩存中桶的數(shù)量而不會(huì)出現(xiàn)越界的情況私植。

?3.2 當(dāng)通過(guò)哈希算法得到對(duì)應(yīng)的索引值后忌栅,接下來(lái)便判斷對(duì)應(yīng)的桶中的key值是否和op相等。每個(gè)桶是一個(gè)struct bucket_t 結(jié)構(gòu)曲稼,里面保存這方法的名稱(key)和方法的實(shí)現(xiàn)地址(imp)索绪。一旦key值和op值相等則表明緩存命中,然后將其中的imp值進(jìn)行保存并結(jié)束查找跳出循環(huán)贫悄;而一旦key值為NULL時(shí)則表明此方法尚未被緩存瑞驱,需要跳出循環(huán)進(jìn)行方法未命中緩存處理;而當(dāng)key為非NULL但是又不等于op時(shí)則表明出現(xiàn)沖突了窄坦,這里解決沖突的機(jī)制是采用開(kāi)地址法將索引值減1來(lái)繼續(xù)循環(huán)來(lái)查找緩存唤反。

當(dāng)你讀完第3部分代碼時(shí)是否會(huì)產(chǎn)生如下幾個(gè)問(wèn)題的思考:

問(wèn)題一: 緩存中哈希桶的數(shù)量會(huì)隨著方法訪問(wèn)的數(shù)量增加而動(dòng)態(tài)增加,那么它又是如何增加的鸭津?

問(wèn)題二: 緩存循環(huán)查找是否會(huì)出現(xiàn)死循環(huán)的情況拴袭?

問(wèn)題三: 當(dāng)桶數(shù)量增加后mask的值也會(huì)跟著變化,那么就會(huì)存在著前后兩次計(jì)算index的值不一致的情況曙博,這又如何解決?

問(wèn)題四: 既然哈希桶的數(shù)量會(huì)在運(yùn)行時(shí)動(dòng)態(tài)添加那么在多線程訪問(wèn)環(huán)境下又是如何做同步和安全處理的?

這四個(gè)問(wèn)題都會(huì)在第4步中的objc_msgSend_uncached函數(shù)內(nèi)部實(shí)現(xiàn)中找到答案怜瞒。

4. 執(zhí)行方法實(shí)現(xiàn)或方法未命中緩存處理函數(shù)

當(dāng)方法在哈希桶中被命中并且存在對(duì)應(yīng)的方法函數(shù)實(shí)現(xiàn)時(shí)就會(huì)調(diào)用對(duì)應(yīng)的方法實(shí)現(xiàn)并且函數(shù)返回父泳,整個(gè)函數(shù)執(zhí)行完成。而當(dāng)方法沒(méi)有被緩存時(shí)則會(huì)調(diào)用objc_msgSend_uncached函數(shù)吴汪,這個(gè)函數(shù)的實(shí)現(xiàn)也是用匯編語(yǔ)言編寫(xiě)的惠窄,它的函數(shù)內(nèi)部做了兩件事情:一是調(diào)用_class_lookupMethodAndLoadCache3函數(shù)在Class對(duì)象中查找方法的實(shí)現(xiàn)體函數(shù)并返回;二是調(diào)用返回的實(shí)現(xiàn)體函數(shù)來(lái)執(zhí)行對(duì)應(yīng)的方法漾橙「巳冢可以從_class_lookupMethodAndLoadCache3函數(shù)名中看出它的功能實(shí)現(xiàn)就是先查找后緩存,而這個(gè)函數(shù)則是用C語(yǔ)言實(shí)現(xiàn)的霜运,因此可以很清晰的去閱讀它的源代碼實(shí)現(xiàn)脾歇。_class_lookupMethodAndLoadCache3函數(shù)的源代碼實(shí)現(xiàn)主要就是先從Class對(duì)象的方法列表或者基類的方法列表中查找對(duì)應(yīng)的方法和實(shí)現(xiàn)蒋腮,并且更新到Class對(duì)象的緩存cache中。如果你仔細(xì)閱讀里面的源代碼就可以很容易回答在第3步所提出的四個(gè)問(wèn)題:

??問(wèn)題一: 緩存中哈希桶的數(shù)量會(huì)隨著方法訪問(wèn)的數(shù)量增加而動(dòng)態(tài)增加藕各,那么它又是如何增加的池摧?

??: 每個(gè)Class類對(duì)象初始化時(shí)會(huì)為緩存分配4個(gè)桶,并且cache中有一個(gè)數(shù)據(jù)成員occupied來(lái)保存緩存中已經(jīng)使用的桶的數(shù)量激况,這樣每當(dāng)將一個(gè)方法的緩存信息保存到桶中時(shí)occupied的數(shù)量加1作彤,如果數(shù)量到達(dá)桶容量的3/4時(shí),系統(tǒng)就會(huì)將桶的容量增大2倍變乌逐,并按照這個(gè)規(guī)則依次繼續(xù)擴(kuò)展下去竭讳。

??問(wèn)題二: 緩存循環(huán)查找是否會(huì)出現(xiàn)死循環(huán)的情況?

??:不會(huì)浙踢,因?yàn)橄到y(tǒng)總是會(huì)將空桶的數(shù)量保證有1/4的空閑绢慢,因此當(dāng)循環(huán)遍歷時(shí)一定會(huì)出現(xiàn)命中緩存或者會(huì)出現(xiàn)key == NULL的情況而退出循環(huán)。

??問(wèn)題三: 當(dāng)桶數(shù)量增加后mask的值也會(huì)跟著變化成黄,那么就會(huì)存在著前后兩次計(jì)算index的值不一致的情況呐芥,這又如何解決?

??: 每次哈希桶的數(shù)量擴(kuò)容后,系統(tǒng)會(huì)為緩存分配一批新的空桶奋岁,并且不會(huì)維護(hù)原來(lái)老的緩存中的桶的信息思瘟。這樣就相當(dāng)于當(dāng)對(duì)桶數(shù)量擴(kuò)充后每個(gè)方法都是需要進(jìn)行重新緩存,所有緩存的信息都清0并重新開(kāi)始闻伶。因此不會(huì)出現(xiàn)兩次index計(jì)算不一致的問(wèn)題滨攻。

??問(wèn)題四: 既然哈希桶的數(shù)量會(huì)在運(yùn)行時(shí)動(dòng)態(tài)添加那么在多線程訪問(wèn)環(huán)境下又是如何做同步和安全處理的?

??:在整個(gè)objc_msgSend函數(shù)中對(duì)方法緩存的讀取操作并沒(méi)有增加任何的鎖和同步信息蓝翰,這樣目的是為了達(dá)到最佳的性能光绕。在多線程環(huán)境下為了保證對(duì)數(shù)據(jù)的安全和同步訪問(wèn),需要在寫(xiě)寫(xiě)和讀寫(xiě)兩種場(chǎng)景下進(jìn)行安全和同步處理:

?首先來(lái)考察多線程同時(shí)寫(xiě)cache緩存的處理方法畜份。假如兩個(gè)線程都檢測(cè)到方法并未在緩存中而需要擴(kuò)充緩存或者寫(xiě)桶數(shù)據(jù)時(shí)诞帐,在擴(kuò)充緩存和寫(xiě)桶數(shù)據(jù)之前使用了一個(gè)全局的互斥鎖來(lái)保證寫(xiě)入的同步處理,而且在鎖住的范圍內(nèi)部還做了一次查緩存的處理爆雹,這樣即使在兩個(gè)線程調(diào)用相同的方法時(shí)也不會(huì)出現(xiàn)寫(xiě)兩次緩存的情況停蕉。因此多線程同時(shí)寫(xiě)入的解決方法只需要簡(jiǎn)單的引入一個(gè)互斥鎖即可解決問(wèn)題。

?再來(lái)考察多線程同時(shí)讀寫(xiě)cache緩存的處理方法钙态。上面有提到當(dāng)對(duì)緩存中的哈希桶進(jìn)行擴(kuò)充時(shí)慧起,系統(tǒng)采用的解決方法是完全丟棄掉老緩存的內(nèi)存數(shù)據(jù),而重新開(kāi)辟一塊新的哈希桶內(nèi)存并更新Class對(duì)象cache中的所有數(shù)據(jù)成員册倒。因此如果處理不當(dāng)就會(huì)在objc_msgSend函數(shù)的第3步中訪問(wèn)cache中的數(shù)據(jù)成員時(shí)發(fā)生異常蚓挤。為了解決這個(gè)問(wèn)題在objc_msgSend函數(shù)的第四條指令中采用了一種非常巧妙的方法:

0x18378c430 <+16>:? ldp? ? x10, x11, [x16, #0x10]

這條指令中會(huì)把cache中的哈希桶buckets和mask|occupied整個(gè)結(jié)構(gòu)體數(shù)據(jù)成員分別讀取到x10和x11兩個(gè)寄存器中去。因?yàn)镃PU能保證單條指令執(zhí)行的原子性,而且在整個(gè)后續(xù)的匯編代碼中函數(shù)并沒(méi)有再次去讀取cache中的buckets和mask數(shù)據(jù)成員灿意,而是一直使用x10和x11兩個(gè)寄存器中的值來(lái)進(jìn)行哈希表的查找估灿。所以即使其他寫(xiě)線程擴(kuò)充了cache中的哈希桶的數(shù)量和重新分配了內(nèi)存也不會(huì)影響當(dāng)前讀線程的數(shù)據(jù)訪問(wèn)。在寫(xiě)入線程擴(kuò)充哈希桶數(shù)量時(shí)會(huì)更新cache中的buckets和mask兩個(gè)數(shù)據(jù)成員的值脾歧。這部分的實(shí)現(xiàn)代碼如下:

//設(shè)置更新緩存的哈希桶內(nèi)存和mask值甲捏。voidcache_t::setBucketsAndMask(structbucket_t*newBuckets,mask_t newMask){// objc_msgSend uses mask and buckets with no locks.// It is safe for objc_msgSend to see new buckets but old mask.// (It will get a cache miss but not overrun the buckets' bounds).// It is unsafe for objc_msgSend to see old buckets and new mask.// Therefore we write new buckets, wait a lot, then write new mask.// objc_msgSend reads mask first, then buckets.// ensure other threads see buckets contents before buckets pointermega_barrier();buckets=newBuckets;// ensure other threads see new buckets before new maskmega_barrier();mask=newMask;occupied=0;}

這段代碼是用C++編寫(xiě)實(shí)現(xiàn)的。代碼中先修改哈希桶數(shù)據(jù)成員buckets再修改mask中的值鞭执。為了保證賦值的順序不被編譯器優(yōu)化這里添加了mega_baerrier()來(lái)實(shí)現(xiàn)編譯內(nèi)存屏障(Compiler Memory Barrier)司顿。假如不添加編譯內(nèi)存屏障的話,編譯器有可能會(huì)優(yōu)化代碼讓mask先賦值而buckets后賦值兄纺,這樣會(huì)造成什么后果呢大溜?當(dāng)寫(xiě)線程先執(zhí)行完mask賦值并在執(zhí)行buckets賦值前讀線程執(zhí)行l(wèi)dp x10, x11, [x16, #0x10]指令時(shí)就有可能讀取到新的mask值和老的buckets值,而新的mask值要比老的mask值大估脆,這樣就會(huì)出現(xiàn)內(nèi)存數(shù)組越界的情況而產(chǎn)生崩潰钦奋。而如果添加了編譯內(nèi)存屏障,就會(huì)保證先執(zhí)行buckets賦值而后執(zhí)行mask賦值疙赠,這樣即使在寫(xiě)線程執(zhí)行完buckets賦值后而在執(zhí)行mask賦值前付材,讀線程執(zhí)行l(wèi)dp x10, x11, [x16, #0x10]時(shí)得到新的buckets值和老的mask值是也不會(huì)出現(xiàn)異常。可見(jiàn)可以在一定的程度上借助編譯內(nèi)存屏障相關(guān)的技巧來(lái)實(shí)現(xiàn)無(wú)鎖讀寫(xiě)同步技術(shù)圃阳。當(dāng)然假如這段代碼不用高級(jí)語(yǔ)言而用匯編語(yǔ)言來(lái)編寫(xiě)則可以不用編譯內(nèi)存屏障技術(shù)而是用stp指令來(lái)寫(xiě)入新的buckets和mask值也能保證順序的寫(xiě)入厌衔。

思考和實(shí)踐:如果你想了解編譯屏障相關(guān)的知識(shí)請(qǐng)參考文章https://blog.csdn.net/world_hello_100/article/details/50131497的介紹

對(duì)于多線程讀寫(xiě)的情況還有一個(gè)問(wèn)題需要解決,就是因?yàn)閷?xiě)線程對(duì)緩存進(jìn)行了擴(kuò)充而分配了新的哈希桶內(nèi)存捍岳,同時(shí)會(huì)銷毀老的哈希桶內(nèi)存富寿,而此時(shí)如果讀線程中正在訪問(wèn)的是老緩存時(shí),就有可能會(huì)因?yàn)樘幚聿划?dāng)時(shí)會(huì)發(fā)生讀內(nèi)存異常而系統(tǒng)崩潰锣夹。為了解決這個(gè)問(wèn)題系統(tǒng)將所有會(huì)訪問(wèn)到Class對(duì)象中的cache數(shù)據(jù)的6個(gè)API函數(shù)的開(kāi)始地址和結(jié)束地址保存到了兩個(gè)全局的數(shù)組中:

uintptr_t objc_entryPoints[]={cache_getImp,objc_msgSend,objc_msgSendSuper,objc_msgSendSuper2,objc_msgLookup,objc_msgLookupSuper2};//LExit開(kāi)頭的表示的是函數(shù)的結(jié)束地址页徐。uintptr_t objc_exitPoints[]={LExit_cache_getImp,LExit_objc_msgSend,LExit_objc_msgSendSuper,LExit_objc_msgSendSuper2,LExit_objc_msgLookup,LExit_objc_msgLookupSuper2};

當(dāng)某個(gè)寫(xiě)線程對(duì)Class對(duì)象cache中的哈希桶進(jìn)行擴(kuò)充時(shí),會(huì)先將已經(jīng)分配的老的需要銷毀的哈希桶內(nèi)存塊地址银萍,保存到一個(gè)全局的垃圾回收數(shù)組變量garbage_refs中变勇,然后再遍歷當(dāng)前進(jìn)程中的所有線程,并查看線程狀態(tài)中的當(dāng)前PC寄存器中的值是否在objc_entryPoints和objc_exitPoints這個(gè)范圍內(nèi)贴唇。也就是說(shuō)查看是否有線程正在執(zhí)行objc_entryPoints列表中的函數(shù)贰锁,如果沒(méi)有則表明此時(shí)沒(méi)有任何函數(shù)會(huì)訪問(wèn)Class對(duì)象中的cache數(shù)據(jù),這時(shí)候就可以放心的將全局垃圾回收數(shù)組變量garbage_refs中的所有待銷毀的哈希桶內(nèi)存塊執(zhí)行真正的銷毀操作滤蝠;而如果有任何一個(gè)線程正在執(zhí)行objc_entryPoints列表中的函數(shù)則不做處理,而等待下次再檢查并在適當(dāng)?shù)臅r(shí)候進(jìn)行銷毀授嘀。這樣也就保證了讀線程在訪問(wèn)Class對(duì)象中的cache中的buckets時(shí)不會(huì)產(chǎn)生內(nèi)存訪問(wèn)異常物咳。

思考和實(shí)踐:上面描述的技術(shù)解決方案其實(shí)就是一種垃圾回收技術(shù)的實(shí)現(xiàn)。垃圾回收時(shí)不立即將內(nèi)存進(jìn)行釋放蹄皱,而是暫時(shí)將內(nèi)存放到某處進(jìn)行統(tǒng)一管理览闰,當(dāng)滿足特定條件時(shí)才將所有分配的內(nèi)存進(jìn)行統(tǒng)一銷毀釋放處理芯肤。

objc2.0的runtime巧妙的利用了ldp指令、編譯內(nèi)存屏障技術(shù)压鉴、內(nèi)存垃圾回收技術(shù)等多種手段來(lái)解決多線程數(shù)據(jù)讀寫(xiě)的無(wú)鎖處理方案崖咨,提升了系統(tǒng)的性能,你是否get到這些技能了呢油吭?

小結(jié)

上面就是objc_msgSend函數(shù)內(nèi)部實(shí)現(xiàn)的所有要說(shuō)的東西击蹲,您是否在這篇文章中又收獲了新的知識(shí)?是否對(duì)Runtime又有了進(jìn)一步的認(rèn)識(shí)婉宰?在介紹這些東西時(shí)歌豺,還順便介紹了享元模式的相關(guān)概念,以及對(duì)指針類型數(shù)據(jù)的內(nèi)存使用優(yōu)化心包,還介紹了多線程下的無(wú)鎖讀寫(xiě)相關(guān)的實(shí)現(xiàn)技巧等等类咧。如果你喜歡這篇文章就記得為我點(diǎn)一個(gè)贊??吧

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市蟹腾,隨后出現(xiàn)的幾起案子痕惋,更是在濱河造成了極大的恐慌,老刑警劉巖娃殖,帶你破解...
    沈念sama閱讀 218,682評(píng)論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件值戳,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡珊随,警方通過(guò)查閱死者的電腦和手機(jī)述寡,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,277評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)叶洞,“玉大人鲫凶,你說(shuō)我怎么就攤上這事●帽伲” “怎么了螟炫?”我有些...
    開(kāi)封第一講書(shū)人閱讀 165,083評(píng)論 0 355
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)艺晴。 經(jīng)常有香客問(wèn)我昼钻,道長(zhǎng),這世上最難降的妖魔是什么封寞? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,763評(píng)論 1 295
  • 正文 為了忘掉前任然评,我火速辦了婚禮,結(jié)果婚禮上狈究,老公的妹妹穿的比我還像新娘碗淌。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,785評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布亿眠。 她就那樣靜靜地躺著碎罚,像睡著了一般。 火紅的嫁衣襯著肌膚如雪纳像。 梳的紋絲不亂的頭發(fā)上荆烈,一...
    開(kāi)封第一講書(shū)人閱讀 51,624評(píng)論 1 305
  • 那天,我揣著相機(jī)與錄音竟趾,去河邊找鬼憔购。 笑死,一個(gè)胖子當(dāng)著我的面吹牛潭兽,可吹牛的內(nèi)容都是我干的倦始。 我是一名探鬼主播,決...
    沈念sama閱讀 40,358評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼山卦,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼鞋邑!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起账蓉,我...
    開(kāi)封第一講書(shū)人閱讀 39,261評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤枚碗,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后铸本,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體肮雨,經(jīng)...
    沈念sama閱讀 45,722評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,900評(píng)論 3 336
  • 正文 我和宋清朗相戀三年箱玷,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了怨规。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,030評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡锡足,死狀恐怖波丰,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情舶得,我是刑警寧澤掰烟,帶...
    沈念sama閱讀 35,737評(píng)論 5 346
  • 正文 年R本政府宣布,位于F島的核電站沐批,受9級(jí)特大地震影響纫骑,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜九孩,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,360評(píng)論 3 330
  • 文/蒙蒙 一先馆、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧躺彬,春花似錦磨隘、人聲如沸缤底。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,941評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至江解,卻和暖如春设预,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背犁河。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,057評(píng)論 1 270
  • 我被黑心中介騙來(lái)泰國(guó)打工鳖枕, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人桨螺。 一個(gè)月前我還...
    沈念sama閱讀 48,237評(píng)論 3 371
  • 正文 我出身青樓宾符,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親灭翔。 傳聞我的和親對(duì)象是個(gè)殘疾皇子魏烫,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,976評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容