前言
- 這篇主要內(nèi)容探索 類與isa是如何關(guān)聯(lián)的。
- 在之前iOS底層探索 alloc&init這篇文章中,我們知道了_class_createInstanceFromZone方法的關(guān)鍵三步:
a. 獲取實例的內(nèi)存空間大小: cls->instanceSize()
b. 根據(jù)內(nèi)存大小,分配內(nèi)存空間,讓實例指向內(nèi)存開始地址: calloc
c. 關(guān)聯(lián)isa,實例的isa指向類: obj->initInstanceIsa(cls, hasCxxDtor), 結(jié)合位運算、聯(lián)合體、位域和結(jié)構(gòu)體的內(nèi)存對齊的知識,我們探索oc對象的本質(zhì)從關(guān)聯(lián)isa,實例的isa指向類開始。
準備工作
先大概了解一個編譯器:clang :
-
clang是一個由Apple主導編寫,基于LLVM的C/C++/OC的編譯器,主要是用于底層編譯,將一些文件``輸出成c++文件,例如main.m 輸出成main.cpp; - 其目的是為了更好的觀察底層的一些結(jié)構(gòu) 及 實現(xiàn)的邏輯,方便理解底層原理。
一 、查看類的編譯源碼
1 .打開終端,cd到文件目錄下,利用clang將main.m編譯成 main.cpp .
//1、將 main.m 編譯成 main.cpp
clang -rewrite-objc main.m -o main.cpp
其他舉例:
//1、將 ViewController.m 編譯成 ViewController.cpp
clang -rewrite-objc -fobjc-arc -fobjc-runtime=ios-13.0.0 -isysroot / /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator13.7.sdk ViewController.m
//以下兩種方式是通過指定架構(gòu)模式的命令行,使用xcode工具 xcrun
//2、模擬器文件編譯
- xcrun -sdk iphonesimulator clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp
//3、真機文件編譯
- xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main- arm64.cpp
2 .打開 main.cpp, 找到HJPerson,發(fā)現(xiàn)HJPerson在底層會被編譯成 struct 結(jié)構(gòu)體

點擊NSObject_IMPL 跳轉(zhuǎn)可以得到 isa
struct NSObject_IMPL {
Class isa;
};
先看這里:
struct HJPerson_IMPL {
struct NSObject_IMPL NSObject_IVARS;
NSString *_name;
};
這里的知識點:
-
HJPerson中的第一個屬性NSObject_IVARS等效于NSObject中的isa, 每個類中都有默認屬性isa;
二 、翻開objc源碼 可以看到 NSObject的isa 也是class類型
@interface NSObject <NSObject> {
Class isa OBJC_ISA_AVAILABILITY;
}
現(xiàn)在我們通過clang 看到類被編譯成結(jié)構(gòu)體之后,找到isa ,那么isa是如何關(guān)聯(lián)類的信息的呢?
我們可以在 _class_createInstanceFromZone 的核心方法的第三步:關(guān)聯(lián)isa,實例的isa指向類: obj->initInstanceIsa(cls, hasCxxDtor)中找到答案,這里做了一系列操作isa和類信息的操作,我們再去查看initInstanceIsa 內(nèi)部源碼
inline void
objc_object::initProtocolIsa(Class cls)
{
return initClassIsa(cls);
}
inline void
objc_object::initInstanceIsa(Class cls, bool hasCxxDtor)
{
ASSERT(!cls->instancesRequireRawIsa());
ASSERT(hasCxxDtor == cls->hasCxxDtor());
initIsa(cls, true, hasCxxDtor);
}
//排隊
inline void
objc_object::initIsa(Class cls, bool nonpointer, bool hasCxxDtor)
{
//斷言
ASSERT(!isTaggedPointer());
if (!nonpointer) {
//關(guān)鍵代碼: 初始化isa
isa = isa_t((uintptr_t)cls);
} else {
//禁用非指針I(yè)sa
ASSERT(!DisableNonpointerIsa);
//實例需要原始Isa
ASSERT(!cls->instancesRequireRawIsa());
//關(guān)鍵代碼: 初始化isa
isa_t newisa(0);
#if SUPPORT_INDEXED_ISA
ASSERT(cls->classArrayIndex() > 0);
newisa.bits = ISA_INDEX_MAGIC_VALUE;
// isa.magic是ISA_MAGIC_VALUE的一部分
// isa.nonpointer是ISA_MAGIC_VALUE的一部分
newisa.has_cxx_dtor = hasCxxDtor;
newisa.indexcls = (uintptr_t)cls->classArrayIndex();
#else
newisa.bits = ISA_MAGIC_VALUE;
newisa.has_cxx_dtor = hasCxxDtor;
newisa.shiftcls = (uintptr_t)cls >> 3;
#endif
isa = newisa;
}
}
重要代碼:isa_t: 指針的初始化
-
isa = isa_t((uintptr_t)cls); -
isa_t newisa(0);
關(guān)鍵單詞:nonpointer 非指針
三 、繼續(xù)探索isa_t是怎么做的,我們再次進入源碼跳轉(zhuǎn)到isa_t內(nèi)部如下:
union isa_t { //聯(lián)合體 isa_t
//兩個初始化方法
isa_t() { }
isa_t(uintptr_t value) : bits(value) { }
//一個Class
Class cls;
//一個 bits 。 uintptr_t :在64位的機器上,intptr_t和uintptr_t分別是long int、unsigned long int的別名
uintptr_t bits;
#if defined(ISA_BITFIELD)
struct {
// ISA_BITFIELD 這是一個宏
ISA_BITFIELD; // defined in isa.h
};
#endif
};
從union isa_t定義可以看出:
-
提供了兩個成員,cls 和 bits,由聯(lián)合體的定義所知,這兩個成員是互斥的,也就意味著,當初始化isa指針時,有兩種初始化方式
通過cls初始化,bits無默認值
通過bits初始化,cls有默認值
-
還提供了一個結(jié)構(gòu)體定義的位域,用于存儲類信息及其他信息,結(jié)構(gòu)體的成員ISA_BITFIELD,這是一個宏定義,有兩個版本 arm64(對應(yīng)ios 移動端) 和 x86_64(對應(yīng)macOS),以下是它們的一些宏定義,如下圖所示:
ISA_BITFIELD
注:
nonpointer有兩個值,表示自定義的類等,占1位
0:純isa指針
1:不只是類對象地址,isa中包含了類信息、對象的引用計數(shù)等has_assoc表示關(guān)聯(lián)對象標志位,占1位
0:沒有關(guān)聯(lián)對象
1:存在關(guān)聯(lián)對象has_cxx_dtor 表示該對象是否有C++/OC的析構(gòu)器(類似于dealloc),占1位如果有析構(gòu)函數(shù),則需要做析構(gòu)邏輯
如果沒有,則可以更快的釋放對象, 析構(gòu)函數(shù) 類似于 oc層面的 dealloc;shiftclx表示存儲類的指針的值(類的地址), 即類信息arm64中占 33位,開啟指針優(yōu)化的情況下,在arm64架構(gòu)中有33位用來存儲類指針x86_64中占 44位;
magic 用于調(diào)試器判斷當前對象是真的對象 還是 沒有初始化的空間,占6位;
weakly_refrenced是 指對象是否被指向 或者 曾經(jīng)指向一個ARC的弱變量, 沒有弱引用的對象可以更快釋放deallocating 標志對象是是否正在釋放內(nèi)存;
has_sidetable_rc表示 當對象引用計數(shù)大于10時,則需要借用該變量存儲進位;
extra_rc(額外的引用計數(shù)) --- 表示該對象的引用計數(shù)值,實際上是引用計數(shù)值減1, 如果對象的引用計數(shù)為10,那么extra_rc為9;
針對兩種不同平臺,其isa的存儲情況如圖所示

我們lldb 調(diào)試 可以看到經(jīng)過一系列賦值 將HJPerson類信息存進了shiftcls中

注:
為什么在shiftcls賦值時(newisa.shiftcls = (uintptr_t)cls >> 3)需要類型強轉(zhuǎn)?因為內(nèi)存的存儲不能存儲字符串,機器碼只能識別 0 、1這兩種數(shù)字,所以需要將其轉(zhuǎn)換為uintptr_t數(shù)據(jù)類型,這樣shiftcls中存儲的類信息才能被機器碼理解, 其中uintptr_t是long。
至此,我們得出結(jié)論:在isa初始化obj->initInstanceIsa(cls, hasCxxDtor)的時候,通過isa_t聯(lián)合體,在位域運算中,將類信息cls存進了存儲類的指針的值shiftclx , 最后isa = newisa;isa中既有HJPerson的指針,又有HJPerson的信息。就這樣isa與類關(guān)聯(lián)到一起了。
四 、拓展 驗證 isa 與 類 的關(guān)聯(lián)
簡單點 我們在x86_64中通過isa & ISA_MSAK驗證

流程:
1.在
_class_createInstanceFromZone方法,此時cls 與isa已經(jīng)關(guān)聯(lián)完成,執(zhí)行po objc2.執(zhí)行
x/4gx obj,得到isa指針的地址0x001d8001000021fd3.將
isa指針地址 & ISA_MASK(處于macOS,使用x86_64中的宏定義),即po 0x001d8001000021fd & 0x00007ffffffffff8,得出HJPerson.x86_64中,ISA_MASK宏定義的值為0x00007ffffffffff8ULL

good~~~加油!

