Runtime原始碼淺析(內部分享)
2.1 Class型別
@interface NSObject <NSObject> { #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wobjc-interface-ivars" Class isaOBJC_ISA_AVAILABILITY; #pragma clang diagnostic pop } 複製程式碼
在Runtime原始碼中,我們能發現NSObject物件只有一個 Class
型別的成員變數: isa
typedef struct objc_class *Class; 複製程式碼
Class物件其實是一個指向objc_class結構體的指標。
struct objc_object { private: isa_t isa; // 這裡省略成員變數以及方法... } 複製程式碼
Class
型別本質是個結構體,該結構體中儲存了該 NSObject
中的所有資訊。
那麼一個NSObject物件佔用多少記憶體?
NSObjcet實際上是隻有一個名為isa的指標的結構體,因此佔用一個指標變數所佔用的記憶體空間大小,如果64bit(64位架構中)佔用8個位元組,如果32bit佔用4個位元組。
2.2 Class方法
- (Class)class { return object_getClass(self); } 複製程式碼
在Runtime原始碼中,我們呼叫Class方法,其實是在呼叫 object_getClass(self)
,最終通過下面程式碼獲取結果值。
inline Class objc_object::ISA() { // 忽略其它方法 return (Class)(isa.bits & ISA_MASK); } 複製程式碼
2.3 isa.bits & ISA_MASK
union isa_t { isa_t() { } isa_t(uintptr_t value) : bits(value) { } Class cls; uintptr_t bits; }; 複製程式碼
上述原始碼可以知道, isa_t
是個聯合體。
typedef unsigned longuintptr_t; 複製程式碼
bits
是 long
型別的數值。
在 isa.h
中,可以找到 ISA_MASK
原始碼
# if __arm64__ #define ISA_MASK0x0000000ffffffff8ULL # elif __x86_64__ #define ISA_MASK0x00007ffffffffff8ULL # else #error unknown architecture for packed isa # endif 複製程式碼
可知,其實 ISA_MASK
還是個數值型別
我們可以看到class方法最終獲取的即是:
結構體 objc_object
的 isa.bits & ISA_MASK
的數值計算結果。
3.NSObject物件的isa_t
3.1 isa_t
// 精簡過的isa_t共用體 union isa_t { isa_t() { } isa_t(uintptr_t value) : bits(value) { } Class cls; uintptr_t bits; # if __arm64__ #define ISA_MASK0x0000000ffffffff8ULL #define ISA_MAGIC_MASK0x000003f000000001ULL #define ISA_MAGIC_VALUE 0x000001a000000001ULL struct { uintptr_t nonpointer: 1; uintptr_t has_assoc: 1; uintptr_t has_cxx_dtor: 1; uintptr_t shiftcls: 33; // MACH_VM_MAX_ADDRESS 0x1000000000 uintptr_t magic: 6; uintptr_t weakly_referenced : 1; uintptr_t deallocating: 1; uintptr_t has_sidetable_rc: 1; uintptr_t extra_rc: 19; #define RC_ONE(1ULL<<45) #define RC_HALF(1ULL<<18) }; #endif }; 複製程式碼
上述原始碼中 isa_t
是union(共用體)型別。可以看到共用體中有一個結構體,結構體內部分別定義了一些變數,變數後面的值代表的是該變數佔用多少個二進位制位,也就是位域技術。
原始碼中通過共用體的形式儲存了64位的值,這些值在結構體中被展示出來,通過對 bits
進行位運算而取出相應位置的值。
3.2 共用體
在進行某些演算法的C語言程式設計的時候,需要使幾種不同型別的變數存放到同一段記憶體單元中。也就是使用覆蓋技術,幾個變數互相覆蓋。這種幾個不同的變數共同佔用一段記憶體的結構,在C語言中,被稱作“共用體”型別結構,簡稱共用體,也叫聯合體。
優點:可以很大程度上節省記憶體空間。
union U1 { int n; char s[11]; double d; }; 複製程式碼
對於U1共用體,s佔11位元組,n佔4位元組,d佔8位元組,因此其至少需11位元組的空間。然而其實際大小並不是11,用運算子sizeof測試其大小為16。這是因為 記憶體對齊原則 ,11既不能被4整除,也不能被8整除。因此補充位元組到16,這樣就符合所有成員的自身對齊了。所以 聯合體的記憶體除了取最大成員記憶體外,還要保證是所有成員型別size的最小公倍數
對比類的記憶體對齊:
原則 1. 前面的地址必須是後面的地址正數倍,不是就補齊。 原則 2. 整個Struct的地址必須是最大位元組的整數倍。
@interface MXRPerson : NSObject{ int _age; } 複製程式碼
person物件的第一個地址要存放isa指標需要8個位元組,第二個地址要存放_age成員變數需要4個位元組,因此person物件就佔用16個位元組空間。
程式碼驗證:
int main(int argc, const char * argv[]) { @autoreleasepool { // 驗證記憶體地址 NSObject *obj = [[NSObject alloc] init]; NSLog(@"%zd",class_getInstanceSize([NSObject class])); NSLog(@"%zd",class_getInstanceSize([MXRPerson class])); } return 0; } // 816 複製程式碼
3.3 isa中儲存的資訊及作用
struct { // 0代表普通的指標,儲存著Class,Meta-Class物件的記憶體地址。 // 1代表優化後的使用位域儲存更多的資訊。 uintptr_t nonpointer: 1; // 是否有設定過關聯物件,如果沒有,釋放時會更快 uintptr_t has_assoc: 1; // 是否有C++解構函式,如果沒有,釋放時會更快 uintptr_t has_cxx_dtor: 1; // 儲存著Class、Meta-Class物件的記憶體地址資訊 uintptr_t shiftcls: 33; // 用於在除錯時分辨物件是否未完成初始化 uintptr_t magic: 6; // 是否有被弱引用指向過。 uintptr_t weakly_referenced : 1; // 物件是否正在釋放 uintptr_t deallocating: 1; // 引用計數器是否過大無法儲存在isa中 // 如果為1,那麼引用計數會儲存在一個叫SideTable的類的屬性中 uintptr_t has_sidetable_rc: 1; // 裡面儲存的值是引用計數器減1 uintptr_t extra_rc: 19; }; 複製程式碼
此時我們重新來看 ISA_MASK
的值 0000000ffffffff8 轉為二進位制:
可以看出 ISA_MASK
的值轉化為二進位制中有33位都為1,所以按位與的作用是可以取出這33位中的值。我們再回頭看看 isa_t
的原始碼,不難發現,這33位對應的是結構體的 shiftcls
的位域。那麼 ISA_MASK
同 shiftcls
進行按位與運算即可以取出Class或Meta-Class的值(記憶體地址的值)。
同時可以看出 ISA_MASK
最後三位的值為0,那麼任何數同 ISA_MASK
按位與運算之後,得到的最後三位必定都為0,因此任何類物件或元類物件的記憶體地址最後三位必定為0,轉化為十六進位制末位必定為8或者0。
物件的isa指標需要同 ISA_MASK
經過一次&(按位與)運算才能得出真正的Class物件地址。
程式碼驗證
- (void)viewDidLoad { [super viewDidLoad]; MXRPerson *person = [[MXRPerson alloc]init]; NSLog(@"%p",[person class]); NSLog(@"%@",person); } 複製程式碼
2019-04-24 18:21:30.424630+0800 IsaTestDemo[58799:8221193] 0x1005c8db0 (lldb) p/x person->isa (Class) $0 = 0x000001a1005c8db1 MXRPerson 複製程式碼
shiftcls
中儲存類物件地址。把轉為2進位制的例項物件isa地址與轉為2進位制的類物件地址作對比,可以看出儲存類物件地址的33位二進位制內容完全相同。

4.Class物件在記憶體中儲存的資訊
4.1 instance物件在記憶體中儲存的資訊包括
- isa指標
- 其他成員變數
4.2 class物件在記憶體中儲存的資訊主要包括
- isa指標
- superclass指標
- 類的屬性資訊(@property),類的成員變數資訊(ivar)
- 類的物件方法資訊(instance method),類的協議資訊(protocol)
4.3 class_rw_t & class_ro_t
我們發現class_rw_t中儲存著方法列表,屬性列表,協議列表等內容。
struct objc_class : objc_object { // Class ISA; Class superclass; cache_t cache;// 方法進行快取 class_data_bits_t bits;// class_rw_t * plus custom rr/alloc flags class_rw_t *data() { return bits.data(); } 複製程式碼
class_rw_t
中的methods是二維陣列的結構,並且可讀可寫,因此可以動態的新增方法,並且更加便於分類方法的新增。
struct class_rw_t { // Be warned that Symbolication knows the layout of this structure. uint32_t flags; uint32_t version; const class_ro_t *ro; method_array_t methods; property_array_t properties; protocol_array_t protocols; 複製程式碼
而class_rw_t是通過bits呼叫data方法得來的,我們來到data方法內部實現。我們可以看到,data函式內部僅僅對bits進行&FAST_DATA_MASK操作
class_rw_t* data() { return (class_rw_t *)(bits & FAST_DATA_MASK); } 複製程式碼
成員變數資訊則是儲存在class_ro_t內部中的,我們來到class_ro_t內檢視。
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; method_list_t *baseMethods() const { return baseMethodList; } }; 複製程式碼
4.4 每個類在記憶體中有且只有一個meta-class物件,在記憶體中儲存的資訊主要包括
- isa指標
- superclass指標
- 類的類方法的資訊(class method)
5.驗證物件的isa指標指向
1.當物件呼叫例項方法的時候,我們上面講到,例項方法資訊是儲存在class類物件中的,那麼要想找到例項方法,就必須找到class類物件,那麼此時isa的作用就來了
instance的isa指向class,當呼叫物件方法時,通過instance的isa找到class,最後找到物件方法的實現進行呼叫。
2.當類物件呼叫類方法的時候,同上,類方法是儲存在meta-class元類物件中的。那麼要找到類方法,就需要找到meta-class元類物件,而class類物件的isa指標就指向元類物件
class的isa指向meta-class當呼叫類方法時,通過class的isa找到meta-class,最後找到類方法的實現進行呼叫
3.當物件呼叫其父類物件方法的時候,又是怎麼找到父類物件方法的呢?,此時就需要使用到class類物件superclass指標。
當Student的instance物件要呼叫Person的物件方法時,會先通過isa找到Student的class,然後通過superclass找到Person的class,最後找到物件方法的實現進行呼叫,同樣如果Person發現自己沒有響應的物件方法,又會通過Person的superclass指標找到NSObject的class物件,去尋找響應的方法
4.當類物件呼叫父類的類方法時,就需要先通過isa指標找到meta-class,然後通過superclass去尋找響應的方法
當Student的class要呼叫Person的類方法時,會先通過isa找到Student的meta-class,然後通過superclass找到Person的meta-class,最後找到類方法的實現進行呼叫

程式碼驗證:
struct mxr_objc_class{ Class isa; }; int main(int argc, const char * argv[]) { @autoreleasepool { // 如何證明isa指標的指向真的如上面所說? NSObject *object = [[NSObject alloc] init]; Class objectClass = [NSObject class]; // 我們自己建立一個同樣的結構體並通過強制轉化拿到isa指標。 struct mxr_objc_class *objectClass2 = (__bridge struct mxr_objc_class *)(objectClass); Class objectMetaClass = object_getClass([NSObject class]); NSLog(@"%p %p %p", object, objectClass, objectMetaClass); } return 0; } 複製程式碼
驗證結果1
(lldb) p/x object->isa (Class) $0 = 0x001d800100b16141 NSObject (lldb) p/x objectClass (Class) $1 = 0x0000000100b16140 NSObject (lldb) p/x 0x00007ffffffffff8 & 0x001d800100b16141 (long) $2 = 0x0000000100b16140 複製程式碼
object-isa指標地址0x001dffff96537141經過同0x00007ffffffffff8位運算,得出objectClass的地址0x00007fff96537140
驗證結果2
我們來驗證class物件的isa指標是否同樣需要位運算計算出 meta-class
物件的地址。 以同樣的方式列印 objectClass->isa
指標時,發現無法列印。
(lldb) p/x objectClass->isa error: member reference base type 'Class' is not a structure or union 複製程式碼
為了拿到isa指標的地址,我們自己建立一個同樣的結構體並通過強制轉化拿到isa指標。
(lldb) p/x objectClass2->isa (Class) $0 = 0x001d800100b160f1 (lldb) p/x objectMetaClass (Class) $1 = 0x0000000100b160f0 (lldb) p/x 0x00007ffffffffff8 & 0x001d800100b160f1 (long) $2 = 0x0000000100b160f0 複製程式碼
objectClass2的isa指標經過位運算之後的地址是meta-class的地址。
