前言
本文注疏的源碼版本為objc-781版本
(一)從提問(wèn)開(kāi)始
拿到源碼后,也不知從何入手。后來(lái)想了想,就從一道我們常見(jiàn)的面試題開(kāi)始吧。isa指針是個(gè)啥?
從代碼中,我們可以看到如下代碼
#include "isa.h"
union isa_t {
isa_t() { }
isa_t(uintptr_t value) : bits(value) { }
Class cls;
uintptr_t bits;
#if defined(ISA_BITFIELD)
struct {
ISA_BITFIELD; // defined in isa.h
};
#endif
};
首先,isa_t就是isa指針的真正類(lèi)型,它是一個(gè)聯(lián)合體(union)而不是一個(gè)結(jié)構(gòu)體(struct)。那么這里的第一問(wèn)就來(lái)了,聯(lián)合體與結(jié)構(gòu)體啥區(qū)別?
union與struct相比,實(shí)際上更省內(nèi)存,但它的“省內(nèi)存”是靠犧牲所有字段的獨(dú)立存儲(chǔ)空間換來(lái)的。換句話說(shuō),結(jié)構(gòu)體分配內(nèi)存后,所有的字段都有獨(dú)立的存儲(chǔ)空間。而聯(lián)合體是所有字段公用一個(gè)存儲(chǔ)空間。
換句話說(shuō),當(dāng)我們?yōu)閏ls字段賦值后,bits字段的值就不存在了。反之亦然??傊瑄nion在內(nèi)存分配時(shí)會(huì)根據(jù)自身內(nèi)部所有的字段大小來(lái)進(jìn)行比較,最終union會(huì)按照內(nèi)存占用最大的字段的大小作為分配的依據(jù)。
那么在這里使用聯(lián)合體,說(shuō)明對(duì)于后面的信息傳遞而言,isa_t里的所有字段,只要有一個(gè)字段有效,就可以足夠進(jìn)行信息傳遞了。
(二)內(nèi)觀isa
下面咱們?cè)倏纯醋侄?。整個(gè)isa_t中有多個(gè)字段,分別是Class cls字段,uintptr_t bits字段以及一個(gè)宏定義的匿名結(jié)構(gòu)體。下面我們逐個(gè)看一下:
Class cls 字段
Class類(lèi)型實(shí)際上也是一個(gè)結(jié)構(gòu)體,在objc-private.h中有以下定義,足以說(shuō)明問(wèn)題
struct objc_class;
struct objc_object;
typedef struct objc_class *Class;
typedef struct objc_object *id;
可以看到,Class實(shí)際上就是objc_class的結(jié)構(gòu)體指針,而下面那句不就是常常出現(xiàn)的面試考題之一嗎 —— “OC中id類(lèi)型的實(shí)質(zhì)”。從上面的定義也可以看到了,id實(shí)際上就是objc_object結(jié)構(gòu)體的指針。
對(duì)于這兩個(gè)結(jié)構(gòu)體,后面會(huì)有詳細(xì)的闡述,此處先跳過(guò)。所以說(shuō)Class cls字段實(shí)際上就是指向objc_class的指針,其描述了當(dāng)前對(duì)象指向的類(lèi)型。
uintptr bits 字段
對(duì)于這個(gè)字段,我們要結(jié)合其下面的定義一起看,也就是如下代碼:
uintptr_t bits;
#if defined(ISA_BITFIELD)
struct {
ISA_BITFIELD; // defined in isa.h
};
#endif
};
這里看到,在bits字段下還有一套匿名的結(jié)構(gòu)體,其具體信息定義在isa.h中。我們?cè)賮?lái)具體看一下:
# if __arm64__
# define ISA_MASK 0x0000000ffffffff8ULL
# define ISA_MAGIC_MASK 0x000003f000000001ULL
# define ISA_MAGIC_VALUE 0x000001a000000001ULL
# define ISA_BITFIELD \
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)
# elif __x86_64__
# define ISA_MASK 0x00007ffffffffff8ULL
# define ISA_MAGIC_MASK 0x001f800000000001ULL
# define ISA_MAGIC_VALUE 0x001d800000000001ULL
# define ISA_BITFIELD \
uintptr_t nonpointer : 1; \
uintptr_t has_assoc : 1; \
uintptr_t has_cxx_dtor : 1; \
uintptr_t shiftcls : 44; /*MACH_VM_MAX_ADDRESS 0x7fffffe00000*/ \
uintptr_t magic : 6; \
uintptr_t weakly_referenced : 1; \
uintptr_t deallocating : 1; \
uintptr_t has_sidetable_rc : 1; \
uintptr_t extra_rc : 8
# define RC_ONE (1ULL<<56)
# define RC_HALF (1ULL<<7)
# else
# error unknown architecture for packed isa
# endif
可以看到,ISA_BITFIELD宏定義了再arm64下和x86下分配位數(shù)不一樣的一組信息。結(jié)合這些信息,以及后面會(huì)說(shuō)到的objc_object對(duì)isa進(jìn)行初始化時(shí)的代碼,我們大致對(duì)bits有了如下說(shuō)明
首先bits就是以上所有信息的存儲(chǔ)單元。換句話說(shuō),雖然bits只是一個(gè)uint_ptr類(lèi)型的值,但實(shí)際上這個(gè)值本身可以通過(guò)不同位存儲(chǔ)了不同的信息。從以上掩碼中可以看出,shiftcls字段在不同的CPU架構(gòu)中,定義了不同的長(zhǎng)度。這其實(shí)就是存儲(chǔ)當(dāng)前對(duì)象Class地址的地方。
所以,bits說(shuō)白了就是一個(gè)掩碼值。OC語(yǔ)言的設(shè)計(jì)者將其按不同的位數(shù),分別賦予了不同的信息存儲(chǔ)。這樣,一個(gè)值就可以存儲(chǔ)很多信息,大大節(jié)省了空間。
匿名struct字段
該字段實(shí)際上在bits里面已經(jīng)有所闡述,他就是一個(gè)對(duì)bits內(nèi)部具體存儲(chǔ)的信息的分段顯示。這里就不再贅述了。這里我們倒是可以好好的聊一聊上面的那些宏定義。
首先,這里要先講解一個(gè)概念,即“位域”的概念。以上面的代碼為例
uintptr_t nonpointer : 1;
這句代碼就定義了一個(gè)“位域”,它的實(shí)際意義是,在uintptr_t這個(gè)64位類(lèi)型中,nonpointer字段占一位。那么其后的所有定義都是按順序占用不同的“位數(shù)”,即has_assoc占1位、has_cxx_dtor占1位、shiftcls占44位等等。那么這些信息定義完成后,其總長(zhǎng)度必然是一個(gè)uintptr_t的長(zhǎng)度,也就是64

上圖展示了在不同CPU架構(gòu)下的isa內(nèi),bits值的各個(gè)字段分配的長(zhǎng)度。我們依次說(shuō)明一下:
nonpointer字段 占1位
該字段只占一位,只有0/1之分。其含義是標(biāo)識(shí)當(dāng)前isa是否是一個(gè)nonpointer。如果是0,則代表當(dāng)前是一個(gè)純粹的isa指針;如果是1,則代表當(dāng)前isa內(nèi)存儲(chǔ)了其他信息,而其本身指向的cls的地址需要從shiftcls字段讀取。
has_assoc字段 占1位
該字段只占一位,只有0/1之分。其含義是標(biāo)識(shí)當(dāng)前擁有該isa指針的對(duì)象(objc_object,下同)是否有關(guān)聯(lián)對(duì)象。1代表有,0代表沒(méi)有。如果是0的話,不會(huì)執(zhí)行有關(guān)關(guān)聯(lián)對(duì)象的操作。
has_cxx_dtor字段 占1位
該字段只占一位,只有0/1之分。其含義是標(biāo)識(shí)當(dāng)前擁有該isa指針的對(duì)象是否有C++或ObjC的析構(gòu)函數(shù)。1代表有,則對(duì)象在釋放時(shí)要執(zhí)行析構(gòu)邏輯;0代表沒(méi)有,則釋放時(shí)會(huì)更快一些。
shiftcls字段 x86占44位,arm64占33位
該字段在不同架構(gòu)下有不同的位數(shù),但都是在isa聯(lián)合體中占位最多的字段,該字段實(shí)際上存儲(chǔ)的是一個(gè)地址。該地址指向當(dāng)前擁有isa的對(duì)象的class-object。掩碼ISA_MASK就是獲取這段地址。
magic字段 占位6位
該字段用來(lái)幫助調(diào)試器來(lái)判斷當(dāng)前擁有該isa的對(duì)象,是一個(gè)真實(shí)的對(duì)象,還是一個(gè)沒(méi)有被初始化的空間。
weakly_referenced字段 占1位
該字段只占一位,只有0/1之分。其含義是標(biāo)識(shí)當(dāng)前對(duì)象是否有或曾經(jīng)有過(guò)ARC的弱引用對(duì)象。1代表有;0代表沒(méi)有。如果沒(méi)有的話,對(duì)象在釋放時(shí)不會(huì)執(zhí)行有關(guān)弱引用釋放相關(guān)的步驟,因此,釋放時(shí)更快。
deallocating字段
該字段只占一位,只有0/1之分。該字段就是為了標(biāo)識(shí)出當(dāng)前對(duì)象是否處于“正在釋放”的階段。
has_sidetable_rc字段 占1位
該字段只占一位,只有0/1之分。該字段是為了標(biāo)記是否用到了sidetable來(lái)記錄引用計(jì)數(shù)。當(dāng)對(duì)象引用計(jì)數(shù)大于 10 時(shí),則需要借用該變量存儲(chǔ)進(jìn)位
extra_rc字段
記錄引用次數(shù),當(dāng)表示該對(duì)象的引用計(jì)數(shù)值,實(shí)際上是引用計(jì)數(shù)值減 1, 例如,如果對(duì)象的引用計(jì)數(shù)為 10,那么 extra_rc 為 9。如果引用計(jì)數(shù)大于 10, 則需要使用到上面的 has_sidetable_rc。
以上就是對(duì)isa_t的所有字段信息的解釋。這里可以注意到,在arm64和x86中,shiftcls的位數(shù)相差較大。從其注釋中也可以發(fā)現(xiàn)
//arm64
uintptr_t shiftcls : 33; /*MACH_VM_MAX_ADDRESS 0x1000000000*/
//x86
uintptr_t shiftcls : 44; /*MACH_VM_MAX_ADDRESS 0x7fffffe00000*/
在不同的CPU架構(gòu)下,留給shiftcls的最大虛擬尋址空間時(shí)不一樣的。我們至少可以猜測(cè),arm架構(gòu)下,系統(tǒng)要占領(lǐng)的虛擬空間相較于x86會(huì)更多
一些,導(dǎo)致shiftcls可用的尋址空間被大幅壓縮。
總結(jié)
至此,我們已經(jīng)完全展示了isa指針或者說(shuō)isa_t聯(lián)合體的全貌,這部分的東西將直接影響到后面objc_object和objc_class的一些理解。后續(xù),我們會(huì)持續(xù)為objc_object和objc_class的源碼注疏。