想要學好一個方向的編程語言,底層基礎必定是個必不可少的前提。而在 Objective-C
這個篇章中,類與對象更是基礎中的基礎,它是能讓咱們串聯起萬物的基石。算法
所以,本篇文章就來好好探索一下 類的本質,類的結構,類的懶加載概念 以及 從編譯時到運行時 到底作了什麼事情,來完全的瞭解一下它 。編程
objc源碼 .設計模式
OC類對象/實例對象/元類解析數據結構
Objective-C
是一門面向對象的編程語言。每一個對象都是其 類 的實例 , 被稱爲實例對象 . 每個對象都有一個名爲 isa 的指針,指向該對象的類。多線程
而類自己也是一個對象。爲何這麼說呢?架構
來看下源碼app
typedef struct objc_object *id;
typedef struct objc_class *Class;
struct objc_class : objc_object {
// Class ISA;
Class superclass;
cache_t cache; // formerly cache pointer and vtable
class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags
/**/
}
struct objc_object {
private:
isa_t isa;
public:
// ISA() assumes this is NOT a tagged pointer object
Class ISA();
// getIsa() allows this to be a tagged pointer object
Class getIsa();
/*...*/
}
複製代碼
首先,對象是一個 id
類型 , 也就是一個指向 objc_object
結構體的指針。而咱們看到 Class
是一個指向 objc_class
結構體指針,而 objc_class
繼承於 objc_object
, 所以,咱們說 類也是一個對象 。
其實 對象是類的實例,而類是由元類對象的實例,實例方法存儲在類中,一樣,類方法則存儲在元類中。一樣咱們能夠得出結論:Objective-C 對象都是 C 語言結構體實現的。
總結:
在
Objective-C
中,每一個對象 ( 其根本是一個objc_object
結構體指針 ) 都有一個名爲isa
的指針,指向該對象的類 ( 其根本是一個objc_class
結構體 )。每個類實際上也是一個對象 ( 由於
objc_class
繼承與objc_object
),所以每個類也能夠接受消息,即調用類方法,而接受者就是類對象isa
所指向的元類。
NSObject
與NSProxy
是兩個基類,他們都遵循了<NSObject>
協議,以此爲其繼承的子類提供了公共接口和能力。
提示 :
OBJC2
中 如下 objc_class
已經被棄用 .
struct objc_class {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
Class _Nullable super_class OBJC2_UNAVAILABLE;
const char * _Nonnull name OBJC2_UNAVAILABLE;
long version OBJC2_UNAVAILABLE;
long info OBJC2_UNAVAILABLE;
long instance_size OBJC2_UNAVAILABLE;
struct objc_ivar_list * _Nullable ivars OBJC2_UNAVAILABLE;
struct objc_method_list * _Nullable * _Nullable methodLists OBJC2_UNAVAILABLE;
struct objc_cache * _Nonnull cache OBJC2_UNAVAILABLE;
struct objc_protocol_list * _Nullable protocols OBJC2_UNAVAILABLE;
#endif
} OBJC2_UNAVAILABLE;
複製代碼
類的結構體源碼以下 :
typedef struct objc_class *Class;
struct objc_class : objc_object {
// Class ISA;
Class superclass;
cache_t cache; // formerly cache pointer and vtable
class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags
/**/
}
複製代碼
首先看到類的本質是一個結構體 , 這也是爲何咱們都說 OC 對象的本質其實是個結構體指針的緣由 .
結構體內部結構以下 :
Class ISA
:指向關聯類 , 繼承自 objc_object
. 參考 isa 的前世此生Class superclass
:父類指針 , 一樣參考上述文章中有詳細指向探索 .cache_t cache
, 方法緩存存儲數據結構 .class_data_bits_t bits
, bits
中存儲了屬性,方法等類的源數據。接下來咱們就來一一探索一下 cache_t
與 class_data_bits_t
.
struct class_data_bits_t {
uintptr_t bits;
class_rw_t* data() {
return (class_rw_t *)(bits & FAST_DATA_MASK);
}
/*.其餘一些方法省略..*/
}
typedef unsigned long uintptr_t;
// data pointer
#define FAST_DATA_MASK 0x00007ffffffffff8UL
複製代碼
點進去咱們看到 , 其實這個 bits
, 跟咱們講述 isa
( isa 的前世此生 ) 時開啓了 isa
優化的狀況下是大體相同的 .
也就是說 ,
bits
使用了 8 個字節總共 64 個二進制位來存儲更多內容 , 讀取時經過mask
進行位運算獲取所存儲指針數據 .
例如 :
class_rw_t* data() {
return (class_rw_t *)(bits & FAST_DATA_MASK);
}
複製代碼
經過 bits & FAST_DATA_MASK
獲取其中固定二進制位下的數據 , 轉化爲 class_rw_t
類型的指針 . ( 其實 objc
源碼中大多數 bits
字樣的寫法都是這種處理方法 , 也是一種優化措施 , 被稱爲 享元設計模式 ) .
class_rw_t
數據結構以下 :
struct class_rw_t {
uint32_t flags;
uint32_t version;
const class_ro_t *ro;
method_array_t methods;
property_array_t properties;
protocol_array_t protocols;
Class firstSubclass;
Class nextSiblingClass;
char *demangledName;
#if SUPPORT_INDEXED_ISA
uint32_t index;
#endif
/*...*/
}
複製代碼
這個就是咱們所熟悉的 方法列表 , 屬性列表 , 協議列表等等數據信息了 .
其中方法列表須要注意的是 : (類對象存放對象方法,元類對象存放類方法)
還有一個值得提的就是 const class_ro_t *ro;
. 其源碼以下 :
struct class_ro_t {
uint32_t flags;
uint32_t instanceStart;
uint32_t instanceSize;
#ifdef __LP64__
uint32_t reserved;
#endif
const uint8_t * ivarLayout;
const char * name;
method_list_t * baseMethodList;
protocol_list_t * baseProtocols;
const ivar_list_t * ivars;
const uint8_t * weakIvarLayout;
property_list_t *baseProperties;
}
複製代碼
能夠看到 ro
中一樣存儲了 baseMethodList
, baseProtocols
, ivars
等數據 , 可是 ro
是被 const
修飾的 , 也就是不可變 .
其結構以下圖 :
那麼 rw
與 ro
是什麼關係 , 爲何須要重複存儲呢 ?
先說結論 .
其實如名稱同樣 ,
rw
即read write
,ro
即read only
.OC
爲了動態的特性 , 在編譯器肯定並保存了一份 類的結構數據在ro
中 , 另外存儲一份在運行時加載到rw
中 , 供runtime
動態修改使用 .
ro
是不可變的 , 而rw
中methods
,properties
以及protocols
內存空間是可變的 . 這也是 已有類 爲何能夠動態添加方法 , 確不能動態添加屬性的緣由 ( 添加屬性會一樣添加成員變量 , 也就是 ivar . 而 ivar 是存儲在 ro 中的 ) .一樣分類不能添加屬性的緣由也是如此 ( 關聯屬性是單獨存儲在
ObjectAssociationMap
中的 , 跟類的原理並不同 )
首先在 從頭梳理 dyld 加載流程 中咱們提到過 , libobjc
的初始化是從 _objc_init
開始的 , 而這個函數中調用了 map_images
, load_images
, 以及 unmap_image
這三個函數 .
其實也就是 dyld
負責將應用由磁盤加載到運行內存中 , 而也是在此時註冊的類及元類的數據和內存結構 . 也就是在 map_images
中 .
提示:
那咱們來從源碼看下 .
1️⃣、 當
dyld
加載到開始連接主程序的時候 , 遞歸調用recursiveInitialization
函數 .2️⃣、 這個函數第一次執行 , 進行
libsystem
的初始化 . 會走到doInitialization
->doModInitFunctions
->libSystemInitialized
.3️⃣、
Libsystem
的初始化 , 它會調用起libdispatch_init
,libdispatch
的init
會調用_os_object_init
, 這個函數裏面調用了_objc_init
.4️⃣、
_objc_init
中註冊並保存了map_images
,load_images
,unmap_image
函數地址.5️⃣ : 註冊完畢繼續回到
recursiveInitialization
遞歸下一次調用 , 例如libobjc
, 當libobjc
來到recursiveInitialization
調用時 , 會觸發libsystem
調用到_objc_init
裏註冊好的回調函數進行調用 . 就來到了libobjc
, 調用map_images
.
void map_images(unsigned count, const char * const paths[], const struct mach_header * const mhdrs[]) {
mutex_locker_t lock(runtimeLock);
return map_images_nolock(count, paths, mhdrs);
}
void map_images_nolock(unsigned mhCount, const char * const mhPaths[], const struct mach_header * const mhdrs[]) {
if (hCount > 0) {
_read_images(hList, hCount, totalClasses, unoptimizedTotalClasses);
}
}
複製代碼
↓↓
void _read_images(header_info **hList, uint32_t hCount, int totalClasses, int unoptimizedTotalClasses)
{
for (EACH_HEADER) {
classref_t *classlist = _getObjc2ClassList(hi, &count);
for (i = 0; i < count; i++) {
Class cls = (Class)classlist[i];
Class newCls = readClass(cls, headerIsBundle, headerIsPreoptimized);
}
}
}
Class readClass(Class cls, bool headerIsBundle, bool headerIsPreoptimized) {
Class replacing = nil;
if (Class newCls = popFutureNamedClass(mangledName)) {
class_rw_t *rw = newCls->data();
const class_ro_t *old_ro = rw->ro;
memcpy(newCls, cls, sizeof(objc_class));
rw->ro = (class_ro_t *)newCls->data();
newCls->setData(rw);
freeIfMutable((char *)old_ro->name);
free((void *)old_ro);
addRemappedClass(cls, newCls);
replacing = cls;
cls = newCls;
}
}
複製代碼
結論 :
能夠看到 , 在 dyld
加載類過程當中 , 將 ro
中數據拷貝到 rw
中 , 另外在 realizeClassWithoutSwift
中也有體現 , 這裏就不貼出來了 .
而在這以前 , ro
的數據已是處理完畢的 , 也就是說類的結構體在編譯期 ro
的數據已經處理完畢 .
那麼首先編譯期 ro
數據肯定咱們如何驗證 ?
答案是明顯的 , clang
+ MachoView
ro
數據驗證新建一個類 , clang readwrite
一下 , 打開 main.cpp
. 查看到以下 :
static struct _class_ro_t _OBJC_METACLASS_RO_$_LBPerson __attribute__ ((used, section ("__DATA,__objc_const"))) = {
1, sizeof(struct _class_t), sizeof(struct _class_t),
(unsigned int)0,
0,
"LBPerson",
0,
0,
0,
0,
0,
};
static struct _class_ro_t _OBJC_CLASS_RO_$_LBPerson __attribute__ ((used, section ("__DATA,__objc_const"))) = {
0, __OFFSETOFIVAR__(struct LBPerson, name), sizeof(struct LBPerson_IMPL),
(unsigned int)0,
0,
"LBPerson",
(const struct _method_list_t *)&_OBJC_$_INSTANCE_METHODS_LBPerson,
0,
(const struct _ivar_list_t *)&_OBJC_$_INSTANCE_VARIABLES_LBPerson,
0,
(const struct _prop_list_t *)&_OBJC_$_PROP_LIST_LBPerson,
};
複製代碼
由上述 clang
中咱們獲得 , ro
是存儲在 __DATA
段 _objc_const
節中 , 使用 MachOView
打開 macho
文件看到以下 :
獲得驗證 , ro
中數據在編譯期肯定並存儲完畢 , 運行時沒法修改 .
那麼接下來 , 咱們就使用 lldb
來調試一下 , 來實際看下 類中數據的內存佈局 .
代碼準備
@interface LBPerson : NSObject{
@public
NSString *ivarName;
}
@property (nonatomic, copy) NSString *propertyName;
+ (void)testClassMethod;
- (void)testInstanceMethod;
@end
@implementation LBPerson
+ (void)testClassMethod{
NSLog(@"%s",__func__);
}
- (void)testInstanceMethod{
NSLog(@"%s",__func__);
}
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
LBPerson * person = [[LBPerson alloc] init];
person->ivarName = @"ivar";
person.propertyName = @"property";
[person propertyName];
[person testInstanceMethod];
NSLog(@"123");
}
return 0;
}
複製代碼
添加斷點到 NSLog
處 , 運行工程 .
lldb
輸入指令 p/x LBPerson.class
. 打印以下 :
0x20
個字節就是 bits
呢 ?
isa
聯合體 8
字節 , superclass
指針 8
字節 , cache_t
結構體 16
字節.struct objc_class : objc_object {
// Class ISA; // 8
Class superclass; // 8
cache_t cache; // 16
class_data_bits_t bits;
}
struct cache_t {
struct bucket_t *_buckets; // 8
mask_t _mask; // 4
mask_t _occupied; // 4
}
複製代碼
另外注意 : 要在 objc
可編譯源碼進行上述 lldb
調試 , 不然沒法強轉 class_data_bits_t
.
找到 rw
咱們就分別來看下 成員變量 , 屬性 , 方法的存儲位置 .
首先在 rw
中咱們看到並無 ivar
. 所以來到 ro
中 .
這裏咱們跟 rw
比較一下發現 , rw
與 ro
中 protocols
, properties
與 methods
內存地址是如出一轍的 . 說明了運行時 ro
讀取到 rw
中的時候 , 這三個列表是淺拷貝 .
繼續獲取 ivar
.
ivarName
, 還有自動生成的
_propertyName
. 定義屬性時自動會生成
_ + 名稱
的實例變量 .
getter
與 setter
, 這也是屬性跟成員變量的區別 .getter
與 setter
以上就是類中主要存儲數據的內容了 . 探索完 bits
. 下面咱們來看看 cache_t
, 順便了解了解方法緩存的原理 .
cache_t
做爲存儲方法緩存的數據結構 , 咱們就來探索一下方法緩存的原理 .
struct cache_t {
struct bucket_t *_buckets; // 緩存數組,即哈希桶
mask_t _mask; // 緩存數組的容量臨界值,其實是爲了 capacity 服務
mask_t _occupied; // 緩存數組中已緩存方法數量
/* ... */
}
#if __LP64__
typedef uint32_t mask_t; // x86_64 & arm64 asm are less efficient with 16-bits
#else
typedef uint16_t mask_t;
#endif
struct bucket_t {
private:
#if __arm64__
uintptr_t _imp;
SEL _sel;
#else
SEL _sel;
uintptr_t _imp;
#endif
}
複製代碼
從源碼得知 , cache_t
在 64 位下佔用 16 個字節 , 而 imp
與 sel
就是存儲在 bucket_t
結構中的.
接下來 , 咱們就使用 lldb
來實際探索一下 方法緩存的原理 .
@interface LBObj : NSObject
- (void)testFunc1;
- (void)testFunc2;
- (void)testFunc3;
@end
@implementation LBObj
- (void)testFunc1{
NSLog(@"%s",__FUNCTION__);
}
- (void)testFunc2{
NSLog(@"%s",__FUNCTION__);
}
- (void)testFunc3{
NSLog(@"%s",__FUNCTION__);
}
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
LBObj * obj = [[LBObj alloc] init];
[obj testFunc1];
[obj testFunc2];
[obj testFunc3];
}
return 0;
}
複製代碼
提示 : 在 objc 源碼中跑項目 , 不然 lldb 強轉會提示找不到 cache_t .
在對象建立前加上斷點 , 先來看下緩存桶中數據 .
過掉建立對象斷點 , 來到調用方法處 , 再次查看 .
init
的 sel
以及 imp
都已經緩存到了桶中 , 已佔用 _occupied
變爲 1 , _mask
變爲 3 .另外咱們有兩個疑問 .
1️⃣ : alloc
方法爲何沒有緩存 ?
2️⃣ : 爲何 init
方法不在 _buckets
第一個位置 ?
答 :
alloc
爲何在類的cache_t
中沒有緩存呢 ? 答案很明顯 , 類方法以及類方法的緩存都是存儲在元類中的 . 有興趣的同窗能夠去驗證一下 .爲何不是按順序存儲呢 , 這個涉及到哈希表的結構設計 , 本篇文章很少作講述了 . 熟悉的同窗應該清楚 , 另外還有
key
值編碼 , 哈希表擴容 , 以及哈希衝突的處理 , 是一個比較經典的題目 , iOS 中使用的也比較多 .
過掉斷點執行 testFunc1
. 繼續查看 .
init
以及 testFunc1
的 sel
以及 imp
都已經緩存到了桶中 , 已佔用 _occupied
變爲 2 , _mask
爲 3 .過掉斷點執行 testFunc2
. 繼續查看 .
init
、testFunc1
、testFunc2
的 sel
以及 imp
都已經緩存到了桶中 , 已佔用 _occupied
變爲 3 , _mask
爲 3 .過掉斷點執行 testFunc3
. 繼續查看 .
這裏就比較有意思了 , 以前存儲的方法緩存已被清理 , testFunc3
的 sel
以及 imp
緩存到了桶中 . 已佔用 _occupied
變爲 1 , _mask
爲 7 , 爲何會這樣呢 ? 下面咱們會詳細探索 .
實際上緩存容量並不取
_mask
屬性 , 而是調用cache_t
結構體中的mask_t capacity();
方法 .
mask_t cache_t::capacity()
{
return mask() ? mask()+1 : 0;
}
mask_t cache_t::mask()
{
return _mask;
}
複製代碼
當沒有調用過方法 ,
_mask
爲 0 , 容量capacity
也爲 0 , 當_mask
有值時 , 實際容量爲_mask + 1
.
當咱們對 cache_t
的內存結構有了深刻了解以後 , 就能夠來看下 OC 調用方法時 , 究竟是如何查找緩存的 . 當緩存到達臨界值時 , 又是如何擴容 , 或者說緩存的淘汰策略又是什麼樣的呢 ?
在 探索OC方法的本質 與 OC方法查找與消息轉發 這兩篇文章中咱們詳細探索了方法的本質和方法查找的完整流程 . 其中第一部分就是 objc_MsgSend
的彙編查找緩存部分 .
以 arm64
爲例 ( 不一樣架構下對應着不一樣的彙編指令集 ) .
ENTRY _objc_msgSend
UNWIND _objc_msgSend, NoFrame
cmp p0, #0 // nil check and tagged pointer check
#if SUPPORT_TAGGED_POINTERS
b.le LNilOrTagged // (MSB tagged pointer looks negative)
#else
b.eq LReturnZero
#endif
ldr p13, [x0] // p13 = isa
GetClassFromIsa_p16 p13 // p16 = class
LGetIsaDone:
CacheLookup NORMAL // calls imp or objc_msgSend_uncached
#if SUPPORT_TAGGED_POINTERS
LNilOrTagged:
b.eq LReturnZero // nil check
// tagged
adrp x10, _objc_debug_taggedpointer_classes@PAGE
add x10, x10, _objc_debug_taggedpointer_classes@PAGEOFF
ubfx x11, x0, #60, #4
ldr x16, [x10, x11, LSL #3]
adrp x10, _OBJC_CLASS_$___NSUnrecognizedTaggedPointer@PAGE
add x10, x10, _OBJC_CLASS_$___NSUnrecognizedTaggedPointer@PAGEOFF
cmp x10, x16
b.ne LGetIsaDone
// ext tagged
adrp x10, _objc_debug_taggedpointer_ext_classes@PAGE
add x10, x10, _objc_debug_taggedpointer_ext_classes@PAGEOFF
ubfx x11, x0, #52, #8
ldr x16, [x10, x11, LSL #3]
b LGetIsaDone
// SUPPORT_TAGGED_POINTERS
#endif
LReturnZero:
// x0 is already zero
mov x1, #0
movi d0, #0
movi d1, #0
movi d2, #0
movi d3, #0
ret
END_ENTRY _objc_msgSend
複製代碼
在有了對
isa
的詳細瞭解之後 , 看這些彙編指令仍是比較清楚的 . 不太清楚isa
的同窗能夠去查閱查閱 isa 的前世此生 .對於彙編指令不太熟悉的同窗能夠藉助
Hopper
與IDA
, 他們上面是有彙編語言還原高級僞代碼的功能 , 幫助查看 .
在整個 _objc_msgSend
彙編部分 , 其實就是查找緩存的流程 , 也就是咱們所說的快速查找流程 ( 緩存 ) . 這個流程具體步驟以下 :
ENTRY _objc_msgSend
UNWIND _objc_msgSend, NoFrame
cmp p0, #0
#if SUPPORT_TAGGED_POINTERS
LNilOrTagged:
b.eq LReturnZero // nil check
複製代碼
方法調用者便是咱們 OC
隱藏參數的第一個 , 在 arm
彙編中 , 方法的第一個參數以及返回值會存儲在 x0
寄存器中 .
所以 , 此處就是對 x0
寄存器中的消息接收者是否爲空的判斷 , 爲空就直接返回了 , 這也是爲何向空對象發送消息 , 不會崩潰也不會調用的緣由 .
#if SUPPORT_TAGGED_POINTERS
b.le LNilOrTagged // (MSB tagged pointer looks negative)
#else
b.eq LReturnZero
#endif
ldr p13, [x0] // p13 = isa
GetClassFromIsa_p16 p13 // p16 = class
複製代碼
因爲對象的根本是 objc_object
, 所以每一個對象都有一個 isa
指針 , 而經過對象獲取 isa
的時候 , 因爲 isa 優化 ( 是否爲 nonpointer_isa
以及是不是 taggedPoint ) 須要不一樣處理 .
若是這部分不太熟悉的同窗請閱讀 isa 的前世此生 , 裏面有很是詳細的講述 .
彙編代碼具體獲取 isa 以下 :
.macro GetClassFromIsa_p16 /* src */
#if SUPPORT_INDEXED_ISA
// Indexed isa
mov p16, $0 // optimistically set dst = src
tbz p16, #ISA_INDEX_IS_NPI_BIT, 1f // done if not non-pointer isa
// isa in p16 is indexed
adrp x10, _objc_indexed_classes@PAGE
add x10, x10, _objc_indexed_classes@PAGEOFF
ubfx p16, p16, #ISA_INDEX_SHIFT, #ISA_INDEX_BITS // extract index
ldr p16, [x10, p16, UXTP #PTRSHIFT] // load class from array
1:
#elif __LP64__
// 64-bit packed isa
and p16, $0, #ISA_MASK
#else
// 32-bit raw isa
mov p16, $0
#endif
.endmacro
複製代碼
這裏就獲取到了 isa
, 放在了 p16
寄存器中 .
其實跟 objc
源碼中 getIsa
中的基本差很少 , 只不過一個是用匯編 , 一個是用 c
來實現的 .
inline Class objc_object::getIsa()
{
if (!isTaggedPointer()) return ISA();
uintptr_t ptr = (uintptr_t)this;
if (isExtTaggedPointer()) {
uintptr_t slot =
(ptr >> _OBJC_TAG_EXT_SLOT_SHIFT) & _OBJC_TAG_EXT_SLOT_MASK;
return objc_tag_ext_classes[slot];
} else {
uintptr_t slot =
(ptr >> _OBJC_TAG_SLOT_SHIFT) & _OBJC_TAG_SLOT_MASK;
return objc_tag_classes[slot];
}
}
複製代碼
彙編源碼 : CacheLookup
.macro CacheLookup
// p1 = SEL, p16 = isa
ldp p10, p11, [x16, #CACHE] // p10 = buckets, p11 = occupied|mask
#if !__LP64__
and w11, w11, 0xffff // p11 = mask
#endif
and w12, w1, w11 // x12 = _cmd & mask
add p12, p10, p12, LSL #(1+PTRSHIFT)
// p12 = buckets + ((_cmd & mask) << (1+PTRSHIFT))
ldp p17, p9, [x12] // {imp, sel} = *bucket
1: cmp p9, p1 // if (bucket->sel != _cmd)
b.ne 2f // scan more
CacheHit $0 // call or return imp
2: // not hit: p12 = not-hit bucket
CheckMiss $0 // miss if bucket->sel == 0
cmp p12, p10 // wrap if bucket == buckets
b.eq 3f
ldp p17, p9, [x12, #-BUCKET_SIZE]! // {imp, sel} = *--bucket
b 1b // loop
3: // wrap: p12 = first bucket, w11 = mask
add p12, p12, w11, UXTW #(1+PTRSHIFT)
// p12 = buckets + (mask << 1+PTRSHIFT)
ldp p17, p9, [x12] // {imp, sel} = *bucket
1: cmp p9, p1 // if (bucket->sel != _cmd)
b.ne 2f // scan more
CacheHit $0 // call or return imp
2: // not hit: p12 = not-hit bucket
CheckMiss $0 // miss if bucket->sel == 0
cmp p12, p10 // wrap if bucket == buckets
b.eq 3f
ldp p17, p9, [x12, #-BUCKET_SIZE]! // {imp, sel} = *--bucket
b 1b // loop
3: // double wrap
JumpMiss $0
.endmacro
複製代碼
CacheLookup
有三種狀況 , NORMAL
, GETIMP
, LOOKUP
. 咱們這裏先走的是 NORMAL
狀況 .
根據咱們前面提到 類的結構 , 8 個字節 isa
+ 8 個字節 superClass
, 而後就是 cache_t
了 .
ldp p10, p11, [x16, #CACHE] // p10 = buckets, p11 = occupied|mask
and w12, w1, w11 // x12 = _cmd & mask
add p12, p10, p12, LSL
複製代碼
3.1 、 這裏就是偏移 16 字節 , 獲取到 cache_t
中的 buckets
和 occupied
放到 p10
, p11
中 . 而後將方法名強轉成 cache_key_t
與 mask
進行與運算 ( and 指令便是 & )
這裏稍微描述一下 , 首先咱們已經知道
mask
的值是緩存中桶的數量減 1,一個類初始緩存中的桶的數量是 4 ,每次桶數量擴容時都乘 2 。也就是說
mask
的值的二進制的全部bit
位數全都是 1,那麼方法名轉成cache_key_t
和mask
進行與操做時也就是取其中低mask
位數來命中哈希桶中的元素。所以這個哈希算法所獲得的
index
索引值必定是小於緩存中桶的數量而不會出現越界的狀況。方法查找時是這個流程 , 那麼存的時候 也必定是這樣 . 下文咱們會繼續講述 .
3.2 、 當經過哈希算法獲得對應的索引值後 , 循環遍歷比較所要查找的 方法名與桶中所存儲方法名 , 每次查找索引值 index--
.
CacheHit $0
, 直接 call or return imp
key
則 if $0 == NORMAL cbz p9, __objc_msgSend_uncached
.key
, 可是沒有對應 imp
, 哈希衝突 , index--
繼續查找 . 4️⃣、 緩存沒有查找到 調用 : __objc_msgSend_uncached
.
STATIC_ENTRY __objc_msgSend_uncached
UNWIND __objc_msgSend_uncached, FrameWithNoSaves
MethodTableLookup
TailCallFunctionPointer x17
END_ENTRY __objc_msgSend_uncached
複製代碼
MethodTableLookup
源碼以下 :
.macro MethodTableLookup
SignLR
stp fp, lr, [sp, #-16]!
mov fp, sp
/*省略*/
// receiver and selector already in x0 and x1
mov x2, x16
bl __class_lookupMethodAndLoadCache3
.endmacro
複製代碼
class_lookupMethodAndLoadCache3
就來到了 C 函數的流程 , 也就是咱們所說的 類和父類消息查找以及轉發流程 .
對這部分感興趣的同窗能夠閱讀 探索OC方法的本質 與 OC方法查找與消息轉發
這兩篇文章中咱們詳細探索了方法的本質和方法查找的完整流程 .
在知道了方法調用時 , 如何查找緩存 , 咱們來看下當沒有找到緩存 ( 也就是結束彙編查找來到了 C 函數的部分 ) 時, 對方法如何進行存儲到緩存的 . 達到臨界值時又是如何擴容以及存儲的 .
在 OC方法查找與消息轉發 中咱們說過 , 查找完本身的類以及父類的方法列表後 .
imp
, 會調用到 log_and_fill_cache
.imp
, 首先會有一次動態方法解析的機會 , 最後會來到 獲取到 resolveInstanceMethod
中返回的 imp
而且進行 cache_fill
.源碼以下 : ( 截取片斷 )
/**查找本類 */
{
Method meth = getMethodNoSuper_nolock(cls, sel);
if (meth) {
log_and_fill_cache(cls, meth->imp, sel, inst, cls);
imp = meth->imp;
goto done;
}
}
/**遍歷查找父類 */
unsigned attempts = unreasonableClassCount();
for (Class curClass = cls->superclass;
curClass != nil;
curClass = curClass->superclass)
{
if (imp) {
if (imp != (IMP)_objc_msgForward_impcache) {
// Found the method in a superclass. Cache it in this class.
log_and_fill_cache(cls, imp, sel, inst, curClass);
goto done;
}
}
/* 動態方法解析後 */
imp = (IMP)_objc_msgForward_impcache;
cache_fill(cls, sel, imp, inst);
複製代碼
log_and_fill_cache
內部也是調用的 cache_fill
, cache_fill
內部調用 cache_fill_nolock
, 所以咱們直接來到這個方法實現 .
static void cache_fill_nolock(Class cls, SEL sel, IMP imp, id receiver) {
cacheUpdateLock.assertLocked();
if (!cls->isInitialized()) return;
// 再查一次 , 確保沒有其餘線程恰好已經將該方法存儲了
if (cache_getImp(cls, sel)) return;
// 取到 cls 類/元類的 cache_t
cache_t *cache = getCache(cls);
// 新的佔用數 爲舊的 + 1
mask_t newOccupied = cache->occupied() + 1;
mask_t capacity = cache->capacity();
if (cache->isConstantEmptyCache()) {
// 檢查是不是類第一次緩存 occupied == 0 && buckets() == emptyBuckets
// cache_t 默認 爲 read-only , 替換掉 .
cache->reallocate(capacity, capacity ?: INIT_CACHE_SIZE);
}
else if (newOccupied <= capacity / 4 * 3) {
// 加上本次後新的佔用數若是不超過總容量的 四分之三 不做處理
}
else {
// 加上本次後新的佔用數超過總容量的四分之三 , 進行擴容
cache->expand();
}
// 經過本sel查找緩存 , 找到則佔用數無須增長,直接更新該 sel 對應的 imp
//沒找到則佔用數 + 1 , 並把 sel 與 imp 設置到這個 bucket 中
bucket_t *bucket = cache->find(sel, receiver);
if (bucket->sel() == 0) cache->incrementOccupied();
bucket->set<Atomic>(sel, imp);
}
複製代碼
緩存方法的填充在上述源碼註釋咱們已經解釋的很是清楚了 .
咱們來看下是如何擴容的 , 以瞭解咱們上述探索中 , 調用完 testFunc3
後 _occupied
變爲 1
, _mask
爲 7
的緣由 .
void cache_t::expand()
{
cacheUpdateLock.assertLocked();
uint32_t oldCapacity = capacity();
uint32_t newCapacity = oldCapacity ? oldCapacity*2 : INIT_CACHE_SIZE;
if ((uint32_t)(mask_t)newCapacity != newCapacity) {
newCapacity = oldCapacity;
}
reallocate(oldCapacity, newCapacity);
}
enum {
INIT_CACHE_SIZE_LOG2 = 2,
INIT_CACHE_SIZE = (1 << INIT_CACHE_SIZE_LOG2)
};
複製代碼
能夠看到 新容量有如下幾個條件 :
INIT_CACHE_SIZE
, 也就是 1 << 2
, 就是 4 .oldCapacity
不爲 0 , 則開闢老的容量的兩倍 .4
字節 , 新開闢容量從新置爲老容量 .容量處理完以後 , reallocate
, 重置哈希桶 , 這也是咱們上面調用完爲何 testFunc3
以前存儲的緩存沒了的緣由 .
🔐 1 、爲何每次擴容都要重置哈希桶 ?
🔑答 :
1️⃣ : 因爲哈希表的特性 -- 地址映射 , 當每次總表擴容時 , 全部元素的映射都會失效 , 由於總容量變了 , 下標哈希結果也會改變 .
2️⃣ : 若是須要以前全部所緩存的方法都從新存儲 , 消耗與花費有點過於大了 .
3️⃣ : 所以知足最新的方法會緩存的前提下 , 老的方法若是每次擴容須要從新存儲 , 對於設計緩存自己的意義 ( 爲了提高查找方法的效率 ) 就衝突了 .
🔐 2 、爲何緩存是在佔用了 四分之三 就進行擴容呢 ? iOS
中不少處淘汰策略都採用了 3/4
處理措施 .
🔑答 :
1️⃣ : 擴容並不必定會成功 , 這樣就算不成功仍是能保證有空間存儲 , 若是用完再擴容 , 擴容失敗那麼本次方法緩存就會存儲失敗了 , 基於最多見的
LRU
( 最近最少訪問原則 ) 來看 , 顯然最新的方法不該該存儲失敗 .2️⃣ : 綜合 空間 和 時間 的利用率以及 哈希衝突問題來看 , 3/4 顯然是最佳處理策略 .
3️⃣ : 當爲系統老是會將空桶的數量保證有
1/4
的空閒的狀況下 ,循環遍歷查找緩存時必定會出現命中緩存或者會出現key == NULL
的狀況而退出循環 , 以此來避免死循環的狀況發生 。
本部分知識文章參考
首先 , 讀取緩存並不會進行寫入操做 , 所以多個線程都是讀取緩存的狀況下 , 是不會有資源多線程安全問題的 , 所以 爲了效率考量 , 在 _objc_msgSend
彙編讀取緩存部分爲了效率沒有加鎖的處理 .
在上述 expand
擴容 , 以及 cache_fill_nolock
中第一步都有一個
cacheUpdateLock.assertLocked();
/*********************************************************************** * Lock management **********************************************************************/
mutex_t runtimeLock;
mutex_t selLock;
mutex_t cacheUpdateLock;
recursive_mutex_t loadMethodLock;
複製代碼
系統使用了一個全局的互斥鎖 , 在填充緩存時 咱們說到了寫入前會 再查一次 , 確保沒有其餘線程恰好已經將該方法存儲了 , 所以多線程寫入緩存安全問題得以確保 .
首先咱們來思考一下 , 多線程讀寫緩存會發生什麼問題 .
bucket
中 sel
對應的 imp
, 其餘線程讀取一樣會出現問題 .buckets
, mask
變大 , 而其餘線程讀取到了舊的 buckets
和新的 mask
, 就會出現越界狀況 .那麼帶着上述問題咱們來探索下 libobjc
是如何保證線程安全的同時最大化不影響性能的需求的 .
問題 1 :
答 : 🔑 爲了保證擴容清零不對其餘線程讀取有影響 爲了解決這個問題系統將全部會訪問到 Class
對象中的 cache
數據的 6 個 API
函數的開始地址和結束地址保存到了兩個全局的數組中:
extern "C" uintptr_t objc_entryPoints[];
extern "C" uintptr_t objc_exitPoints[];
複製代碼
當某個寫線程對
Class
對象cache
中的哈希桶進行擴充時,會先將已經分配的老的須要銷燬的哈希桶內存塊地址,保存到一個全局的垃圾回收數組變量garbage_refs
中.而後再遍歷當前進程中的全部線程,並查看線程狀態中的當前 PC 寄存器中的值是否在
objc_entryPoints
和objc_exitPoints
這個範圍內。也就是說查看是否有線程正在執行
objc_entryPoints
列表中的函數 .
- 若是沒有則代表此時沒有任何函數會訪問
Class
對象中的cache
數據,這時候就能夠放心的將全局垃圾回收數組變量garbage_refs
中的全部待銷燬的哈希桶內存塊執行真正的銷燬操做;- 而若是有任何一個線程正在執行
objc_entryPoints
列表中的函數則不作處理,而等待下次再檢查並在適當的時候進行銷燬。這樣也就保證了讀線程在訪問
Class
對象中的cache
中的buckets
時不會產生內存訪問異常。
問題 2 / 3 :
答 : 🔑在上述 _objc_msgSend
彙編查找緩存部分 , 咱們看到
ldp x10, x11, [x16, #CACHE] // x10 = buckets, x11 = occupied|mask
複製代碼
在整個查找緩存方法中 , 將 buckets
與 mask
( occupied 就是 mask + 1 , mask 爲 0 時 occupied 也爲 0 , 無須讀取 ) 讀取到了 x10
與 x11
寄存器中 , 後續處理都是使用的這兩個寄存器裏的值 .
而彙編指令因爲自己就是最小執行單位 . ( 原子(atom)指化學反應不可再分的基本微粒,原子在化學反應中不可分割 ) , 所以絕大多數狀況下 , 彙編指令能夠保證原子性 .
lock
指令前綴來保護指令執行過程層中的數據安全 .lock
修飾的單條編譯指令以及這些特殊的安全指令纔算是真正的原子操做lock addl $0x1 %r8d
因此 只要保證讀取 _buckets
與 _mask
到 x10 , x11
寄存器 這條指令讀取到的是互相匹配的(即要麼同時是擴容前的數據,要麼同時是擴容後的數據),那麼即便在這個過程當中該 bucket
被刪掉了 , 也不會影響本次調用 , 由於寄存器中已經存儲了 。
那麼如何保證 這條指令讀取到的 _buckets
與 _mask
是匹配的 , 不會被編譯器優化所影響呢 ?
首先咱們知道 _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;
}
複製代碼
上述代碼中註釋已經寫的很清楚了 , 利用編譯內存屏障僅僅是保證 _buckets
的賦值會優先於 _mask
的賦值,也就是說,指令
ldp x10, x11, [x16, #CACHE]
複製代碼
執行後,要麼是舊的 _mask
和 舊的 buckets
, 要麼是新的 buckets
和舊的 mask
, 都不會出現越界的狀況 , 綜上這些手段來保證了多線程讀寫緩存的安全 .
至此 , 方法的緩存查找以及緩存的讀取咱們已經探索完畢了 .
cache_t
的 buckets
哈希桶中 . 消息發送時會先查找緩存 , 找到則不會繼續方法查找和轉發的流程 .cache_t
使用 capacity
( mask = 0 ? mask + 1 : 0
)來記錄當前最大容量 .cache_t
使用 occupied
來記錄當前已經使用容量 .3/4
時 , 哈希桶會進行擴容 , 擴容容量爲當前容量的兩倍 ( 當使用超過 4 字節不擴容 ) . 擴容時會清空歷史緩存 , 只保留最新 sel
和 imp
.關於類的基礎知識 , 所涉及的知識點咱們基本上都講述完畢了 , 接下來會繼續帶來 iOS 相關底層知識文章 , 敬請關注 .