前言
- 這篇主要內容探索 類與isa是如何關聯的。
- 在之前iOS底層探索 alloc&init這篇文章中,我們知道了_class_createInstanceFromZone方法的關鍵三步:
a. 獲取實例的內存空間大小: cls->instanceSize()
b. 根據內存大小,分配內存空間,讓實例指向內存開始地址: calloc
c. 關聯isa,實例的isa指向類: obj->initInstanceIsa(cls, hasCxxDtor)
, 結合位運算、聯合體、位域和結構體的內存對齊的知識,我們探索oc對象的本質從關聯isa,實例的isa指向類
開始。
準備工作
先大概了解一個編譯器:clang
:
-
clang
是一個由Apple主導編寫,基于LLVM的C/C++/OC的編譯器,主要是用于底層編譯,將一些文件``輸出成c++文件,例如main.m 輸出成main.cpp; - 其目的是為了更好的觀察底層的一些結構 及 實現的邏輯,方便理解底層原理。
一 、查看類的編譯源碼
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
//以下兩種方式是通過指定架構模式的命令行,使用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,發現HJPerson在底層會被編譯成 struct 結構體
點擊NSObject_IMPL
跳轉可以得到 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;
}
現在我們通過clang 看到類被編譯成結構體之后,找到isa ,那么isa是如何關聯類的信息的呢?
我們可以在 _class_createInstanceFromZone
的核心方法的第三步:關聯isa,實例的isa指向類: obj->initInstanceIsa(cls, hasCxxDtor)
中找到答案,這里做了一系列操作isa和類信息的操作,我們再去查看initInstanceIsa
內部源碼
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) {
//關鍵代碼: 初始化isa
isa = isa_t((uintptr_t)cls);
} else {
//禁用非指針Isa
ASSERT(!DisableNonpointerIsa);
//實例需要原始Isa
ASSERT(!cls->instancesRequireRawIsa());
//關鍵代碼: 初始化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)
;
關鍵單詞:nonpointer
非指針
三 、繼續探索isa_t
是怎么做的,我們再次進入源碼跳轉到isa_t
內部如下:
union isa_t { //聯合體 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,由聯合體的定義所知,這兩個成員是互斥的,也就意味著,當初始化isa指針時,有兩種初始化方式
通過cls初始化,bits無默認值
通過bits初始化,cls有默認值
-
還提供了一個結構體定義的位域,用于存儲類信息及其他信息,結構體的成員ISA_BITFIELD,這是一個宏定義,有兩個版本 arm64(對應ios 移動端) 和 x86_64(對應macOS),以下是它們的一些宏定義,如下圖所示:
ISA_BITFIELD
注:
nonpointer有兩個值,表示自定義的類等,占1位
0:純isa指針
1:不只是類對象地址,isa中包含了類信息、對象的引用計數等has_assoc表示關聯對象標志位,占1位
0:沒有關聯對象
1:存在關聯對象has_cxx_dtor 表示該對象是否有C++/OC的析構器(類似于dealloc),占1位如果有析構函數,則需要做析構邏輯
如果沒有,則可以更快的釋放對象, 析構函數 類似于 oc層面的 dealloc;shiftclx表示存儲類的指針的值(類的地址), 即類信息arm64中占 33位,開啟指針優化的情況下,在arm64架構中有33位用來存儲類指針x86_64中占 44位;
magic 用于調試器判斷當前對象是真的對象 還是 沒有初始化的空間,占6位;
weakly_refrenced是 指對象是否被指向 或者 曾經指向一個ARC的弱變量, 沒有弱引用的對象可以更快釋放deallocating 標志對象是是否正在釋放內存;
has_sidetable_rc表示 當對象引用計數大于10時,則需要借用該變量存儲進位;
extra_rc(額外的引用計數) --- 表示該對象的引用計數值,實際上是引用計數值減1, 如果對象的引用計數為10,那么extra_rc為9;
針對兩種不同平臺,其isa的存儲情況如圖所示
我們lldb 調試 可以看到經過一系列賦值 將HJPerson類信息存進了shiftcls中
注:
為什么在shiftcls賦值時(newisa.shiftcls = (uintptr_t)cls >> 3)
需要類型強轉?因為內存的存儲不能存儲字符串,機器碼只能識別 0 、1這兩種數字,所以需要將其轉換為uintptr_t數據類型,這樣shiftcls中存儲的類信息才能被機器碼理解, 其中uintptr_t是long。
至此,我們得出結論:在isa初始化obj->initInstanceIsa(cls, hasCxxDtor)
的時候,通過isa_t
聯合體,在位域
運算中,將類信息cls
存進了存儲類的指針的值shiftclx
, 最后isa = newisa
;isa
中既有HJPerson
的指針,又有HJPerson
的信息。就這樣isa與類關聯到一起了。
四 、拓展 驗證 isa 與 類 的關聯
簡單點 我們在x86_64
中通過isa & ISA_MSAK
驗證
流程:
1.在
_class_createInstanceFromZone
方法,此時cl
s 與isa
已經關聯完成,執行po objc
2.執行
x/4gx obj
,得到isa指針
的地址0x001d8001000021fd
3.將
isa指針地址 & ISA_MASK
(處于macOS
,使用x86_64
中的宏定義),即po 0x001d8001000021fd & 0x00007ffffffffff8
,得出HJPerson
.x86_64
中,ISA_MASK
宏定義的值為0x00007ffffffffff8ULL
good~~~加油!