深入解構objc_msgSend函式的實現

歐陽大哥2013發表於2018-08-13

閱讀本文後你將會進一步瞭解Runtime的實現,享元設計模式的實踐,記憶體資料儲存優化,編譯記憶體屏障,多執行緒無鎖讀寫實現,垃圾回收等相關的技術點。

objc_class(Class物件)結構簡介

熟悉OC語言的Runtime(執行時)機制以及物件方法呼叫機制的開發者都知道,所有OC方法呼叫在編譯時都會轉化為對C函式objc_msgSend的呼叫。

/*下面的例子是在arm64體系下的函式呼叫實現,本文中如果沒有特殊說明都是指在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");
複製程式碼

系統的Runtime庫通過函式objc_msgSend以及OC物件中隱藏的isa資料成員來實現多型和執行時方法查詢以及執行。每個物件的isa中儲存著這個物件的類物件指標,類物件是一個Class型別的資料,而Class則是一個objc_class結構體指標型別的別名,它被定義如下:

   typedef struct objc_class * Class;
複製程式碼

雖然在對外公開暴露的標頭檔案#import <objc/runtime.h>中可以看到關於struct objc_class的定義,但可惜的是那只是objc1.0版本的定義,而目前所執行的objc2.0版本執行時庫並沒有暴露出struct objc_class所定義的詳細內容。

你可以在https://opensource.apple.com/source/objc4/objc4-723/中下載和檢視開源的最新版本的Runtime庫原始碼。Runtime庫的原始碼是用匯編和C++混合實現的,你可以在標頭檔案objc-runtime-new.h中看到關於struct objc_class結構的詳細定義。objc_class結構體用來描述一個OC類的類資訊:包括類的名字、所繼承的基類、類中定義的方法列表描述、屬性列表描述、實現的協議描述、定義的成員變數描述等等資訊。在OC中類資訊也是一個物件,所以又稱類資訊為Class物件。 下面是一張objc_class結構體定義的靜態類圖:

objc_class類結構圖

圖片最左邊顯示的內容有一個編輯錯誤,不應該是NSObject而應該是objc_class。

objc_class結構體中的資料成員非常的多也非常的複雜,這裡並不打算深入的去介紹它,本文主要介紹的是objc_msgSend函式內部的實現,因此在下面的程式碼中將會隱藏大部分資料成員的定義,並在不改變真實結構體定義的基礎上只列出objc_msgSend方法內部會訪問和使用到的資料成員。

objc_msgSend函式的內部實現

objc_msgSend函式是所有OC方法呼叫的核心引擎,它負責查詢真實的類或者物件方法的實現,並去執行這些方法函式。因呼叫頻率是如此之高,所以要求其內部實現近可能達到最高的效能。這個函式的內部程式碼實現是用匯編語言來編寫的,並且其中並沒有涉及任何需要執行緒同步和鎖相關的程式碼。你可以在上面說到的開源URL連結中的Messengers資料夾下檢視各種體系架構下的組合語言的實現。

     ;這裡列出的是在arm64位真機模式下的彙編程式碼實現。
    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    
複製程式碼

畢竟組合語言程式碼比較晦澀難懂,因此這裡將函式的實現反彙編成C語言的虛擬碼:

//下面的結構體中只列出objc_msgSend函式內部訪問用到的那些資料結構和成員。

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

/*
IMP型別就是所有OC方法的函式原型型別。
*/
typedef id (*IMP)(id self, SEL _cmd, ...); 


/*
  方法名和方法實現桶結構體
*/
struct bucket_t  {
    SEL  key;       //方法名稱
    IMP imp;       //方法的實現,imp是一個函式指標型別
};

/*
   用於加快方法執行的快取結構體。這個結構體其實就是一個基於開地址衝突解決法的雜湊桶。
*/
struct cache_t {
    struct bucket_t *buckets;    //快取方法的雜湊桶陣列指標,桶的數量 = mask + 1
    int  mask;        //桶的數量 - 1
    int  occupied;   //桶中已經快取的方法數量。
};

/*
    OC物件的類結構體描述表示,所有OC物件的第一個引數儲存是的一個isa指標。
*/
struct objc_object {
  void *isa;
};

/*
   OC類資訊結構體,這裡只展示出了必要的資料成員。
*/
struct objc_class : objc_object {
    struct objc_class * superclass;   //基類資訊結構體。
    cache_t cache;    //方法快取雜湊表
    //... 其他資料成員忽略。
};



/*
objc_msgSend的C語言版本虛擬碼實現.
receiver: 是呼叫方法的物件
op: 是要呼叫的方法名稱字串
*/
id  objc_msgSend(id receiver, SEL op, ...)
{

    //1............................ 物件空值判斷。
    //如果傳入的物件是nil則直接返回nil
    if (receiver == nil)
        return nil;
    
   //2............................ 獲取或者構造物件的isa資料。
    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資料,所以需要特殊處理來查詢對應的isa資料。
        
        //如果物件地址的最高4位為0xF, 那麼表示是一個使用者自定義擴充套件的Tagged Pointer型別物件
        if (((NSUInteger) receiver) >= 0xf000000000000000) {
            
            //自定義擴充套件的Tagged Pointer型別物件中的52-59位儲存的是一個全域性擴充套件Tagged Pointer類陣列的索引值。
            int  classidx = (receiver & 0xFF0000000000000) >> 52
            isa =  objc_debug_taggedpointer_ext_classes[classidx];
        }
        else {
            
            //系統自帶的Tagged Pointer型別物件中的60-63位儲存的是一個全域性Tagged Pointer類陣列的索引值。
            int classidx = ((NSUInteger) receiver) >> 60;
            isa  =  objc_debug_taggedpointer_classes[classidx];
        }
    }
    
   //因為記憶體地址對齊的原因和虛擬記憶體空間的約束原因,
   //以及isa定義的原因需要將isa與上0xffffffff8才能得到物件所屬的Class物件。
    struct objc_class  *cls = (struct objc_class *)(isa & 0xffffffff8);
    
   //3............................ 遍歷快取雜湊桶並查詢快取中的方法實現。
    IMP  imp = NULL;
    //cmd與cache中的mask進行與計算得到雜湊桶中的索引,來查詢方法是否已經放入快取cache雜湊桶中。
    int index =  cls->cache.mask & op;
    while (true) {
        
        //如果快取雜湊桶中命中了對應的方法實現,則儲存到imp中並退出迴圈。
        if (cls->cache.buckets[index].key == op) {
              imp = cls->cache.buckets[index].imp;
              break;
        }
        
        //方法實現並沒有被快取,並且對應的桶的資料是空的就退出迴圈
        if (cls->cache.buckets[index].key == NULL) {
             break;
        }
        
        //如果雜湊桶中對應的項已經被佔用但是又不是要執行的方法,則通過開地址法來繼續尋找快取該方法的桶。
        if (index == 0) {
            index = cls->cache.mask;  //從尾部尋找
        }
        else {
            index--;   //索引減1繼續尋找。
        }
    } /*end while*/

   //4............................ 執行方法實現或方法未命中快取處理函式
    if (imp != NULL)
         return imp(receiver, op,  ...); //這裡的... 是指傳遞給objc_msgSend的OC方法中的引數。
    else
         return objc_msgSend_uncached(receiver, op, cls, ...);
}

/*
  方法未命中快取處理函式:objc_msgSend_uncached的C語言版本虛擬碼實現,這個函式也是用匯編語言編寫。
*/
id objc_msgSend_uncached(id receiver, SEL op, struct objc_class *cls)
{
   //這個函式很簡單就是直接呼叫了_class_lookupMethodAndLoadCache3 來查詢方法並快取到struct objc_class中的cache中,最後再返回IMP型別。
  IMP  imp =   _class_lookupMethodAndLoadCache3(receiver, op, cls);
  return imp(receiver, op, ....);
}

複製程式碼

可以看出objc_msgSend函式的實現邏輯主要分為4個部分:

1. 物件空值判斷

首先對傳進來的方法接收者receiver進行是否為空判斷,如果是nil則函式直接返回,這也就說明了當對一個nil物件呼叫方法時,不會產生崩潰,也不會進入到對應的方法實現中去,整個過程其實什麼也不會發生而是直接返回nil。

2. 獲取或者構造物件的isa資料

通常情況下每個OC物件的最開始處都有一個隱藏的資料成員isa,isa儲存有類的描述資訊,所以在執行方法前就需要從物件處獲取到這個指標值。為了減少記憶體資源的浪費,蘋果提出了Tagged Pointer型別物件的概念。比如一些NSString和NSNumber型別的例項物件就會被定義為Tagged Pointer型別的物件。Tagged Pointer型別的物件採用一個跟機器字長一樣長度的整數來表示一個OC物件,而為了跟普通OC物件區分開來,每個Tagged Pointer型別物件的最高位為1而普通的OC物件的最高位為0。因此上面的程式碼中如果物件receiver地址的最高位為1則會將物件當做Tagged Pointer物件來處理。從程式碼實現中還可以看出系統中存在兩種型別的Tagged Pointer物件:如果是高四位全為1則是使用者自定義擴充套件的Tagged Pointer物件,否則就是系統內建的Tagged Pointer物件。因為Tagged Pointer物件中是不可能儲存一個isa的資訊的,而是用Tagged Pointer型別的物件中的某些bit位來儲存所屬的類資訊的索引值。系統分別定義了兩個全域性陣列變數:

   extern "C" { 
    extern Class objc_debug_taggedpointer_classes[16*2];
    extern Class objc_debug_taggedpointer_ext_classes[256];
}
複製程式碼

來儲存所有的Tagged Pointer型別的類資訊。對於內建Tagged Pointer型別的物件來說,其中的高四位儲存的是一個索引值,通過這個索引值可以在objc_debug_taggedpointer_classes陣列中查詢到物件所屬的Class物件;對於自定義擴充套件Tagged Pointer型別的物件來說,其中的高52位到59位這8位bit儲存的是一個索引值,通過這個索引值可以在objc_debug_taggedpointer_ext_classes陣列中查詢到物件所屬的Class物件。

思考和實踐: Tagged Pointer型別的物件中獲取isa資料的方式採用的是享元設計模式,這種設計模式在一定程度上還可以縮小一個物件佔用的記憶體尺寸。還有比如256色的點陣圖中每個畫素位置中儲存的是顏色索引值而非顏色的RGB值,從而減少了低色彩點陣圖的檔案儲存空間。儲存一個物件引用可能需要佔用8個位元組,而儲存一個索引值時可能只需要佔用1個位元組。

在第二步中不管是普通的OC物件還是Tagged Pointer型別的物件都需要找到物件所屬的isa資訊,並進一步找到所屬的類物件,只有找到了類物件才能查詢到對應的方法的實現。

isa的內部結構

上面的程式碼實現中,在將isa轉化為struct objc_class 時發現還進行一次和0xffffffff8的與操作。雖然isa是一個長度為8位元組的指標值, 但是它儲存的值並不一定是一個struct objc_class 物件的指標。在arm64位體系架構下的使用者程式最大可訪問的虛擬記憶體地址範圍是0x0000000000 - 0x1000000000,也就是每個使用者程式的可用虛擬記憶體空間是64GB。同時因為一個指標型別的變數存在著記憶體地址對齊的因素所以指標變數的最低3位一定是0。所以將isa中儲存的內容和0xffffffff8進行與操作得到的值才是真正的物件的Class物件指標。 arm64體系架構對isa中的內容進行了優化設計,它除了儲存著Class物件的指標外,還儲存著諸如OC物件自身的引用計數值,物件是否被弱引用標誌,物件是否建立了關聯物件標誌,物件是否正在銷燬中等等資訊。如果要想更加詳細的瞭解isa的內部結構請參考文章:blog.csdn.net/u012581760/… 中的介紹。

思考和實踐:對於所有指標型別的資料,我們也可以利用其中的特性來使用0-2以及36-63這兩個區段的bit位進行一些特定資料的儲存和設定,從而減少一些記憶體的浪費和開銷。

3. 遍歷快取雜湊桶並查詢快取中的方法實現

一個Class物件的資料成員中有一個方法列表陣列儲存著這個類的所有方法的描述和實現的函式地址入口。如果每次方法呼叫時都要進行一次這樣的查詢,而且當呼叫基類方法時,還需要遍歷基類進行方法查詢,這樣勢必會對效能造成非常大的損耗。為了解決這個問題系統為每個類建立了一個雜湊表進行方法快取**(objc_class 中的資料成員cache是一個cache_t型別的物件)**。這個雜湊表快取由雜湊桶來實現,每次當執行一個方法呼叫時,總是優先從這個快取中進行方法查詢,如果找到則執行快取中儲存的方法函式,如果不在快取中才到Class物件中的方法列表陣列或者基類的方法列表陣列中去查詢,當找到後將方法名和方法函式地址儲存到快取中以便下次加速執行。所以objc_msgSend函式第3部分的內容主要實現的就是在Class物件的快取雜湊表中進行對應方法的查詢:

☛ 3.1 函式首先將方法名op與cache中的mask進行與操作。這個mask的值是快取中桶的數量減1,一個類初始快取中的桶的數量是4,每次桶數量擴容時都乘2。也就是說mask的值的二進位制的所有bit位數全都是1,這樣當op和mask進行與操作時也就是取op中的低mask位數來命中雜湊桶中的元素。因此這個雜湊演算法所得到的index索引值一定是小於快取中桶的數量而不會出現越界的情況。

☛3.2 當通過雜湊演算法得到對應的索引值後,接下來便判斷對應的桶中的key值是否和op相等。每個桶是一個struct bucket_t 結構,裡面儲存這方法的名稱(key)和方法的實現地址(imp)。一旦key值和op值相等則表明快取命中,然後將其中的imp值進行儲存並結束查詢跳出迴圈;而一旦key值為NULL時則表明此方法尚未被快取,需要跳出迴圈進行方法未命中快取處理;而當key為非NULL但是又不等於op時則表明出現衝突了,這裡解決衝突的機制是採用開地址法將索引值減1來繼續迴圈來查詢快取。

當你讀完第3部分程式碼時是否會產生如下幾個問題的思考: 問題一: 快取中雜湊桶的數量會隨著方法訪問的數量增加而動態增加,那麼它又是如何增加的?

問題二: 快取迴圈查詢是否會出現死迴圈的情況?

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

問題四: 既然雜湊桶的數量會在執行時動態新增那麼在多執行緒訪問環境下又是如何做同步和安全處理的?

這四個問題都會在第4步中的objc_msgSend_uncached函式內部實現中找到答案。

4. 執行方法實現或方法未命中快取處理函式

當方法在雜湊桶中被命中並且存在對應的方法函式實現時就會呼叫對應的方法實現並且函式返回,整個函式執行完成。而當方法沒有被快取時則會呼叫objc_msgSend_uncached函式,這個函式的實現也是用匯編語言編寫的,它的函式內部做了兩件事情:一是呼叫_class_lookupMethodAndLoadCache3函式在Class物件中查詢方法的實現體函式並返回;二是呼叫返回的實現體函式來執行對應的方法。可以從_class_lookupMethodAndLoadCache3函式名中看出它的功能實現就是先查詢後快取,而這個函式則是用C語言實現的,因此可以很清晰的去閱讀它的原始碼實現。_class_lookupMethodAndLoadCache3函式的原始碼實現主要就是先從Class物件的方法列表或者基類的方法列表中查詢對應的方法和實現,並且更新到Class物件的快取cache中。如果你仔細閱讀裡面的原始碼就可以很容易回答在第3步所提出的四個問題:

?問題一: 快取中雜湊桶的數量會隨著方法訪問的數量增加而動態增加,那麼它又是如何增加的? ?: 每個Class類物件初始化時會為快取分配4個桶,並且cache中有一個資料成員occupied來儲存快取中已經使用的桶的數量,這樣每當將一個方法的快取資訊儲存到桶中時occupied的數量加1,如果數量到達桶容量的3/4時,系統就會將桶的容量增大2倍變,並按照這個規則依次繼續擴充套件下去。

?問題二: 快取迴圈查詢是否會出現死迴圈的情況? ?:不會,因為系統總是會將空桶的數量保證有1/4的空閒,因此當迴圈遍歷時一定會出現命中快取或者會出現key == NULL的情況而退出迴圈。

?問題三: 當桶數量增加後mask的值也會跟著變化,那麼就會存在著前後兩次計算index的值不一致的情況,這又如何解決? ?: 每次雜湊桶的數量擴容後,系統會為快取分配一批新的空桶,並且不會維護原來老的快取中的桶的資訊。這樣就相當於當對桶數量擴充後每個方法都是需要進行重新快取,所有快取的資訊都清0並重新開始。因此不會出現兩次index計算不一致的問題。

?問題四: 既然雜湊桶的數量會在執行時動態新增那麼在多執行緒訪問環境下又是如何做同步和安全處理的? ?:在整個objc_msgSend函式中對方法快取的讀取操作並沒有增加任何的鎖和同步資訊,這樣目的是為了達到最佳的效能。在多執行緒環境下為了保證對資料的安全和同步訪問,需要在寫寫和讀寫兩種場景下進行安全和同步處理: ☞首先來考察多執行緒同時寫cache快取的處理方法。假如兩個執行緒都檢測到方法並未在快取中而需要擴充快取或者寫桶資料時,在擴充快取和寫桶資料之前使用了一個全域性的互斥鎖來保證寫入的同步處理,而且在鎖住的範圍內部還做了一次查快取的處理,這樣即使在兩個執行緒呼叫相同的方法時也不會出現寫兩次快取的情況。因此多執行緒同時寫入的解決方法只需要簡單的引入一個互斥鎖即可解決問題。

☞再來考察多執行緒同時讀寫cache快取的處理方法。上面有提到當對快取中的雜湊桶進行擴充時,系統採用的解決方法是完全丟棄掉老快取的記憶體資料,而重新開闢一塊新的雜湊桶記憶體並更新Class物件cache中的所有資料成員。因此如果處理不當就會在objc_msgSend函式的第3步中訪問cache中的資料成員時發生異常。為了解決這個問題在objc_msgSend函式的第四條指令中採用了一種非常巧妙的方法:

 0x18378c430 <+16>:  ldp    x10, x11, [x16, #0x10]
複製程式碼

這條指令中會把cache中的雜湊桶buckets和mask|occupied整個結構體資料成員分別讀取到x10和x11兩個暫存器中去。因為CPU能保證單條指令執行的原子性,而且在整個後續的彙編程式碼中函式並沒有再次去讀取cache中的buckets和mask資料成員,而是一直使用x10和x11兩個暫存器中的值來進行雜湊表的查詢。所以即使其他寫執行緒擴充了cache中的雜湊桶的數量和重新分配了記憶體也不會影響當前讀執行緒的資料訪問。在寫入執行緒擴充雜湊桶數量時會更新cache中的buckets和mask兩個資料成員的值。這部分的實現程式碼如下:

//設定更新快取的雜湊桶記憶體和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++編寫實現的。程式碼中先修改雜湊桶資料成員buckets再修改mask中的值。為了保證賦值的順序不被編譯器優化這裡新增了mega_baerrier()來實現**編譯記憶體屏障(Compiler Memory Barrier)**。假如不新增編譯記憶體屏障的話,編譯器有可能會優化程式碼讓mask先賦值而buckets後賦值,這樣會造成什麼後果呢?當寫執行緒先執行完mask賦值並在執行buckets賦值前讀執行緒執行ldp x10, x11, [x16, #0x10]指令時就有可能讀取到新的mask值和老的buckets值,而新的mask值要比老的mask值大,這樣就會出現記憶體陣列越界的情況而產生崩潰。而如果新增了編譯記憶體屏障,就會保證先執行buckets賦值而後執行mask賦值,這樣即使在寫執行緒執行完buckets賦值後而在執行mask賦值前,讀執行緒執行ldp x10, x11, [x16, #0x10]時得到新的buckets值和老的mask值是也不會出現異常。 可見可以在一定的程度上藉助編譯記憶體屏障相關的技巧來實現無鎖讀寫同步技術。當然假如這段程式碼不用高階語言而用匯編語言來編寫則可以不用編譯記憶體屏障技術而是用stp指令來寫入新的buckets和mask值也能實現無鎖的讀寫。

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

對於多執行緒讀寫的情況還有一個問題需要解決,就是因為寫執行緒對快取進行了擴充而分配了新的雜湊桶記憶體,同時會銷燬老的雜湊桶記憶體,而此時如果讀執行緒中正在訪問的是老快取時,就有可能會因為處理不當時會發生讀記憶體異常而系統崩潰。為了解決這個問題系統將所有會訪問到Class物件中的cache資料的6個API函式的開始地址和結束地址儲存到了兩個全域性的陣列中:

 uintptr_t objc_entryPoints[] = {cache_getImp, objc_msgSend, objc_msgSendSuper, objc_msgSendSuper2, objc_msgLookup, objc_msgLookupSuper2};
//LExit開頭的表示的是函式的結束地址。
 uintptr_t objc_exitPoints[] = {LExit_cache_getImp,LExit_objc_msgSend, LExit_objc_msgSendSuper, LExit_objc_msgSendSuper2, LExit_objc_msgLookup,LExit_objc_msgLookupSuper2};
複製程式碼

當某個寫執行緒對Class物件cache中的雜湊桶進行擴充時,會先將已經分配的老的需要銷燬的雜湊桶記憶體塊地址,儲存到一個全域性的垃圾回收陣列變數garbage_refs中,然後再遍歷當前程式中的所有執行緒,並檢視執行緒狀態中的當前PC暫存器中的值是否在objc_entryPoints和objc_exitPoints這個範圍內。也就是說檢視是否有執行緒正在執行objc_entryPoints列表中的函式,如果沒有則表明此時沒有任何函式會訪問Class物件中的cache資料,這時候就可以放心的將全域性垃圾回收陣列變數garbage_refs中的所有待銷燬的雜湊桶記憶體塊執行真正的銷燬操作;而如果有任何一個執行緒正在執行objc_entryPoints列表中的函式則不做處理,而等待下次再檢查並在適當的時候進行銷燬。這樣也就保證了讀執行緒在訪問Class物件中的cache中的buckets時不會產生記憶體訪問異常。

思考和實踐:上面描述的技術解決方案其實就是一種垃圾回收技術的實現。垃圾回收時不立即將記憶體進行釋放,而是暫時將記憶體放到某處進行統一管理,當滿足特定條件時才將所有分配的記憶體進行統一銷燬釋放處理。

objc2.0的runtime巧妙的利用了ldp指令、編譯記憶體屏障技術、記憶體垃圾回收技術等多種手段來解決多執行緒資料讀寫的無鎖處理方案,提升了系統的效能,你是否get到這些技能了呢?

小結

上面就是objc_msgSend函式內部實現的所有要說的東西,您是否在這篇文章中又收穫了新的知識?是否對Runtime又有了進一步的認識?在介紹這些東西時,還順便介紹了享元模式的相關概念,以及對指標型別資料的記憶體使用優化,還介紹了多執行緒下的無鎖讀寫相關的實現技巧等等。如果你喜歡這篇文章就記得為我點一個贊?吧,


歡迎大家訪問我的github地址

相關文章