從iOS底層之alloc、init探究這篇文章,我們可以知道,alloc
一個對象的過程,主要是計算所需內存大小cls->instanceSize
、申請內存空間calloc
、將isa
與類進行關聯obj->initInstanceIsa
。
??那么指針和類是怎么關聯的呢?
isa
到底是什么結構?保存了什么信息?下面來一一解惑。
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
BKPerson *objc = [BKPerson alloc];
NSLog(@"Hello, World! %@",objc);
}
return 0;
}
從alloc
的源碼跟進去到關聯指針和類的步驟:
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);
}
跟進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
條件是否滿足,都會生成一個isa_t
的類型。跟進去可以發現:
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
是一個union
聯合體,Class cls
代表isa
關聯的類的類型,uintptr_t bits
是一段保存著isa
指針優化、是否關聯對象標志位、對象是否有析構函數、類相關信息、引用計數等信息的8
字節大小的無符號長整型數據。要知道聯合體和結構體的區別是:
結構體(
struct
)中所有變量是“共存”的——優點是“有容乃大”, 全面;缺點是struct內存空間的分配是粗放的,不管用不用,全分配。結構體的類型大小大于等于
內部所有變量的類型大小總和,最終的類型大小是最大成員的類型大小的倍數,不足補齊。聯合體(
union
)中是各變量是“互斥”的——缺點就是不夠“包容”; 但優點是內存使用更為精細靈活,也節省了內存空間。聯合體類型的大小等于
最大成員類型大小。
就是說聯合體采用內存覆蓋機制
,只有一塊變量存儲區,只能存一個變量的值,新的成員賦值會把原本存儲的成員信息替換掉。也就是Class cls
和uintptr_t bits
只能set
賦值其中一個。而內存使用的精細靈活,體現在以位域
(即二進制中每一位均可表示不同的信息)存儲成員數據,也就是以計算機二進制存儲的方式,位bit
為單位,用1
和0
標記數據,數據的尺寸大小是以占用多少bit
,而不是以每個數據成員數據類型的尺寸大小(多少字節byte
)存儲。isa
的bits
占用的內存大小是8字節
,即64位
,可以存儲足夠多的信息,很大節省了內存。
isa
的bits
成員的位域,定義在isa.h
源文件中
struct {
ISA_BITFIELD; // defined in isa.h
};
ISA_BITFIELD
是一個宏定義,分別在x86_64架構
和arm64架構
是這樣的:
bits
的64位存儲分布圖:
其中存儲的成員信息:
-
nonpointer
是否開啟isa指針優化。上面介紹isa_t聯合體的兩個成員(Class cls;
uintptr_t bits;),當nonpointer=1時,則代表對象isa不單指class了,而是使用了優化的bits保存類的信息和其他內存管理的信息。一般自定義的類都是1的,而系統類才會有純isa
指針的情況,占1
位。
0
:純isa指針,即表示class地址。
1
:不只是類對象地址,isa
中包含了類信息、對象的引用計數等。 -
has_assoc
關聯對象標志位,占1
位。當關聯對象標志位被設置為 1 時,表示該對象具有關聯對象(Associated Objects)。關聯對象允許開發者將額外的數據與一個對象相關聯,而無需修改該對象的類結構。這對于給現有的類添加屬性或附加其他數據非常有用,特別是在無法修改類定義的情況下。沒有關聯對象的對象釋放的更快。 -
has_cxx_dtor
該對象是否有C++
或Objc的析構器,如果有析構函數,則需要做析構邏輯,如果沒有,則可以更快釋放對象,占1
位。 -
shiftclx
存儲類指針的值, 也就是類信息,開啟指針優化的情況下,在arm64
架構中有33
位用來存儲類指針,x86_64
架構中占44
位。 -
magic
用于調試器判斷,在調試時區分對象是否已經初始化,占6
位。固定為0x1a。 -
weakly_refrenced
是用于表示該對象是否被別的對象弱引用。沒有被弱引用的對象釋放的更快。 -
deallocating
標志該對象是否正在被釋放。 -
has_sidetable_rc
用于標識是否當前的引用計數過大,無法在isa中存儲,而需要借用sidetable來存儲。 -
extra_rc
表示該對象的引用計數值-1,比如,一個object對象的引用計數為9,則此時extra_rc的值為8。 如果大于最大容量,就需要取一半計數存到散列表中,真機上最多有8
張散列表存儲對象引用計數,x86_64
則最多64
張,這時上面的has_sidetable_rc
值置為true
。
了解完isa
內部結構之后,我們來驗證一下alloc
的過程中isa
跟類是如何關聯的。
在執行BKPerson *objc = [BKPerson alloc];
時跟進到initIsa
的方法中:
可以看到
!nonpointer
條件為false,說明BKPerson
類并不是一個純isa指針
,需要開啟指針優化,所以走到下面的初始化流程。
打印出這個newisa
:
這時的isa
的成員cls
為nil
,bits
默認為0
,bits
的位域信息都是初始值0
。
往下執行
這一句是給
bits
賦值一個初始值,這是一個系統宏定義
# define ISA_MAGIC_VALUE 0x001d800000000001ULL
這時再打印newisa
,可以看到賦值后的nonpointer
已經是1了,magic
為59。
上面我們了解到magic
在x86_64
架構下的bits
位域分布在47-52位
,占據6位
,用計算器驗證下這個59
是不是0x001d800000000001ULL
里的:
可以看到第一位是
1
,跟我們的打印結果一致,nonpointer
值變為1
,第47
位往后數6
位是111011
,那么59
的二進制是:此時此刻,可以得出結果,這magic
的59
確實是由0x001d800000000001ULL
填進去的。
再往下執行
has_cxx_dtor賦值為false,表示沒有自定義的析構函數。
newisa.shiftcls = (uintptr_t)cls >> 3;
表示將cls類地址右移3位
,賦值給shiftcls
,上面我們知道x86_64
下,shiftcls
在bits
的64位
內存中占用44位
,從3-46位
。
通過打印的信息,cls = BKPerson
能看出來已經將類信息關聯上指針了,也就是這個shiftcls = 536871965
這個信息保存著類的信息。
打印
cls
這個類,并手動將其地址右移3位
,可以得出536871965
,確實等于shiftcls
的數值。
??那么為什么要右移3位呢?而不直接賦值過去呢?
從圖可以清晰解釋,為什么需要右移3位?因為
bits
的成員shiftcls
在x86_64
下占據44位
,而類cls
內存存儲的類信息是在第3位
到47位
,所以需要右移3位
后開始存儲,存到44位
滿了就停止存儲。這樣才能準確的存儲到類的信息。
??那我們怎么證明得出的這個類就是已經關聯上了我們的對象指針?
我們將斷點的堆棧回退到obj
的關聯類的地方。
控制臺打印這個
obj
指針,并將isa
的內存地址右移3位
,再左移20位
,再右移17位
,這時再打印地址移動之后的isa
的地址,可以看到,就是我們上面關聯的類,也就是說這時對象指針和類關聯上了。
這個過程可以用下圖清晰表現出來:
獲取對象的類
這個操作其實在我們日常開發中經常用到,我們通過導入#import <objc/runtime.h>
,
BKPerson *objc = [BKPerson alloc];
NSLog(@"%@", object_getClass(objc));
結果為BKPerson
。
查看這個函數的源碼,
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);
也就是取出對象的isa
里的bits
去與運算
上 ISA_MASK
。
這個宏的定義是:
我們再通過lldb
命令驗證這個過程。取出對象obj
的isa地址
& ISA_MASK
,可以得出就是對象的類。
那么這個算法,其實就簡化了我們上面對isa
地址的一頓左移右移操作,直接一步到位得出類。
通過計算器查看這個ISA_MASK
宏的二進制
可以看到,從第
4位
到47位
,一共44位
,都為1
,其他位都為0
,而與運算
,就是兩個數只有相同位上都為1
,才會得出1
,所以這個與運算
,就是為了取出中間44位
的類信息的算法,其他位補0
,得出一個64位
的數,表示這個類。至此,我們了解了
isa
結構,及其位域的分布和成員作用,并探索了對象指針關聯類的過程并驗證結果。感謝閱讀~