學習之前,我們先補充下位域和聯合體的知識。
位域
位域的定義
所謂位域就是把一個位元組中的二進位劃分為幾個不同的區域,並說明每個區域的位數。每個域有一個域名,允許在程式中按域名進行操作——這樣就可以把幾個不同的物件用一個位元組的二進位制位域來表示。位域是C語言一種資料結構。
使用位域的好處是:
- 有些資訊在儲存時,並不需要佔用一個完整的位元組,而只需佔幾個或一個二進位制位。例如在存放一個開關量時,只有0和1 兩種狀態,用一位二進位即可。這樣節省儲存空間,而且處理簡便。 這樣就可以把幾個不同的物件用一個位元組的二進位制位域來表示。
- 可以很方便的利用位域把一個變數給按位分解。比如只需要4個大小在0到3的隨即數,就可以只rand()一次,然後每個位域取2個二進位制位即可,省時省空間。
位域的使用
在C語言中,位域的宣告和結構體(struct)類似,但它的成員是一個或多個位的欄位,這些不同長度的欄位實際儲存在一個或多個整型變數中。
在宣告時,位域成員必須是整形或列舉型別(通常是無符號型別),且在成員名的後面是一個冒號和一個整數,整數規定了成員所佔用的位數。
位域不能是靜態型別。不能使用&對位域做取地址運算,因此不存在位域的指標,編譯器通常不支援位域的引用(reference)。
// 結構體
struct Struct {
// (資料型別 元素);
char a; // 1位元組 0 補1 2 3
int b; // 4位元組 4 5 6 7
} Str;
// 位域
struct BitArea {
// (資料型別 位域名: 位域長度);
char a: 1;
int b: 3;
} Bit;
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSLog(@"Struct:%lu——BitArea:%lu", sizeof(Str), sizeof(Bit));
}
return 0;
}
複製程式碼
聯合體
聯合體的定義
聯合體(union,又叫共用體):使幾種不同型別的變數存放到同一段記憶體單元中。即使用覆蓋技術,幾個變數互相覆蓋重疊。
union MyValue{
// (資料型別 元素)
int x;
int y;
double z;
};
void main(){
union MyValue d1;
d1.x = 90;
d1.y = 100;
d1.z = 23.8; // 最後一次賦值有效
printf("%d,%d,%lf\n",d1.x,d1.y,d1.z);
}
// 輸出結果:
-858993459,-858993459,23.800000
複製程式碼
聯合體定義使用時注意點:
- union中可以定義多個成員,union的大小由最大的成員的大小決定。
- union成員共享同一塊大小的記憶體,一次只能使用其中的一個成員。
- 對某一個成員賦值,會覆蓋其他成員的值,因為他們共享一塊記憶體。
- union中各個成員儲存的起始地址都是相對於基地址的偏移都為0。
聯合體和結構體區別:
- 結構體的各個成員會佔用不同的記憶體,互相之間沒有影響;而共用體的所有成員佔用同一段記憶體,修改一個成員會影響其餘所有成員。
- 結構體佔用的記憶體大於等於所有成員佔用的記憶體的總和(成員之間可能會存在縫隙),共用體佔用的記憶體等於最長的成員佔用的記憶體。共用體使用了記憶體覆蓋技術,同一時刻只能儲存一個成員的值,如果對新的成員賦值,就會把原來成員的值覆蓋掉
isa 的結構
在之前的iOS探索alloc流程中,我們提了一句obj->initInstanceIsa(cls, hasCxxDtor)在內部呼叫initIsa(cls, true, hasCxxDtor)初始化isa,今天就分析下isa。
static ALWAYS_INLINE id
callAlloc(Class cls, bool checkNil, bool allocWithZone=false)
{
if (fastpath(!cls->ISA()->hasCustomAWZ())) {
if (fastpath(cls->canAllocFast())) {
// No ctors, raw isa, etc. Go straight to the metal.
bool dtor = cls->hasCxxDtor();
id obj = (id)calloc(1, cls->bits.fastInstanceSize());
if (slowpath(!obj)) return callBadAllocHandler(cls);
obj->initInstanceIsa(cls, dtor);
return obj;
} else {
// Has ctor or raw isa or something. Use the slower path.
id obj = class_createInstance(cls, 0);
if (slowpath(!obj)) return callBadAllocHandler(cls);
return obj;
}
}
}
複製程式碼
isa 的初始化
inline void
objc_object::initIsa(Class cls, bool nonpointer, bool hasCxxDtor)
{
assert(!isTaggedPointer());
if (!nonpointer) {
isa.cls = 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;
}
}
複製程式碼
- TaggedPointer 專門用來儲存小的物件(8-10),例如NSNumber和NSDate。
- 建立物件跟著斷點發現nonpointer為true
- else流程走SUPPORT_INDEXED_ISA,表示isa_t中存放的 Class資訊是Class 的地址,還是一個索引(根據該索引可在類資訊表中查詢該類結構地址)
isa_t 的結構
union isa_t {
isa_t() { } // 初始化方法1
isa_t(uintptr_t value) : bits(value) { } // 初始化方法2
Class cls; // 成員1
uintptr_t bits; // 成員2
#if defined(ISA_BITFIELD)
struct { // 成員3
ISA_BITFIELD; // defined in isa.h // 位域巨集定義
};
#endif
};
複製程式碼
通過原始碼我們發現isa它一個聯合體,8個位元組,它的特性就是共用記憶體,或者說是互斥(比如說如果cls賦值了,再對bits進行賦值時會覆蓋掉cls)。在isa_t聯合體內使用巨集ISA_BITFIELD定義了位域,我們進入位域內檢視原始碼。
ISA_BITFIELD
# 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
複製程式碼
- nonpointer:是否對isa指標開啟指標優化
- 當nonpointer = 0:不優化,純isa指標,當訪問isa指標時,直接通過isa.cls和類進行關聯,返回其成員變數cls
- 當nonpointer = 1:優化過的isa指標,指標內容不止是類物件地址,還會使用位域存放類資訊、物件的引用計數,此時建立newisa並初始化後賦值給isa指標。 如果沒有,則可以更快的釋放物件。
- has_assoc:是否有關聯物件,0沒有,1存在。
- has_cxx_dtor:該物件是否有 C++ 或者 Objc 的析構器,如果有解構函式,則需要做析構邏輯, 如果沒有,則可以更快的釋放物件
- shiftcls:儲存類物件和元類物件的指標的值,在開啟指標優化的情況下,在 arm64 架構中用 33 位用來儲存類指標
- magic:用於偵錯程式判斷當前物件是真的物件還是沒有初始化的空間
- weakly_referenced:物件是否被指向或者曾經指向一個 ARC 的弱變數, 沒有弱引用的物件可以更快釋放
- deallocating:標誌物件是否正在釋放記憶體
- has_sidetable_rc:當物件引用技術大於 10 時,則需要借用該變數儲存進位(rc = retainCount)
- extra_rc:當表示該物件的引用計數值,實際上是引用計數值減 1, 例如,如果物件的引用計數為 10,那麼 extra_rc 為 9。如果引用計數大於 10, 則需要使用到下面的 has_sidetable_rc。
isa_t聯合體有3個成員(Class cls、uintptr_t bits、聯合體+位域ISA_BITFIELD),3個成員共同佔用8位元組的記憶體空間,通過ISA_BITFIELD裡面的位域成員,可以對8位元組空間的不同二進位制位進行操作,達到節省記憶體空間的目的。
聯合體所有屬性共用記憶體,記憶體長度等於其最長成員的長度,使程式碼儲存資料高效率的同時,有較強的可讀性;而位域可以容納更多型別
shiftcls關聯類
在shiftcls中儲存著類物件和元類物件的記憶體地址資訊,我們重點看一下newisa.indexcls = (uintptr_t)cls->classArrayIndex()和uintptr_t shiftcls : 33這兩行原始碼。
上篇文章中我們提到,在Person例項物件裡面可能因為記憶體優化,屬性的位置可能發生變換(比如ch1和ch2)。但是物件記憶體的第一個屬性必然是isa。因為isa來自於NSObject類,是繼承過來的,根本還沒有編輯屬性列表(關於ro/rw我們後續章節會提到)。
我們就測試下,person的第一個屬性是不是isa。
int main(int argc, const char * argv[]) {
@autoreleasepool {
Person *person = [Person alloc];
objc_getClass();
}
return 0;
}
Class object_getClass(id obj)
{
if (obj) return obj->getIsa();
else return Nil;
}
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];
}
}
複製程式碼
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 __arm64__
# define ISA_MASK 0x0000000ffffffff8ULL
# elif __x86_64__
# define ISA_MASK 0x00007ffffffffff8ULL
# endif
複製程式碼
列印出isa & mask的值,與class第一段地址比較。
例項物件首地址一定 是isa。例項物件通過isa & isa_mask關聯類。
思考
如果我們把聯合體中的位域換成基本資料型別來表示,結合記憶體對齊原則,ISA_BITFIELD佔用24個位元組。 通過位域,每一個繼承自NSObject的物件都至少減少了16位元組的記憶體空間。
// 在arm64下將位域換成基本資料型別
struct isa_t_bitFields {
unsigned char nonpointer; // 1位元組 0
unsigned char has_assoc; // 1位元組 1
unsigned char has_cxx_dtor; // 1位元組 2 補3 4 5 6 7
unsigned long shiftcls; // 8位元組 8 9 10 11 12 13 14 15
unsigned char magic; // 1位元組 16
unsigned char weakly_referenced; // 1位元組 17
unsigned char deallocating; // 1位元組 18
unsigned char has_sidetable_rc; // 1位元組 19
unsigned int extra_rc; // 4位元組 20 21 22 23
};
複製程式碼
isa走位
類在記憶體中只存在一個
Class class1 = [Person class];
Class class2 = [Person alloc].class;
Class class3 = object_getClass([Person alloc]);
Class class4 = [Person alloc].class;
NSLog(@"\n%p\n%p\n%p\n%p", class1, class2, class3, class4);
複製程式碼
0x1000020f0
0x1000020f0
0x1000020f0
0x1000020f0
複製程式碼
類在記憶體中只會存在一個,而例項物件可以存在多個。
通過物件/類檢視isa走位
- 例項物件由類例項化出來。例項物件 和 類 通過isa關聯
- 類本質上也是物件,通過元類例項化出來。類物件 和 元類通過isa關聯:
- 例項對 - isa - 類 - isa - 元類
我們模仿object_getClass,通過isa & isa_mask,得到物件通過isa關聯的類。
- 列印AKPerson類取得isa
- 由AKPerson類進行偏移得到AKPerson元類指標,列印AKPerson元類取得isa
- 由AKPerson元類進行偏移得到NSObject根元類指標,列印NSObject根元類取得isa
- 由NSObject根元類進行偏移得到NSObject根元類本身指標
- 列印NSObject根類取得isa
- 由NSObject根類進行偏移得到NSObject根元類指標
通過NSObject檢視isa走位
int main(int argc, const char * argv[]) {
@autoreleasepool {
// NSObject例項物件
NSObject *object1 = [NSObject alloc];
// NSObject類
Class class = object_getClass(object1);
// NSObject元類
Class metaClass = object_getClass(class);
// NSObject根元類
Class rootMetaClass = object_getClass(metaClass);
// NSObject根根元類
Class rootRootMetaClass = object_getClass(rootMetaClass);
NSLog(@"\n%p 例項物件\n%p 類\n%p 元類\n%p 根元類\n%p 根根元類",object1, class, metaClass, rootMetaClass, rootRootMetaClass);
}
return 0;
}
複製程式碼
0x103239120 例項物件
0x7fff9498b118 類
0x7fff9498b0f0 元類
0x7fff9498b0f0 根元類
0x7fff9498b0f0 根根元類
複製程式碼
1.例項物件-> 類物件 -> 元類 -> 根元類 -> 根元類(本身)
2.NSObject(根類) -> 根元類 -> 根元類(本身)
3.指向根元類的isa都是一樣的
類、元類是由系統建立的
1.物件是程式猿根據類例項化的。
2.類是程式碼編寫的,記憶體中只有一份,是系統建立的。
3.元類是系統編譯時,系統編譯器建立的,便於方法的編譯
isa走點陣圖
isa 走位(虛線):例項物件 -> 類物件 -> 元類 -> 根元類 -> 根元類自身
繼承關係(實現):子類 -> 父類 -> NSObject -> nil。 根元類的父類為NSObject。