今天探索的主要目的是理解類與isa是如何關(guān)聯(lián)的
探索之前,先了解一個(gè)編譯器:clang
Clang
clang是一個(gè)由Apple主導(dǎo)編寫,基于LLVM的C/C++/OC的編譯器主要是用于
底層編譯,將一些文件輸出成c++文件,例如main.m輸出成main.cpp,其目的是為了更好的觀察底層的一些結(jié)構(gòu)及實(shí)現(xiàn)的邏輯,方便理解底層原理。
探索OC對(duì)象本質(zhì)
- 在
main中自定義一個(gè)類LGPerson,有一個(gè)屬性name
@interface LGPerson : NSObject
@property (nonatomic, copy) NSString *name;
@end
@implementation LGPerson
@end
- 通過終端,利用
clang將main.m編譯成main.cpp,有以下幾種編譯命令,這里使用的是第一種
//1、將 main.m 編譯成 main.cpp
clang -rewrite-objc main.m -o main.cpp
//2、將 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
//3、模擬器文件編譯
- xcrun -sdk iphonesimulator clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp
//4、真機(jī)文件編譯
- xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main- arm64.cpp
-
打開編譯好的
main.cpp,找到LGPerson的定義,發(fā)現(xiàn)LGPerson在底層會(huì)被編譯成struct結(jié)構(gòu)體LGPerson_IMPL中的第一個(gè)屬性 其實(shí)就是 isa,是繼承自NSObject,屬于偽繼承,偽繼承的方式是直接將NSObject結(jié)構(gòu)體定義為LGPerson中的第一個(gè)屬性,意味著LGPerson 擁有 NSObject中的所有成員變量。
LGPerson中的第一個(gè)屬性 NSObject_IVARS 等效于 NSObject中的 isa
//NSObject的定義
@interface NSObject <NSObject> {
Class isa OBJC_ISA_AVAILABILITY;
}
//NSObject 的底層編譯
struct NSObject_IMPL {
Class isa;
};
//LGPerson的底層編譯
struct LGPerson_IMPL {
struct NSObject_IMPL NSObject_IVARS; // 等效于 Class isa;
NSString *_name;
};
如下圖所示

通過上述分析,理解了OC對(duì)象的本質(zhì),但是看到
NSObject的定義,會(huì)產(chǎn)生一個(gè)疑問:為什么isa的類型是Class?在iOS-底層原理 :alloc & init & new 源碼分析文章中,提及過
alloc方法的核心之一的initInstanceIsa方法,通過查看這個(gè)方法的源碼實(shí)現(xiàn),我們發(fā)現(xiàn),在初始化isa指針時(shí),是通過isa_t類型初始化的,-
而在
NSObject定義中isa的類型是Class,其根本原因是由于isa對(duì)外反饋的是類信息,為了讓開發(fā)人員更加清晰明確,需要在isa返回時(shí)做了一個(gè)類型強(qiáng)制轉(zhuǎn)換,類似于swift中的as的強(qiáng)轉(zhuǎn)。源碼中isa的強(qiáng)轉(zhuǎn)如下圖所示
源碼中`isa`的`強(qiáng)轉(zhuǎn)`
總結(jié)
所以從上述探索過程中可以得出:
-
OC對(duì)象的本質(zhì)其實(shí)就是結(jié)構(gòu)體
*LGPerson中的isa是繼承自NSObject中的isa
objc_setProperty 源碼探索
除了LGPersong的底層定義,我們發(fā)現(xiàn)還有屬性 name對(duì)應(yīng)的 set和get方法,如下圖所示,其中set方法的實(shí)現(xiàn)依賴于runtime中的objc_setProperty。

可以通過以下步驟來一步步解開objc_setProperty的底層實(shí)現(xiàn)
- 在
objc4-781中全局搜索objc_setProperty,找到objc_setProperty的源碼實(shí)現(xiàn)
objc_setProperty的源碼實(shí)現(xiàn) - 進(jìn)入
reallySetProperty的源碼實(shí)現(xiàn),其方法的原理就是新值retain,舊值release
reallySetProperty源碼實(shí)現(xiàn)
總結(jié)
通過對(duì)objc_setProperty的底層源碼探索,有以下幾點(diǎn)說明:
objc_setProperty方法的目的適用于關(guān)聯(lián) 上層的set方法 以及底層的set方法,其本質(zhì)就是一個(gè)接口這么設(shè)計(jì)的原因是,
上層的set方法有很多,如果直接調(diào)用底層set方法中,會(huì)產(chǎn)生很多的臨時(shí)變量,當(dāng)你想查找一個(gè)set時(shí),會(huì)非常麻煩基于上述原因,蘋果采用了
配器設(shè)計(jì)模式(即將底層接口適配為客戶端需要的接口),對(duì)外提供一個(gè)接口,供上層的set方法使用,對(duì)內(nèi)調(diào)用底層的set方法,使其相互不受影響,即無論上層怎么變,下層都是不變的,或者下層的變化也無法影響上層,主要是達(dá)到上下層接口隔離的目的
下圖是上層、隔離層、底層之間的關(guān)系

cls 與 類 的關(guān)聯(lián)原理
在iOS-底層原理 :alloc & init & new 源碼分析與iOS-底層原理 :內(nèi)存對(duì)齊分別分析了alloc中3核心的前兩個(gè),今天來探索initInstanceIsa是如何將cls與isa關(guān)聯(lián)的.
在此之前,需要先了解什么是聯(lián)合體,為什么isa的類型isa_t是使用聯(lián)合體定義.
聯(lián)合體(union)
構(gòu)造數(shù)據(jù)類型的方式有以下兩種:
-
結(jié)構(gòu)體(struct) -
聯(lián)合體(union,也稱為共用體)
結(jié)構(gòu)體
結(jié)構(gòu)體是指把不同的數(shù)據(jù)組合成一個(gè)整體,其變量是共存的,變量不管是否使用,都會(huì)分配內(nèi)存。
缺點(diǎn):所有屬性都分配內(nèi)存,比較
浪費(fèi)內(nèi)存,假設(shè)有4個(gè)int成員,一共分配了16字節(jié)的內(nèi)存,但是在使用時(shí),你只使用了4字節(jié),剩余的12字節(jié)就是屬于內(nèi)存的浪費(fèi)優(yōu)點(diǎn):存儲(chǔ)
容量較大,包容性強(qiáng),且成員之間不會(huì)相互影響
聯(lián)合體
聯(lián)合體也是由不同的數(shù)據(jù)類型組成,但其變量是互斥的,所有的成員共占一段內(nèi)存。而且共用體采用了內(nèi)存覆蓋技術(shù),同一時(shí)刻只能保存一個(gè)成員的值,如果對(duì)新的成員賦值,就會(huì)將原來成員的值覆蓋掉
缺點(diǎn):,包容性弱
優(yōu)點(diǎn):所有成員共用一段內(nèi)存,使內(nèi)存的使用更為精細(xì)靈活,同時(shí)也節(jié)省了內(nèi)存空間
兩者的區(qū)別
-
內(nèi)存占用情況
- 結(jié)構(gòu)體的
各個(gè)成員會(huì)占用不同的內(nèi)存,互相之間沒有影響 - 共用體的
所有成員占用同一段內(nèi)存,修改一個(gè)成員會(huì)影響其余所有成員
- 結(jié)構(gòu)體的
-
內(nèi)存分配大小
- 結(jié)構(gòu)體內(nèi)存
>=所有成員占用的內(nèi)存總和(成員之間可能會(huì)有縫隙) -
共用體占用的內(nèi)存等于最大的成員占用的內(nèi)存
- 結(jié)構(gòu)體內(nèi)存
isa的類型 isa_t
以下是isa指針的類型isa_t的定義,從定義中可以看出是通過聯(lián)合體(union)定義的
union isa_t { //聯(lián)合體
isa_t() { }
isa_t(uintptr_t value) : bits(value) { }
//提供了cls 和 bits ,兩者是互斥關(guān)系
Class cls;
uintptr_t bits;
#if defined(ISA_BITFIELD)
struct {
ISA_BITFIELD; // defined in isa.h
};
#endif
};
isa_t類型使用聯(lián)合體的原因也是基于內(nèi)存優(yōu)化的考慮,這里的內(nèi)存優(yōu)化是指在isa指針中通過char + 位域(即二進(jìn)制中每一位均可表示不同的信息)的原理實(shí)現(xiàn)。通常來說,isa指針占用的內(nèi)存大小是8字節(jié),即64位,已經(jīng)足夠存儲(chǔ)很多的信息了,這樣可以極大的節(jié)省內(nèi)存,以提高性能
從isa_t的定義中可以看出:
提供了兩個(gè)成員,
cls和bits,由聯(lián)合體的定義所知,這兩個(gè)成員是互斥的,也就意味著,當(dāng)初始化isa指針時(shí),有兩種初始化方式通過
cls初始化,bits無默認(rèn)值通過
bits初始化,cls有默認(rèn)值-
還提供了一個(gè)結(jié)構(gòu)體定義的
位域,用于存儲(chǔ)類信息及其他信息,結(jié)構(gòu)體的成員ISA_BITFIELD,這是一個(gè)宏定義,有兩個(gè)版本__arm64__(對(duì)應(yīng)ios 移動(dòng)端) 和__x86_64__(對(duì)應(yīng)macOS),以下是它們的一些宏定義,如下圖所示
位域的宏定義 -
nonpointer有兩個(gè)值,表示自定義的類等,占1位-
0:純isa指針 -
1:不只是類對(duì)象地址,isa中包含了類信息、對(duì)象的引用計(jì)數(shù)等
-
-
has_assoc表示關(guān)聯(lián)對(duì)象標(biāo)志位,占1位
*
0:沒有關(guān)聯(lián)對(duì)象-
1:存在關(guān)聯(lián)對(duì)象
-
-
has_cxx_dtor表示該對(duì)象是否有C++/OC的析構(gòu)器(類似于dealloc),占1位- 如果
有析構(gòu)函數(shù),則需要做析構(gòu)邏輯 - 如果
沒有,則可以更快的釋放對(duì)象
- 如果
-
shiftclx表示存儲(chǔ)類的指針的值(類的地址), 即類信息
-
arm64中占33位,開啟指針優(yōu)化的情況下,在arm64架構(gòu)中有33位用來存儲(chǔ)類指針 -
x86_64中占44位
-
magic用于調(diào)試器判斷當(dāng)前對(duì)象是真的對(duì)象還是沒有初始化的空間,占6位-
weakly_refrenced是指對(duì)象是否被指向或者曾經(jīng)指向一個(gè)ARC的弱變量- 沒有弱引用的對(duì)象可以更快釋放
deallocating標(biāo)志對(duì)象是是否正在釋放內(nèi)存has_sidetable_rc表示 當(dāng)對(duì)象引用計(jì)數(shù)大于10時(shí),則需要借用該變量存儲(chǔ)進(jìn)位-
extra_rc(額外的引用計(jì)數(shù)) --- 導(dǎo)尿管表示該對(duì)象的引用計(jì)數(shù)值,實(shí)際上是引用計(jì)數(shù)值減1- 如果對(duì)象的引用計(jì)數(shù)為10,那么extra_rc為9
針對(duì)兩種不同平臺(tái),其isa的存儲(chǔ)情況如圖所示

原理探索
- 通過
alloc --> _objc_rootAlloc --> callAlloc --> _objc_rootAllocWithZone --> _class_createInstanceFromZone方法路徑,查找到initInstanceIsa,并進(jìn)入其原理實(shí)現(xiàn)
inline void
objc_object::initInstanceIsa(Class cls, bool hasCxxDtor)
{
ASSERT(!cls->instancesRequireRawIsa());
ASSERT(hasCxxDtor == cls->hasCxxDtor());
//初始化isa
initIsa(cls, true, hasCxxDtor);
}
- 進(jìn)入
initIsa方法的源碼實(shí)現(xiàn),主要是初始化isa指針
initIsa源碼實(shí)現(xiàn)
該方法的邏輯主要分為兩部分
* 通過 `cls` 初始化 `isa`
* 通過 `bits` 初始化 `isa`
驗(yàn)證 isa指針 位域(0-64)
根據(jù)前文提及的0-64位域,可以在這里通過initIsa方法中證明有isa指針中有這些位域(目前是處于macOS,所以使用的是x86_64)
- 首先通過main中的
LGPerson斷點(diǎn) -->initInstanceIsa-->initIsa--> 走到else中的isa初始化
else中的isa初始化
-
執(zhí)行l(wèi)ldb命令:
p newisa,得到newisa的詳細(xì)信息
lldb命令結(jié)果 -
繼續(xù)往下執(zhí)行,走到
newisa.bits = ISA_MAGIC_VALUE;下一行,表示為isa的bits成員賦值,重新執(zhí)行l(wèi)ldb命令p newisa,得到的結(jié)果如下
再次執(zhí)行p命令的結(jié)果
通過與前一個(gè)newsize的信息對(duì)比,發(fā)現(xiàn)isa指針中有一些變化,如下圖所示

-
其中
magic是59是由于將isa指針地址轉(zhuǎn)換為二進(jìn)制,從47(因?yàn)榍懊嬗?個(gè)位域,共占用47位,地址是從0開始)位開始讀取6位,再轉(zhuǎn)換為十進(jìn)制,如下圖所示magic是59的來源
isa 與 類 的關(guān)聯(lián)
cls 與 isa 關(guān)聯(lián)原理就是isa指針中的shiftcls位域中存儲(chǔ)了類信息,其中initInstanceIsa的過程是將 calloc 指針 和當(dāng)前的 類cls 關(guān)聯(lián)起來,有以下幾種驗(yàn)證方式:
1、:通過initIsa方法中的newisa.shiftcls = (uintptr_t)cls >> 3;驗(yàn)證
-
運(yùn)行至
newisa.shiftcls = (uintptr_t)cls >> 3;前一步,其中shiftcls存儲(chǔ)當(dāng)前類的值信息此時(shí)查看
cls,是LGPerson類-
shiftcls賦值的邏輯是將LGPerson進(jìn)行編碼后,右移3位
shiftcls的賦值中的cls
-
執(zhí)行l(wèi)ldb命令
p (uintptr_t)cls,結(jié)果為(uintptr_t) $2 = 4294975720,再右移三位,有以下兩種方式(任選其一),將得到536871965存儲(chǔ)到newisa的shiftcls中p (uintptr_t)cls >> 3-
通過上一步的結(jié)果
$2,執(zhí)行l(wèi)ldb命令p $2 >> 3isa右移3位
-
繼續(xù)執(zhí)行程序到
isa = newisa;部分,此時(shí)執(zhí)行p newisa執(zhí)行p的結(jié)果與
bits賦值結(jié)果的對(duì)比,bits的位域中有兩處變化cls由默認(rèn)值,變成了LGPerson,將isa與cls完美關(guān)聯(lián)-
shiftcls由0變成了536871965前后結(jié)果對(duì)比
所以isa中通過初始化后的成員的值變化過程,如下圖所示

為什么在shiftcls賦值時(shí)需要類型強(qiáng)轉(zhuǎn)?
因?yàn)?code>內(nèi)存的存儲(chǔ)不能存儲(chǔ)字符串,機(jī)器碼只能識(shí)別 0 、1這兩種數(shù)字,所以需要將其轉(zhuǎn)換為uintptr_t數(shù)據(jù)類型,這樣shiftcls中存儲(chǔ)的類信息才能被機(jī)器碼理解, 其中uintptr_t是long
為什么需要右移3位?
主要是由于shiftcls處于isa指針地址的中間部分,前面還有3個(gè)位域,為了不影響前面的3個(gè)位域的數(shù)據(jù),需要右移將其抹零。
2、:通過isa指針地址與ISA_MSAK 的值 & 來驗(yàn)證
在方式一后,繼續(xù)執(zhí)行,回到
_class_createInstanceFromZone方法,此時(shí)cls 與 isa已經(jīng)關(guān)聯(lián)完成,執(zhí)行po objc執(zhí)行
x/4gx obj,得到isa指針的地址0x001d8001000020e9-
將
isa指針地址 &ISA_MASK(處于macOS,使用x86_64中的宏定義),即po 0x001d8001000020e9 & 0x00007ffffffffff8,得出LGPersonarm64中,ISA_MASK 宏定義的值為0x0000000ffffffff8ULL-
x86_64中,ISA_MASK 宏定義的值為0x00007ffffffffff8ULLisa & ISA_MASK的結(jié)果
3、通過runtime的方法object_getClass驗(yàn)證
通過查看object_getClass的源碼實(shí)現(xiàn),同樣可以驗(yàn)證isa與類關(guān)聯(lián)的原理,有以下幾步:
main中導(dǎo)入#import <objc/runtime.h>
通過
runtime的api,即object_getClass函數(shù)獲取類信息
object_getClass(<#id _Nullable obj#>)
-
查看
object_getClass函數(shù) 源碼的實(shí)現(xiàn)objc4中查找object_getClass函數(shù)
-
點(diǎn)擊進(jìn)入
object_getClass底層實(shí)現(xiàn)object_getClass源碼實(shí)現(xiàn)
-
進(jìn)入
getIsa的源碼實(shí)現(xiàn)getIsa源碼實(shí)現(xiàn)
-
點(diǎn)擊
ISA(),進(jìn)入源碼,可以看到如果是indexed類型,執(zhí)行if流程,反之 執(zhí)行的是else流程ISA()源碼實(shí)現(xiàn)- 在
else流程中,拿到isa的bits這個(gè)位,再& ISA_MASK,這與方式二中的原理是一致的,獲得當(dāng)前的類信息 - 從這里也可以
得出 cls 與 isa 已經(jīng)完美關(guān)聯(lián)
- 在
4、:通過位運(yùn)算驗(yàn)證
-
回到
_class_createInstanceFromZone方法。通過x/4gx obj得到obj的存儲(chǔ)信息,當(dāng)前類的信息存儲(chǔ)在isa指針中,且isa中的shiftcls此時(shí)占44位(因?yàn)樘幱?code>macOS環(huán)境)obj內(nèi)存情況
-
想要
讀取中間的44位類信息,就需要經(jīng)過位運(yùn)算,將右邊3位,和左邊除去44位以外的部分都抹零,其相對(duì)位置是不變的。其位運(yùn)算過程如圖所示,其中shiftcls即為需要讀取的類信息位運(yùn)算計(jì)算過程將
isa地址右移3位:p/x 0x001d8001000020e9 >> 3,得到0x0003b0002000041d-
在將得到的
0x0003b0002000041d``左移20位:p/x 0x0003b0002000041d << 20,得到0x0002000041d00000- 為什么是
左移20位?因?yàn)橄?code>右移了3位,相當(dāng)于向右偏移了3位,而左邊需要抹零的位數(shù)有17位,所以一共需要移動(dòng)20位
- 為什么是
將得到的
0x0002000041d00000再右移17位:p/x 0x0002000041d00000 >> 17得到新的0x00000001000020e8
-
獲取cls的地址 與 上面的進(jìn)行驗(yàn)證 :
p/x cls也得出0x00000001000020e8,所以由此可以證明 cls 與 isa 是關(guān)聯(lián)的執(zhí)行p的結(jié)果




















