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

閱讀本文后你將會進一步了解Runtime的實現(xiàn)俱两,享元設計模式的實踐赎线,內(nèi)存數(shù)據(jù)存儲優(yōu)化,編譯內(nèi)存屏障,多線程無鎖讀寫實現(xiàn)径荔,垃圾回收等相關的技術點督禽。

objc_class(Class對象)結構簡介

熟悉OC語言的Runtime(運行時)機制以及對象方法調(diào)用機制的開發(fā)者都知道淘这,所有OC方法調(diào)用在編譯時都會轉(zhuǎn)化為對C函數(shù)objc_msgSend的調(diào)用需频。

/*下面的例子是在arm64體系下的函數(shù)調(diào)用實現(xiàn),本文中如果沒有特殊說明都是指在arm64體系下的結論*/
   // [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庫通過函數(shù)objc_msgSend以及OC對象中隱藏的isa數(shù)據(jù)成員來實現(xiàn)多態(tài)和運行時方法查找以及執(zhí)行夭坪。每個對象的isa中保存著這個對象的類對象指針鹦马,類對象是一個Class類型的數(shù)據(jù)胧谈,而Class則是一個objc_class結構體指針類型的別名,它被定義如下:

   typedef struct objc_class * Class;

雖然在對外公開暴露的頭文件#import <objc/runtime.h>中可以看到關于struct objc_class的定義荸频,但可惜的是那只是objc1.0版本的定義菱肖,而目前所運行的objc2.0版本運行時庫并沒有暴露出struct objc_class所定義的詳細內(nèi)容。

你可以在https://opensource.apple.com/source/objc4/objc4-723/中下載和查看開源的最新版本的Runtime庫源代碼试溯。Runtime庫的源代碼是用匯編和C++混合實現(xiàn)的蔑滓,你可以在頭文件objc-runtime-new.h中看到關于struct objc_class結構的詳細定義郊酒。objc_class結構體用來描述一個OC類的類信息:包括類的名字遇绞、所繼承的基類、類中定義的方法列表描述燎窘、屬性列表描述摹闽、實現(xiàn)的協(xié)議描述、定義的成員變量描述等等信息褐健。在OC中類信息也是一個對象付鹿,所以又稱類信息為Class對象。 下面是一張objc_class結構體定義的靜態(tài)類圖:

objc_class結構

圖片最左邊顯示的內(nèi)容有一個編輯錯誤蚜迅,不應該是NSObject而應該是objc_class舵匾。

objc_class結構體中的數(shù)據(jù)成員非常的多也非常的復雜,這里并不打算深入的去介紹它谁不,本文主要介紹的是objc_msgSend函數(shù)內(nèi)部的實現(xiàn)坐梯,因此在下面的代碼中將會隱藏大部分數(shù)據(jù)成員的定義,并在不改變真實結構體定義的基礎上只列出objc_msgSend方法內(nèi)部會訪問和使用到的數(shù)據(jù)成員刹帕。

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

objc_msgSend函數(shù)是所有OC方法調(diào)用的核心引擎吵血,它負責查找真實的類或者對象方法的實現(xiàn),并去執(zhí)行這些方法函數(shù)偷溺。因調(diào)用頻率是如此之高蹋辅,所以要求其內(nèi)部實現(xiàn)近可能達到最高的性能。這個函數(shù)的內(nèi)部代碼實現(xiàn)是用匯編語言來編寫的挫掏,并且其中并沒有涉及任何需要線程同步和鎖相關的代碼侦另。你可以在上面說到的開源URL鏈接中的Messengers文件夾下查看各種體系架構下的匯編語言的實現(xiàn)。

     ;這里列出的是在arm64位真機模式下的匯編代碼實現(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    

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

//下面的結構體中只列出objc_msgSend函數(shù)內(nèi)部訪問用到的那些數(shù)據(jù)結構和成員硫麻。

/*
其實SEL類型就是一個字符串指針類型,所描述的就是方法字符串指針
*/
typedef char * SEL;

/*
IMP類型就是所有OC方法的函數(shù)原型類型樊卓。
*/
typedef id (*IMP)(id self, SEL _cmd, ...); 


/*
  方法名和方法實現(xiàn)桶結構體
*/
struct bucket_t  {
    SEL  key;       //方法名稱
    IMP imp;       //方法的實現(xiàn)拿愧,imp是一個函數(shù)指針類型
};

/*
   用于加快方法執(zhí)行的緩存結構體。這個結構體其實就是一個基于開地址沖突解決法的哈希桶碌尔。
*/
struct cache_t {
    struct bucket_t *buckets;    //緩存方法的哈希桶數(shù)組指針浇辜,桶的數(shù)量 = mask + 1
    int  mask;        //桶的數(shù)量 - 1
    int  occupied;   //桶中已經(jīng)緩存的方法數(shù)量。
};

/*
    OC對象的類結構體描述表示唾戚,所有OC對象的第一個參數(shù)保存是的一個isa指針柳洋。
*/
struct objc_object {
  void *isa;
};

/*
   OC類信息結構體,這里只展示出了必要的數(shù)據(jù)成員叹坦。
*/
struct objc_class : objc_object {
    struct objc_class * superclass;   //基類信息結構體熊镣。
    cache_t cache;    //方法緩存哈希表
    //... 其他數(shù)據(jù)成員忽略。
};



/*
objc_msgSend的C語言版本偽代碼實現(xiàn).
receiver: 是調(diào)用方法的對象
op: 是要調(diào)用的方法名稱字符串
*/
id  objc_msgSend(id receiver, SEL op, ...)
{

    //1............................ 對象空值判斷募书。
    //如果傳入的對象是nil則直接返回nil
    if (receiver == nil)
        return nil;
    
   //2............................ 獲取或者構造對象的isa數(shù)據(jù)绪囱。
    void *isa = NULL;
    //如果對象的地址最高位為0則表明是普通的OC對象,否則就是Tagged Pointer類型的對象
    if ((receiver & 0x8000000000000000) == 0) {
        struct objc_object  *ocobj = (struct objc_object*) receiver;
        isa = ocobj->isa;
    }
    else { //Tagged Pointer類型的對象中沒有直接保存isa數(shù)據(jù)莹捡,所以需要特殊處理來查找對應的isa數(shù)據(jù)鬼吵。
        
        //如果對象地址的最高4位為0xF, 那么表示是一個用戶自定義擴展的Tagged Pointer類型對象
        if (((NSUInteger) receiver) >= 0xf000000000000000) {
            
            //自定義擴展的Tagged Pointer類型對象中的52-59位保存的是一個全局擴展Tagged Pointer類數(shù)組的索引值。
            int  classidx = (receiver & 0xFF0000000000000) >> 52
            isa =  objc_debug_taggedpointer_ext_classes[classidx];
        }
        else {
            
            //系統(tǒng)自帶的Tagged Pointer類型對象中的60-63位保存的是一個全局Tagged Pointer類數(shù)組的索引值篮赢。
            int classidx = ((NSUInteger) receiver) >> 60;
            isa  =  objc_debug_taggedpointer_classes[classidx];
        }
    }
    
   //因為內(nèi)存地址對齊的原因和虛擬內(nèi)存空間的約束原因齿椅,
   //以及isa定義的原因需要將isa與上0xffffffff8才能得到對象所屬的Class對象。
    struct objc_class  *cls = (struct objc_class *)(isa & 0xffffffff8);
    
   //3............................ 遍歷緩存哈希桶并查找緩存中的方法實現(xiàn)启泣。
    IMP  imp = NULL;
    //cmd與cache中的mask進行與計算得到哈希桶中的索引涣脚,來查找方法是否已經(jīng)放入緩存cache哈希桶中。
    int index =  cls->cache.mask & op;
    while (true) {
        
        //如果緩存哈希桶中命中了對應的方法實現(xiàn)寥茫,則保存到imp中并退出循環(huán)遣蚀。
        if (cls->cache.buckets[index].key == op) {
              imp = cls->cache.buckets[index].imp;
              break;
        }
        
        //方法實現(xiàn)并沒有被緩存,并且對應的桶的數(shù)據(jù)是空的就退出循環(huán)
        if (cls->cache.buckets[index].key == NULL) {
             break;
        }
        
        //如果哈希桶中對應的項已經(jīng)被占用但是又不是要執(zhí)行的方法坠敷,則通過開地址法來繼續(xù)尋找緩存該方法的桶妙同。
        if (index == 0) {
            index = cls->cache.mask;  //從尾部尋找
        }
        else {
            index--;   //索引減1繼續(xù)尋找。
        }
    } /*end while*/

   //4............................ 執(zhí)行方法實現(xiàn)或方法未命中緩存處理函數(shù)
    if (imp != NULL)
         return imp(receiver, op,  ...); //這里的... 是指傳遞給objc_msgSend的OC方法中的參數(shù)膝迎。
    else
         return objc_msgSend_uncached(receiver, op, cls, ...);
}

/*
  方法未命中緩存處理函數(shù):objc_msgSend_uncached的C語言版本偽代碼實現(xiàn)粥帚,這個函數(shù)也是用匯編語言編寫。
*/
id objc_msgSend_uncached(id receiver, SEL op, struct objc_class *cls)
{
   //這個函數(shù)很簡單就是直接調(diào)用了_class_lookupMethodAndLoadCache3 來查找方法并緩存到struct objc_class中的cache中限次,最后再返回IMP類型芒涡。
  IMP  imp =   _class_lookupMethodAndLoadCache3(receiver, op, cls);
  return imp(receiver, op, ....);
}

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

1. 對象空值判斷

首先對傳進來的方法接收者receiver進行是否為空判斷柴灯,如果是nil則函數(shù)直接返回,這也就說明了當對一個nil對象調(diào)用方法時费尽,不會產(chǎn)生崩潰赠群,也不會進入到對應的方法實現(xiàn)中去,整個過程其實什么也不會發(fā)生而是直接返回nil旱幼。

2. 獲取或者構造對象的isa數(shù)據(jù)

通常情況下每個OC對象的最開始處都有一個隱藏的數(shù)據(jù)成員isa查描,isa保存有類的描述信息,所以在執(zhí)行方法前就需要從對象處獲取到這個指針值柏卤。為了減少內(nèi)存資源的浪費冬三,蘋果提出了Tagged Pointer類型對象的概念。比如一些NSString和NSNumber類型的實例對象就會被定義為Tagged Pointer類型的對象缘缚。Tagged Pointer類型的對象采用一個跟機器字長一樣長度的整數(shù)來表示一個OC對象勾笆,而為了跟普通OC對象區(qū)分開來,每個Tagged Pointer類型對象的最高位為1而普通的OC對象的最高位為0桥滨。因此上面的代碼中如果對象receiver地址的最高位為1則會將對象當做Tagged Pointer對象來處理窝爪。從代碼實現(xiàn)中還可以看出系統(tǒng)中存在兩種類型的Tagged Pointer對象:如果是高四位全為1則是用戶自定義擴展的Tagged Pointer對象,否則就是系統(tǒng)內(nèi)置的Tagged Pointer對象齐媒。因為Tagged Pointer對象中是不可能保存一個isa的信息的蒲每,而是用Tagged Pointer類型的對象中的某些bit位來保存所屬的類信息的索引值。系統(tǒng)分別定義了兩個全局數(shù)組變量:

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

來保存所有的Tagged Pointer類型的類信息里初。對于內(nèi)置Tagged Pointer類型的對象來說啃勉,其中的高四位保存的是一個索引值忽舟,通過這個索引值可以在objc_debug_taggedpointer_classes數(shù)組中查找到對象所屬的Class對象双妨;對于自定義擴展Tagged Pointer類型的對象來說,其中的高52位到59位這8位bit保存的是一個索引值叮阅,通過這個索引值可以在objc_debug_taggedpointer_ext_classes數(shù)組中查找到對象所屬的Class對象刁品。

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

在第二步中不管是普通的OC對象還是Tagged Pointer類型的對象都需要找到對象所屬的isa信息,并進一步找到所屬的類對象眯分,只有找到了類對象才能查找到對應的方法的實現(xiàn)拌汇。

isa的內(nèi)部結構

上面的代碼實現(xiàn)中,在將isa轉(zhuǎn)化為struct objc_class 時發(fā)現(xiàn)還進行一次和0xffffffff8的與操作弊决。雖然isa是一個長度為8字節(jié)的指針值噪舀, 但是它保存的值并不一定是一個struct objc_class 對象的指針魁淳。在arm64位體系架構下的用戶進程最大可訪問的虛擬內(nèi)存地址范圍是0x0000000000 - 0x1000000000,也就是每個用戶進程的可用虛擬內(nèi)存空間是64GB与倡。同時因為一個指針類型的變量存在著內(nèi)存地址對齊的因素所以指針變量的最低3位一定是0界逛。所以將isa中保存的內(nèi)容和0xffffffff8進行與操作得到的值才是真正的對象的Class對象指針。 arm64體系架構對isa中的內(nèi)容進行了優(yōu)化設計纺座,它除了保存著Class對象的指針外息拜,還保存著諸如OC對象自身的引用計數(shù)值,對象是否被弱引用標志净响,對象是否建立了關聯(lián)對象標志该溯,對象是否正在銷毀中等等信息。如果要想更加詳細的了解isa的內(nèi)部結構請參考文章:https://blog.csdn.net/u012581760/article/details/81230721 中的介紹别惦。

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

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

一個Class對象的數(shù)據(jù)成員中有一個方法列表數(shù)組保存著這個類的所有方法的描述和實現(xiàn)的函數(shù)地址入口氯庆。如果每次方法調(diào)用時都要進行一次這樣的查找,而且當調(diào)用基類方法時扰付,還需要遍歷基類進行方法查找堤撵,這樣勢必會對性能造成非常大的損耗。為了解決這個問題系統(tǒng)為每個類建立了一個哈希表進行方法緩存(objc_class 中的數(shù)據(jù)成員cache是一個cache_t類型的對象)羽莺。這個哈希表緩存由哈希桶來實現(xiàn)实昨,每次當執(zhí)行一個方法調(diào)用時,總是優(yōu)先從這個緩存中進行方法查找盐固,如果找到則執(zhí)行緩存中保存的方法函數(shù)荒给,如果不在緩存中才到Class對象中的方法列表數(shù)組或者基類的方法列表數(shù)組中去查找,當找到后將方法名和方法函數(shù)地址保存到緩存中以便下次加速執(zhí)行刁卜。所以objc_msgSend函數(shù)第3部分的內(nèi)容主要實現(xiàn)的就是在Class對象的緩存哈希表中進行對應方法的查找:

? 3.1 函數(shù)首先將方法名op與cache中的mask進行與操作志电。這個mask的值是緩存中桶的數(shù)量減1,一個類初始緩存中的桶的數(shù)量是4蛔趴,每次桶數(shù)量擴容時都乘2挑辆。也就是說mask的值的二進制的所有bit位數(shù)全都是1,這樣當op和mask進行與操作時也就是取op中的低mask位數(shù)來命中哈希桶中的元素孝情。因此這個哈希算法所得到的index索引值一定是小于緩存中桶的數(shù)量而不會出現(xiàn)越界的情況鱼蝉。

?3.2 當通過哈希算法得到對應的索引值后,接下來便判斷對應的桶中的key值是否和op相等箫荡。每個桶是一個struct bucket_t 結構魁亦,里面保存這方法的名稱(key)和方法的實現(xiàn)地址(imp)。一旦key值和op值相等則表明緩存命中菲茬,然后將其中的imp值進行保存并結束查找跳出循環(huán)吉挣;而一旦key值為NULL時則表明此方法尚未被緩存派撕,需要跳出循環(huán)進行方法未命中緩存處理;而當key為非NULL但是又不等于op時則表明出現(xiàn)沖突了睬魂,這里解決沖突的機制是采用開地址法將索引值減1來繼續(xù)循環(huán)來查找緩存终吼。

當你讀完第3部分代碼時是否會產(chǎn)生如下幾個問題的思考:
問題一: 緩存中哈希桶的數(shù)量會隨著方法訪問的數(shù)量增加而動態(tài)增加,那么它又是如何增加的氯哮?

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

問題三: 當桶數(shù)量增加后mask的值也會跟著變化,那么就會存在著前后兩次計算index的值不一致的情況喉钢,這又如何解決?

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

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

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

當方法在哈希桶中被命中并且存在對應的方法函數(shù)實現(xiàn)時就會調(diào)用對應的方法實現(xiàn)并且函數(shù)返回肠虽,整個函數(shù)執(zhí)行完成幔戏。而當方法沒有被緩存時則會調(diào)用objc_msgSend_uncached函數(shù),這個函數(shù)的實現(xiàn)也是用匯編語言編寫的税课,它的函數(shù)內(nèi)部做了兩件事情:一是調(diào)用_class_lookupMethodAndLoadCache3函數(shù)在Class對象中查找方法的實現(xiàn)體函數(shù)并返回闲延;二是調(diào)用返回的實現(xiàn)體函數(shù)來執(zhí)行對應的方法『妫可以從_class_lookupMethodAndLoadCache3函數(shù)名中看出它的功能實現(xiàn)就是先查找后緩存垒玲,而這個函數(shù)則是用C語言實現(xiàn)的,因此可以很清晰的去閱讀它的源代碼實現(xiàn)找颓。_class_lookupMethodAndLoadCache3函數(shù)的源代碼實現(xiàn)主要就是先從Class對象的方法列表或者基類的方法列表中查找對應的方法和實現(xiàn)合愈,并且更新到Class對象的緩存cache中。如果你仔細閱讀里面的源代碼就可以很容易回答在第3步所提出的四個問題:

??問題一: 緩存中哈希桶的數(shù)量會隨著方法訪問的數(shù)量增加而動態(tài)增加击狮,那么它又是如何增加的佛析?
??: 每個Class類對象初始化時會為緩存分配4個桶,并且cache中有一個數(shù)據(jù)成員occupied來保存緩存中已經(jīng)使用的桶的數(shù)量帘不,這樣每當將一個方法的緩存信息保存到桶中時occupied的數(shù)量加1说莫,如果數(shù)量到達桶容量的3/4時,系統(tǒng)就會將桶的容量增大2倍變寞焙,并按照這個規(guī)則依次繼續(xù)擴展下去。

??問題二: 緩存循環(huán)查找是否會出現(xiàn)死循環(huán)的情況互婿?
??:不會捣郊,因為系統(tǒng)總是會將空桶的數(shù)量保證有1/4的空閑,因此當循環(huán)遍歷時一定會出現(xiàn)命中緩存或者會出現(xiàn)key == NULL的情況而退出循環(huán)慈参。

??問題三: 當桶數(shù)量增加后mask的值也會跟著變化呛牲,那么就會存在著前后兩次計算index的值不一致的情況,這又如何解決?
??: 每次哈希桶的數(shù)量擴容后驮配,系統(tǒng)會為緩存分配一批新的空桶娘扩,并且不會維護原來老的緩存中的桶的信息着茸。這樣就相當于當對桶數(shù)量擴充后每個方法都是需要進行重新緩存,所有緩存的信息都清0并重新開始琐旁。因此不會出現(xiàn)兩次index計算不一致的問題涮阔。

??問題四: 既然哈希桶的數(shù)量會在運行時動態(tài)添加那么在多線程訪問環(huán)境下又是如何做同步和安全處理的?
??:在整個objc_msgSend函數(shù)中對方法緩存的讀取操作并沒有增加任何的鎖和同步信息灰殴,這樣目的是為了達到最佳的性能敬特。在多線程環(huán)境下為了保證對數(shù)據(jù)的安全和同步訪問,需要在寫寫和讀寫兩種場景下進行安全和同步處理:
?首先來考察多線程同時寫cache緩存的處理方法牺陶。假如兩個線程都檢測到方法并未在緩存中而需要擴充緩存或者寫桶數(shù)據(jù)時伟阔,在擴充緩存和寫桶數(shù)據(jù)之前使用了一個全局的互斥鎖來保證寫入的同步處理,而且在鎖住的范圍內(nèi)部還做了一次查緩存的處理掰伸,這樣即使在兩個線程調(diào)用相同的方法時也不會出現(xiàn)寫兩次緩存的情況皱炉。因此多線程同時寫入的解決方法只需要簡單的引入一個互斥鎖即可解決問題。

?再來考察多線程同時讀寫cache緩存的處理方法狮鸭。上面有提到當對緩存中的哈希桶進行擴充時娃承,系統(tǒng)采用的解決方法是完全丟棄掉老緩存的內(nèi)存數(shù)據(jù),而重新開辟一塊新的哈希桶內(nèi)存并更新Class對象cache中的所有數(shù)據(jù)成員怕篷。因此如果處理不當就會在objc_msgSend函數(shù)的第3步中訪問cache中的數(shù)據(jù)成員時發(fā)生異常历筝。為了解決這個問題在objc_msgSend函數(shù)的第四條指令中采用了一種非常巧妙的方法:

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

這條指令中會把cache中的哈希桶buckets和mask|occupied整個結構體數(shù)據(jù)成員分別讀取到x10和x11兩個寄存器中去。因為CPU能保證單條指令執(zhí)行的原子性廊谓,而且在整個后續(xù)的匯編代碼中函數(shù)并沒有再次去讀取cache中的buckets和mask數(shù)據(jù)成員梳猪,而是一直使用x10和x11兩個寄存器中的值來進行哈希表的查找。所以即使其他寫線程擴充了cache中的哈希桶的數(shù)量和重新分配了內(nèi)存也不會影響當前讀線程的數(shù)據(jù)訪問蒸痹。在寫入線程擴充哈希桶數(shù)量時會更新cache中的buckets和mask兩個數(shù)據(jù)成員的值春弥。這部分的實現(xiàn)代碼如下:

//設置更新緩存的哈希桶內(nèi)存和mask值。
  void cache_t::setBucketsAndMask(struct bucket_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 pointer
    mega_barrier();

    buckets = newBuckets;
    
    // ensure other threads see new buckets before new mask
    mega_barrier();
    
    mask = newMask;
    occupied = 0;
}

這段代碼是用C++編寫實現(xiàn)的叠荠。代碼中先修改哈希桶數(shù)據(jù)成員buckets再修改mask中的值匿沛。為了保證賦值的順序不被編譯器優(yōu)化這里添加了mega_baerrier()來實現(xiàn)編譯內(nèi)存屏障(Compiler Memory Barrier)。假如不添加編譯內(nèi)存屏障的話榛鼎,編譯器有可能會優(yōu)化代碼讓mask先賦值而buckets后賦值逃呼,這樣會造成什么后果呢?當寫線程先執(zhí)行完mask賦值并在執(zhí)行buckets賦值前讀線程執(zhí)行ldp x10, x11, [x16, #0x10]指令時就有可能讀取到新的mask值和老的buckets值者娱,而新的mask值要比老的mask值大抡笼,這樣就會出現(xiàn)內(nèi)存數(shù)組越界的情況而產(chǎn)生崩潰。而如果添加了編譯內(nèi)存屏障黄鳍,就會保證先執(zhí)行buckets賦值而后執(zhí)行mask賦值推姻,這樣即使在寫線程執(zhí)行完buckets賦值后而在執(zhí)行mask賦值前,讀線程執(zhí)行ldp x10, x11, [x16, #0x10]時得到新的buckets值和老的mask值是也不會出現(xiàn)異常框沟。 可見可以在一定的程度上借助編譯內(nèi)存屏障相關的技巧來實現(xiàn)無鎖讀寫同步技術藏古。當然假如這段代碼不用高級語言而用匯編語言來編寫則可以不用編譯內(nèi)存屏障技術而是用stp指令來寫入新的buckets和mask值也能保證順序的寫入增炭。

思考和實踐:如果你想了解編譯屏障相關的知識請參考文章https://blog.csdn.net/world_hello_100/article/details/50131497的介紹

對于多線程讀寫的情況還有一個問題需要解決,就是因為寫線程對緩存進行了擴充而分配了新的哈希桶內(nèi)存拧晕,同時會銷毀老的哈希桶內(nèi)存隙姿,而此時如果讀線程中正在訪問的是老緩存時,就有可能會因為處理不當時會發(fā)生讀內(nèi)存異常而系統(tǒng)崩潰防症。為了解決這個問題系統(tǒng)將所有會訪問到Class對象中的cache數(shù)據(jù)的6個API函數(shù)的開始地址和結束地址保存到了兩個全局的數(shù)組中:

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

當某個寫線程對Class對象cache中的哈希桶進行擴充時,會先將已經(jīng)分配的老的需要銷毀的哈希桶內(nèi)存塊地址蔫敲,保存到一個全局的垃圾回收數(shù)組變量garbage_refs中饲嗽,然后再遍歷當前進程中的所有線程,并查看線程狀態(tài)中的當前PC寄存器中的值是否在objc_entryPoints和objc_exitPoints這個范圍內(nèi)奈嘿。也就是說查看是否有線程正在執(zhí)行objc_entryPoints列表中的函數(shù)貌虾,如果沒有則表明此時沒有任何函數(shù)會訪問Class對象中的cache數(shù)據(jù),這時候就可以放心的將全局垃圾回收數(shù)組變量garbage_refs中的所有待銷毀的哈希桶內(nèi)存塊執(zhí)行真正的銷毀操作裙犹;而如果有任何一個線程正在執(zhí)行objc_entryPoints列表中的函數(shù)則不做處理尽狠,而等待下次再檢查并在適當?shù)臅r候進行銷毀。這樣也就保證了讀線程在訪問Class對象中的cache中的buckets時不會產(chǎn)生內(nèi)存訪問異常叶圃。

思考和實踐:上面描述的技術解決方案其實就是一種垃圾回收技術的實現(xiàn)袄膏。垃圾回收時不立即將內(nèi)存進行釋放,而是暫時將內(nèi)存放到某處進行統(tǒng)一管理掺冠,當滿足特定條件時才將所有分配的內(nèi)存進行統(tǒng)一銷毀釋放處理沉馆。

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

小結

上面就是objc_msgSend函數(shù)內(nèi)部實現(xiàn)的所有要說的東西锌奴,您是否在這篇文章中又收獲了新的知識?是否對Runtime又有了進一步的認識憾股?在介紹這些東西時鹿蜀,還順便介紹了享元模式的相關概念,以及對指針類型數(shù)據(jù)的內(nèi)存使用優(yōu)化荔燎,還介紹了多線程下的無鎖讀寫相關的實現(xiàn)技巧等等耻姥。如果你喜歡這篇文章就記得為我點一個贊??吧,


歡迎大家訪問我的github地址簡書地址

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末有咨,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子蒸健,更是在濱河造成了極大的恐慌座享,老刑警劉巖婉商,帶你破解...
    沈念sama閱讀 206,311評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異渣叛,居然都是意外死亡丈秩,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,339評論 2 382
  • 文/潘曉璐 我一進店門淳衙,熙熙樓的掌柜王于貴愁眉苦臉地迎上來蘑秽,“玉大人,你說我怎么就攤上這事箫攀〕ι” “怎么了?”我有些...
    開封第一講書人閱讀 152,671評論 0 342
  • 文/不壞的土叔 我叫張陵靴跛,是天一觀的道長缀雳。 經(jīng)常有香客問我,道長梢睛,這世上最難降的妖魔是什么肥印? 我笑而不...
    開封第一講書人閱讀 55,252評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮绝葡,結果婚禮上深碱,老公的妹妹穿的比我還像新娘。我一直安慰自己藏畅,他們只是感情好敷硅,可當我...
    茶點故事閱讀 64,253評論 5 371
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著墓赴,像睡著了一般竞膳。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上诫硕,一...
    開封第一講書人閱讀 49,031評論 1 285
  • 那天坦辟,我揣著相機與錄音,去河邊找鬼章办。 笑死锉走,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的藕届。 我是一名探鬼主播挪蹭,決...
    沈念sama閱讀 38,340評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼休偶!你這毒婦竟也來了梁厉?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 36,973評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎词顾,沒想到半個月后八秃,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,466評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡肉盹,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,937評論 2 323
  • 正文 我和宋清朗相戀三年昔驱,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片上忍。...
    茶點故事閱讀 38,039評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡骤肛,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出窍蓝,到底是詐尸還是另有隱情腋颠,我是刑警寧澤,帶...
    沈念sama閱讀 33,701評論 4 323
  • 正文 年R本政府宣布它抱,位于F島的核電站秕豫,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏观蓄。R本人自食惡果不足惜混移,卻給世界環(huán)境...
    茶點故事閱讀 39,254評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望侮穿。 院中可真熱鬧歌径,春花似錦、人聲如沸亲茅。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,259評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽克锣。三九已至茵肃,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間袭祟,已是汗流浹背验残。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留巾乳,地道東北人您没。 一個月前我還...
    沈念sama閱讀 45,497評論 2 354
  • 正文 我出身青樓,卻偏偏與公主長得像胆绊,于是被迫代替她去往敵國和親氨鹏。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 42,786評論 2 345

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

  • 轉(zhuǎn)至元數(shù)據(jù)結尾創(chuàng)建: 董瀟偉压状,最新修改于: 十二月 23, 2016 轉(zhuǎn)至元數(shù)據(jù)起始第一章:isa和Class一....
    40c0490e5268閱讀 1,679評論 0 9
  • 1.ios高性能編程 (1).內(nèi)層 最小的內(nèi)層平均值和峰值(2).耗電量 高效的算法和數(shù)據(jù)結構(3).初始化時...
    歐辰_OSR閱讀 29,320評論 8 265
  • Swift1> Swift和OC的區(qū)別1.1> Swift沒有地址/指針的概念1.2> 泛型1.3> 類型嚴謹 對...
    cosWriter閱讀 11,089評論 1 32
  • 引入 眾所周知仆抵,Objective-C動態(tài)性的根源在方法的調(diào)用是通過message來實現(xiàn)的,一次發(fā)生message...
    吸血鬼de晚餐閱讀 5,413評論 8 9
  • 我們都在思考生活的意義还栓,每個人又活出不同的生活道路碌廓,拿自己的固執(zhí)去框架別人传轰,結果就生出無盡的煩惱。生活的沉沉浮浮谷婆,...
    柏拉圖女人閱讀 312評論 0 0