從iOS底層之a(chǎn)lloc、init探究這篇文章,我們可以知道,alloc一個(gè)對(duì)象的過程,主要是計(jì)算所需內(nèi)存大小cls->instanceSize、申請(qǐng)內(nèi)存空間calloc、將isa與類進(jìn)行關(guān)聯(lián)obj->initInstanceIsa。
??那么指針和類是怎么關(guān)聯(lián)的呢?
isa到底是什么結(jié)構(gòu)?保存了什么信息?下面來一一解惑。
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
BKPerson *objc = [BKPerson alloc];
NSLog(@"Hello, World! %@",objc);
}
return 0;
}
從alloc的源碼跟進(jìn)去到關(guān)聯(lián)指針和類的步驟:
if (!zone && fast) {
obj->initInstanceIsa(cls, hasCxxDtor);
} else {
// Use raw pointer isa on the assumption that they might be
// doing something weird with the zone or RR.
obj->initIsa(cls);
}
跟進(jìn)obj->initInstanceIsa(cls, hasCxxDtor);
inline void
objc_object::initInstanceIsa(Class cls, bool hasCxxDtor)
{
ASSERT(!cls->instancesRequireRawIsa());
ASSERT(hasCxxDtor == cls->hasCxxDtor());
initIsa(cls, true, hasCxxDtor);
}
主要做的事情是initIsa(cls, true, hasCxxDtor);
inline void
objc_object::initIsa(Class cls, bool nonpointer, bool hasCxxDtor)
{
ASSERT(!isTaggedPointer());
if (!nonpointer) {
isa = isa_t((uintptr_t)cls);
} else {
ASSERT(!DisableNonpointerIsa);
ASSERT(!cls->instancesRequireRawIsa());
isa_t newisa(0);
#if SUPPORT_INDEXED_ISA
ASSERT(cls->classArrayIndex() > 0);
newisa.bits = ISA_INDEX_MAGIC_VALUE;
// isa.magic is part of ISA_MAGIC_VALUE
// isa.nonpointer is part of ISA_MAGIC_VALUE
newisa.has_cxx_dtor = hasCxxDtor;
newisa.indexcls = (uintptr_t)cls->classArrayIndex();
#else
newisa.bits = ISA_MAGIC_VALUE;
// isa.magic is part of ISA_MAGIC_VALUE
// isa.nonpointer is part of ISA_MAGIC_VALUE
newisa.has_cxx_dtor = hasCxxDtor;
newisa.shiftcls = (uintptr_t)cls >> 3;
#endif
// This write must be performed in a single store in some cases
// (for example when realizing a class because other threads
// may simultaneously try to use the class).
// fixme use atomics here to guarantee single-store and to
// guarantee memory order w.r.t. the class index table
// ...but not too atomic because we don't want to hurt instantiation
isa = newisa;
}
}
可以看到不管!nonpointer條件是否滿足,都會(huì)生成一個(gè)isa_t的類型。跟進(jìn)去可以發(fā)現(xiàn):
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
};
這個(gè)isa_t是一個(gè)union聯(lián)合體,Class cls代表isa關(guān)聯(lián)的類的類型,uintptr_t bits是一段保存著isa指針優(yōu)化、是否關(guān)聯(lián)對(duì)象標(biāo)志位、對(duì)象是否有析構(gòu)函數(shù)、類相關(guān)信息、引用計(jì)數(shù)等信息的8字節(jié)大小的無符號(hào)長整型數(shù)據(jù)。要知道聯(lián)合體和結(jié)構(gòu)體的區(qū)別是:
結(jié)構(gòu)體(
struct)中所有變量是“共存”的——優(yōu)點(diǎn)是“有容乃大”, 全面;缺點(diǎn)是struct內(nèi)存空間的分配是粗放的,不管用不用,全分配。結(jié)構(gòu)體的類型大小大于等于內(nèi)部所有變量的類型大小總和,最終的類型大小是最大成員的類型大小的倍數(shù),不足補(bǔ)齊。聯(lián)合體(
union)中是各變量是“互斥”的——缺點(diǎn)就是不夠“包容”; 但優(yōu)點(diǎn)是內(nèi)存使用更為精細(xì)靈活,也節(jié)省了內(nèi)存空間。聯(lián)合體類型的大小等于最大成員類型大小。
就是說聯(lián)合體采用內(nèi)存覆蓋機(jī)制,只有一塊變量存儲(chǔ)區(qū),只能存一個(gè)變量的值,新的成員賦值會(huì)把原本存儲(chǔ)的成員信息替換掉。也就是Class cls和uintptr_t bits只能set賦值其中一個(gè)。而內(nèi)存使用的精細(xì)靈活,體現(xiàn)在以位域(即二進(jìn)制中每一位均可表示不同的信息)存儲(chǔ)成員數(shù)據(jù),也就是以計(jì)算機(jī)二進(jìn)制存儲(chǔ)的方式,位bit為單位,用1和0標(biāo)記數(shù)據(jù),數(shù)據(jù)的尺寸大小是以占用多少bit,而不是以每個(gè)數(shù)據(jù)成員數(shù)據(jù)類型的尺寸大小(多少字節(jié)byte)存儲(chǔ)。isa的bits占用的內(nèi)存大小是8字節(jié),即64位,可以存儲(chǔ)足夠多的信息,很大節(jié)省了內(nèi)存。
isa的bits成員的位域,定義在isa.h源文件中
struct {
ISA_BITFIELD; // defined in isa.h
};
ISA_BITFIELD是一個(gè)宏定義,分別在x86_64架構(gòu)和arm64架構(gòu)是這樣的:

bits的64位存儲(chǔ)分布圖:

其中存儲(chǔ)的成員信息:
-
nonpointer是否開啟isa指針優(yōu)化。上面介紹isa_t聯(lián)合體的兩個(gè)成員(Class cls;
uintptr_t bits;),當(dāng)nonpointer=1時(shí),則代表對(duì)象isa不單指class了,而是使用了優(yōu)化的bits保存類的信息和其他內(nèi)存管理的信息。一般自定義的類都是1的,而系統(tǒng)類才會(huì)有純isa指針的情況,占1位。
0:純isa指針,即表示class地址。
1:不只是類對(duì)象地址,isa中包含了類信息、對(duì)象的引用計(jì)數(shù)等。 -
has_assoc關(guān)聯(lián)對(duì)象標(biāo)志位,占1位。當(dāng)關(guān)聯(lián)對(duì)象標(biāo)志位被設(shè)置為 1 時(shí),表示該對(duì)象具有關(guān)聯(lián)對(duì)象(Associated Objects)。關(guān)聯(lián)對(duì)象允許開發(fā)者將額外的數(shù)據(jù)與一個(gè)對(duì)象相關(guān)聯(lián),而無需修改該對(duì)象的類結(jié)構(gòu)。這對(duì)于給現(xiàn)有的類添加屬性或附加其他數(shù)據(jù)非常有用,特別是在無法修改類定義的情況下。沒有關(guān)聯(lián)對(duì)象的對(duì)象釋放的更快。 -
has_cxx_dtor該對(duì)象是否有C++或Objc的析構(gòu)器,如果有析構(gòu)函數(shù),則需要做析構(gòu)邏輯,如果沒有,則可以更快釋放對(duì)象,占1位。 -
shiftclx存儲(chǔ)類指針的值, 也就是類信息,開啟指針優(yōu)化的情況下,在arm64架構(gòu)中有33位用來存儲(chǔ)類指針,x86_64架構(gòu)中占44位。 -
magic用于調(diào)試器判斷,在調(diào)試時(shí)區(qū)分對(duì)象是否已經(jīng)初始化,占6位。固定為0x1a。 -
weakly_refrenced是用于表示該對(duì)象是否被別的對(duì)象弱引用。沒有被弱引用的對(duì)象釋放的更快。 -
deallocating標(biāo)志該對(duì)象是否正在被釋放。 -
has_sidetable_rc用于標(biāo)識(shí)是否當(dāng)前的引用計(jì)數(shù)過大,無法在isa中存儲(chǔ),而需要借用sidetable來存儲(chǔ)。 -
extra_rc表示該對(duì)象的引用計(jì)數(shù)值-1,比如,一個(gè)object對(duì)象的引用計(jì)數(shù)為9,則此時(shí)extra_rc的值為8。 如果大于最大容量,就需要取一半計(jì)數(shù)存到散列表中,真機(jī)上最多有8張散列表存儲(chǔ)對(duì)象引用計(jì)數(shù),x86_64則最多64張,這時(shí)上面的has_sidetable_rc值置為true。
了解完isa內(nèi)部結(jié)構(gòu)之后,我們來驗(yàn)證一下alloc的過程中isa跟類是如何關(guān)聯(lián)的。
在執(zhí)行BKPerson *objc = [BKPerson alloc];時(shí)跟進(jìn)到initIsa的方法中:

可以看到
!nonpointer條件為false,說明BKPerson類并不是一個(gè)純isa指針,需要開啟指針優(yōu)化,所以走到下面的初始化流程。
打印出這個(gè)newisa:

這時(shí)的isa的成員cls為nil,bits默認(rèn)為0,bits的位域信息都是初始值0。
往下執(zhí)行

這一句是給
bits賦值一個(gè)初始值,這是一個(gè)系統(tǒng)宏定義
# define ISA_MAGIC_VALUE 0x001d800000000001ULL
這時(shí)再打印newisa,可以看到賦值后的nonpointer已經(jīng)是1了,magic為59。
上面我們了解到magic在x86_64架構(gòu)下的bits位域分布在47-52位,占據(jù)6位,用計(jì)算器驗(yàn)證下這個(gè)59是不是0x001d800000000001ULL里的:

可以看到第一位是
1,跟我們的打印結(jié)果一致,nonpointer值變?yōu)?code>1,第47位往后數(shù)6位是111011,那么59的二進(jìn)制是:
此時(shí)此刻,可以得出結(jié)果,這magic的59確實(shí)是由0x001d800000000001ULL填進(jìn)去的。
再往下執(zhí)行

has_cxx_dtor賦值為false,表示沒有自定義的析構(gòu)函數(shù)。
newisa.shiftcls = (uintptr_t)cls >> 3;
表示將cls類地址右移3位,賦值給shiftcls,上面我們知道x86_64下,shiftcls在bits的64位內(nèi)存中占用44位,從3-46位。
通過打印的信息,cls = BKPerson能看出來已經(jīng)將類信息關(guān)聯(lián)上指針了,也就是這個(gè)shiftcls = 536871965這個(gè)信息保存著類的信息。

打印
cls這個(gè)類,并手動(dòng)將其地址右移3位,可以得出536871965,確實(shí)等于shiftcls的數(shù)值。
??那么為什么要右移3位呢?而不直接賦值過去呢?

從圖可以清晰解釋,為什么需要右移3位?因?yàn)?code>bits的成員
shiftcls在x86_64下占據(jù)44位,而類cls內(nèi)存存儲(chǔ)的類信息是在第3位到47位,所以需要右移3位后開始存儲(chǔ),存到44位滿了就停止存儲(chǔ)。這樣才能準(zhǔn)確的存儲(chǔ)到類的信息。
??那我們?cè)趺醋C明得出的這個(gè)類就是已經(jīng)關(guān)聯(lián)上了我們的對(duì)象指針?
我們將斷點(diǎn)的堆?;赝说?code>obj的關(guān)聯(lián)類的地方。

控制臺(tái)打印這個(gè)
obj指針,并將isa的內(nèi)存地址右移3位,再左移20位,再右移17位,這時(shí)再打印地址移動(dòng)之后的isa的地址,可以看到,就是我們上面關(guān)聯(lián)的類,也就是說這時(shí)對(duì)象指針和類關(guān)聯(lián)上了。
這個(gè)過程可以用下圖清晰表現(xiàn)出來:

獲取對(duì)象的類這個(gè)操作其實(shí)在我們?nèi)粘i_發(fā)中經(jīng)常用到,我們通過導(dǎo)入#import <objc/runtime.h>,
BKPerson *objc = [BKPerson alloc];
NSLog(@"%@", object_getClass(objc));
結(jié)果為BKPerson。
查看這個(gè)函數(shù)的源碼,
Class object_getClass(id obj)
{
if (obj) return obj->getIsa();
else return Nil;
}
繼而查找getIsa()
inline Class
objc_object::getIsa()
{
if (fastpath(!isTaggedPointer())) return ISA();
extern objc_class OBJC_CLASS_$___NSUnrecognizedTaggedPointer;
uintptr_t slot, ptr = (uintptr_t)this;
Class cls;
slot = (ptr >> _OBJC_TAG_SLOT_SHIFT) & _OBJC_TAG_SLOT_MASK;
cls = objc_tag_classes[slot];
if (slowpath(cls == (Class)&OBJC_CLASS_$___NSUnrecognizedTaggedPointer)) {
slot = (ptr >> _OBJC_TAG_EXT_SLOT_SHIFT) & _OBJC_TAG_EXT_SLOT_MASK;
cls = objc_tag_ext_classes[slot];
}
return cls;
}
再查找ISA()
inline Class
objc_object::ISA()
{
ASSERT(!isTaggedPointer());
#if SUPPORT_INDEXED_ISA
if (isa.nonpointer) {
uintptr_t slot = isa.indexcls;
return classForIndex((unsigned)slot);
}
return (Class)isa.bits;
#else
return (Class)(isa.bits & ISA_MASK);
#endif
}
查看if里的條件的宏定義,
# define SUPPORT_INDEXED_ISA 0
可以知道走的是這行代碼return (Class)(isa.bits & ISA_MASK);
也就是取出對(duì)象的isa里的bits 去與運(yùn)算上 ISA_MASK。
這個(gè)宏的定義是:

我們?cè)偻ㄟ^lldb命令驗(yàn)證這個(gè)過程。取出對(duì)象obj的isa地址 & ISA_MASK,可以得出就是對(duì)象的類。
那么這個(gè)算法,其實(shí)就簡化了我們上面對(duì)isa地址的一頓左移右移操作,直接一步到位得出類。
通過計(jì)算器查看這個(gè)ISA_MASK宏的二進(jìn)制

可以看到,從第
4位到47位,一共44位,都為1,其他位都為0,而與運(yùn)算,就是兩個(gè)數(shù)只有相同位上都為1,才會(huì)得出1,所以這個(gè)與運(yùn)算,就是為了取出中間44位的類信息的算法,其他位補(bǔ)0,得出一個(gè)64位的數(shù),表示這個(gè)類。至此,我們了解了
isa結(jié)構(gòu),及其位域的分布和成員作用,并探索了對(duì)象指針關(guān)聯(lián)類的過程并驗(yàn)證結(jié)果。感謝閱讀~