閱讀本文後你將會進一步瞭解Runtime的實現,享元設計模式的實踐,內存數據存儲優化,編譯內存屏障,多線程無鎖讀寫實現,垃圾回收等相關的技術點。html
熟悉OC語言的Runtime(運行時)機制以及對象方法調用機制的開發者都知道,全部OC方法調用在編譯時都會轉化爲對C函數objc_msgSend的調用。git
/*下面的例子是在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結構體指針類型的別名,它被定義以下:github
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結構體定義的靜態類圖: 設計模式
圖片最左邊顯示的內容有一個編輯錯誤,不該該是NSObject而應該是objc_class。數組
objc_class結構體中的數據成員很是的多也很是的複雜,這裏並不打算深刻的去介紹它,本文主要介紹的是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語言的僞代碼:bash
//下面的結構體中只列出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個部分:數據結構
首先對傳進來的方法接收者receiver進行是否爲空判斷,若是是nil則函數直接返回,這也就說明了當對一個nil對象調用方法時,不會產生崩潰,也不會進入到對應的方法實現中去,整個過程其實什麼也不會發生而是直接返回nil。
一般狀況下每一個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轉化爲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位進行一些特定數據的存儲和設置,從而減小一些內存的浪費和開銷。
一個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函數內部實現中找到答案。
當方法在哈希桶中被命中而且存在對應的方法函數實現時就會調用對應的方法實現而且函數返回,整個函數執行完成。而當方法沒有被緩存時則會調用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地址