本文的目的主要是理解類與 isa 是如何關(guān)聯(lián)的
在介紹正文之前,首先要理解一個(gè)概念:oc 對(duì)象的本質(zhì)是什么
OC 對(duì)象的本質(zhì)
在探索本質(zhì)之前,先了解一個(gè)編譯器 clang
Clang
-
是有 Apple 主導(dǎo)編寫,基于 LLVM 和 c/c++/oc 的編譯器
- 主要是用于底層編譯,將一些文件輸出成 c++文件,例如 main.m 輸出成 main.cpp,其目的是為了更好的觀察底層實(shí)現(xiàn)邏輯.
探索對(duì)象本質(zhì)
- 在 main 中自定義一個(gè) LGPerson 類,寫一個(gè) name 屬性
@interface LGPerson : NSObject
@property(nonatomic,copy)NSString * name;
@end
@implementation LGPerson
@end
- 通過(guò)終端,將 main.m 編譯成 main.cpp 文件,可以通過(guò)以下幾種方式
//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
//以下兩種方式是通過(guò)指定架構(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
- 打開 cpp 文件,找到 LGPerson 的定義,發(fā)現(xiàn)被定義成了一個(gè)結(jié)構(gòu)體
- LGPerson_IMPL 中的第一個(gè)屬性其實(shí)就是 isa,是繼承自 NSObject,屬于偽繼承,偽繼承的方式就是把NSObject_IMPL定義為 LGPerson_IMPL 的第一個(gè)屬性,意味著 LGPerson 就有了 NSObject 的所有屬性
struct LGPerson_IMPL {
struct NSObject_IMPL NSObject_IVARS;
NSString *_name;
};
struct NSObject_IMPL {
Class isa;
};
通過(guò)以上分析,理解了 oc 對(duì)象的本質(zhì),但是有一個(gè)疑問,為什么 isa 的類型是 Class???
- 在底層 2中分析過(guò),alloc 核心方法之一initInstanceIsa,通過(guò)這個(gè)方法的實(shí)現(xiàn),isa 是一個(gè) isa_t 類型
- 而在 NSObject 中定義的 isa 是 class 類型,根本原因在于,isa 對(duì)外反饋的是類信息,為了讓開發(fā)人員更加清晰明確,在 isa 返回時(shí)做了一個(gè)強(qiáng)制類型轉(zhuǎn)換,源碼中的強(qiáng)轉(zhuǎn)如下
inline Class
isa_t::getDecodedClass(bool authenticated) {
#if SUPPORT_INDEXED_ISA
if (nonpointer) {
return classForIndex(indexcls);
}
return (Class)cls;
#else
return getClass(authenticated);
#endif
}
inline Class
objc_object::ISA(bool authenticated)
{
ASSERT(!isTaggedPointer());
return isa.getDecodedClass(authenticated);
}

總結(jié)
- oc 對(duì)象的本質(zhì)是結(jié)構(gòu)體
- LGPerson 中 isa 繼承自 NSObject 中的 isa
objc_setProperty 源碼探索
除了 LGPerson 的底層定義,我們還發(fā)現(xiàn)了 name 的 set 和 get 方法定義,其中 set 方法依賴objc_setProperty

下面就來(lái)探索 objc_setProperty 源碼
- 在源碼中搜索 objc_setProperty,找到源碼實(shí)現(xiàn)
void objc_setProperty(id self, SEL _cmd, ptrdiff_t offset, id newValue, BOOL atomic, signed char shouldCopy)
{
bool copy = (shouldCopy && shouldCopy != MUTABLE_COPY);
bool mutableCopy = (shouldCopy == MUTABLE_COPY);
reallySetProperty(self, _cmd, newValue, offset, atomic, copy, mutableCopy);
}
-
進(jìn)入reallySetProperty源碼實(shí)現(xiàn),其方法主要是新值 retain,舊值 release
image.png
總結(jié)
通過(guò) objc_setProperty 源碼探索,有幾下幾點(diǎn)說(shuō)明
- object_setProperty 主要作用就是關(guān)聯(lián)上層 set 方法以及底層reallySetProperty方法,作為一個(gè)中間層
- 設(shè)計(jì)的原因.上層的 set 方法有很多,如果直接調(diào)用底層方法,會(huì)產(chǎn)生很多的臨時(shí)變量,當(dāng)你想查找一個(gè) sel 的時(shí)候,會(huì)非常麻煩
-
所以蘋果采用了適配器設(shè)計(jì)模式(將底層接口適配為客戶端需要的接口),對(duì)外提供一個(gè)接口,供上層使用,對(duì)內(nèi)調(diào)用底層的 set 方法,使其相互不受影響,無(wú)論上層怎么變,下層都不變,主要達(dá)到一個(gè)上下層隔離的目的
下圖代表,上層,隔離層,底層的關(guān)系
未命名文件.png
cls 與類的關(guān)聯(lián)原理
探索出發(fā)點(diǎn)就是initInstanceIsa函數(shù), 探究 isa 與類是如何關(guān)聯(lián)到一起的
在這之前需要了解一個(gè)聯(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é)的空間,但在使用的時(shí)候,你只用了 4 個(gè), 就會(huì)有 12 字節(jié)浪費(fèi)掉了
- 優(yōu)點(diǎn):存儲(chǔ)量大,包容性強(qiáng), 互相之間不影響
聯(lián)合體
聯(lián)合體也是由不同的數(shù)據(jù)類型組成,但其變量是互斥的,所有成員共占同一塊內(nèi)存,而且共用體采用了內(nèi)存覆蓋覆蓋技術(shù),同一時(shí)刻只能保存一個(gè)成員的值,如果對(duì)新的成員賦值,就會(huì)把原來(lái)成員的值覆蓋掉
- 缺點(diǎn):包容性弱
- 優(yōu)點(diǎn):所有成員共用一段內(nèi)存,節(jié)省了內(nèi)存空間
兩者的區(qū)別
- 內(nèi)存占用情況
- 結(jié)構(gòu)體的各個(gè)成員占用不同的內(nèi)存,互相不影響
- 共用體各個(gè)成員占用同一段內(nèi)存,修改其中一個(gè)成員,會(huì)影響其他所有成員 - 內(nèi)存分配大小
- 結(jié)構(gòu)體的內(nèi)存 >= 內(nèi)部所有成員內(nèi)存相加(中間會(huì)有間隙)
- 共用體占用的內(nèi)存 == 其內(nèi)部最大成員占用的內(nèi)存
isa 的類型 isa_t
以下是 isa 指針的類型 isa_t 的定義,從定義可以看出是通過(guò)聯(lián)合體(union)定義的
union isa_t {
isa_t() { }
isa_t(uintptr_t value) : bits(value) { }
uintptr_t bits;
private:
// Accessing the class requires custom ptrauth operations, so
// force clients to go through setClass/getClass by making this
// private.
Class cls;
public:
#if defined(ISA_BITFIELD)
struct {
ISA_BITFIELD; // defined in isa.h
};
bool isDeallocating() {
return extra_rc == 0 && has_sidetable_rc == 0;
}
void setDeallocating() {
extra_rc = 0;
has_sidetable_rc = 0;
}
#endif
void setClass(Class cls, objc_object *obj);
Class getClass(bool authenticated);
Class getDecodedClass(bool authenticated);
};
isa類型使用聯(lián)合體的原因也是基于內(nèi)存優(yōu)化考慮的,這里的內(nèi)存優(yōu)化是指在 isa 指針中通過(guò)char+位域(即二進(jìn)制中的每一位都可以代表不同的信息)的原理實(shí)現(xiàn),通常來(lái)說(shuō) isa 指針占用的內(nèi)存大小是 8 字節(jié),也就是 64 位,足夠存儲(chǔ)很多信息了,這樣可以極高的節(jié)省內(nèi)存
從 isa 的定義中可以看出
- 提供了兩個(gè)成員 cls 和 bits,由聯(lián)合體的定義可知,這兩個(gè)成員是互斥的,也就意味著,初始化 isa 指針,有兩種方式
- 通過(guò) cls 初始化,bits 無(wú)值
- 通過(guò) bits 初始化,cls 無(wú)值
還提供了一個(gè)結(jié)構(gòu)體定義的位域,用于存儲(chǔ)類信息和其他信息,結(jié)構(gòu)體的成員ISA_BITFIELD這是一個(gè)宏定義,有兩個(gè)版本,arm64(對(duì)應(yīng) ios 移動(dòng)端)和x86_64(macos)
# else
# define ISA_MASK 0x0000000ffffffff8ULL
# define ISA_MAGIC_MASK 0x000003f000000001ULL
# define ISA_MAGIC_VALUE 0x000001a000000001ULL
# define ISA_HAS_CXX_DTOR_BIT 1
# define ISA_BITFIELD \
uintptr_t nonpointer : 1; //是否對(duì) isa 指針開啟了指針優(yōu)化
uintptr_t has_assoc : 1; //是否有關(guān)聯(lián)對(duì)象
uintptr_t has_cxx_dtor : 1; //是否有 c++實(shí)現(xiàn)
uintptr_t shiftcls : 33; //存儲(chǔ)類信息
uintptr_t magic : 6; //調(diào)試器判斷對(duì)象是真對(duì)象還是未初始化空間
uintptr_t weakly_referenced : 1; //對(duì)象是否被指向或者曾經(jīng)指向一個(gè)ARC 弱變量
uintptr_t unused : 1; \
uintptr_t has_sidetable_rc : 1; //是否有外掛的散列表
uintptr_t extra_rc : 19//額外的引用計(jì)數(shù)
# define RC_ONE (1ULL<<45)
# define RC_HALF (1ULL<<18)
# endif
# elif __x86_64__
# define ISA_MASK 0x00007ffffffffff8ULL
# define ISA_MAGIC_MASK 0x001f800000000001ULL
# define ISA_MAGIC_VALUE 0x001d800000000001ULL
# define ISA_HAS_CXX_DTOR_BIT 1
# 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 unused : 1; \
uintptr_t has_sidetable_rc : 1; \
uintptr_t extra_rc : 8
- nonpointer有兩個(gè)值,標(biāo)識(shí)自定義的類等,占 1 位
- 0:純 isa 指針
- 1:不只是類對(duì)象地址,還包含了類信息,對(duì)象的引用計(jì)數(shù)等
- has_assoc:標(biāo)識(shí)關(guān)聯(lián)對(duì)象標(biāo)志,占一位
- 0:沒有關(guān)聯(lián)對(duì)象
- 1:有關(guān)聯(lián)對(duì)象
- has_cxx_dtor:表示該對(duì)象是否有 c++/oc 的析構(gòu)器(dealloc),占 1 位
- 如果有,做析構(gòu)邏輯
- 如果沒有,可以更快的釋放對(duì)象
- shiftcls:表示存儲(chǔ)類的指針值,即類信息
- arm64 占 33 位,開啟指針優(yōu)化的情況下,在 arm64 架構(gòu)下有 33 位存儲(chǔ)類指針
- x86_64 占 44 位,
- magic:用于調(diào)試器判斷當(dāng)前對(duì)象是真對(duì)象還是未初始化空間,占 6 位
- weakly_referenced:代表對(duì)象是否被指向或者曾經(jīng)指向一個(gè)ARC 的弱變量
- 沒有弱引用的變量可以更快的釋放
- has_sidetable_rc:表示當(dāng)對(duì)象的引用計(jì)數(shù)大于 10(舉例而已,不一定是 10),則需要借用該變量存儲(chǔ)進(jìn)位
- extra_rc:額外的引用計(jì)數(shù),表示該對(duì)象的引用計(jì)數(shù)值,實(shí)際是引用計(jì)數(shù)值減 1,
-
如果對(duì)象的引用計(jì)數(shù)為 10,那么 extra_rc 的值為 9(舉例而已),實(shí)際上 iphone 真機(jī)上的exra_rc 是使用 19 位來(lái)存儲(chǔ)引用計(jì)數(shù)的
針對(duì) arm64 平臺(tái) isa存儲(chǔ)情況如下
未命名文件-2.png
-
原理探索
- 通過(guò)alloc->_objc_rootAlloc->callAlloc->_objc_rootAllocWithZone->_class_createInstanceFromZone->initInstanceIsa進(jìn)入查看實(shí)現(xiàn)
objc_object::initInstanceIsa(Class cls, bool hasCxxDtor)
{
ASSERT(!cls->instancesRequireRawIsa());
ASSERT(hasCxxDtor == cls->hasCxxDtor());
initIsa(cls, true, hasCxxDtor);
}
-
進(jìn)入 initIsa 實(shí)現(xiàn),主要是初始化 isa 指針
image.png
該方法的邏輯主要分為兩部分
- 1.通過(guò) cls 初始化 isa
- 2.通過(guò) bits 初始化 isa
驗(yàn)證 isa 指針 位域(0-64)
根據(jù)前面提到的位域信息,可以在這里驗(yàn)證一下位域是真的存在的,在newisa.bits 處打一個(gè)斷點(diǎn)

在執(zhí)行到這句代碼是,通過(guò) lldb 打印p newisa, 然后走到下一行在打印一次 newisa,得到的信息如下圖

通過(guò)與前一個(gè)newisa 相比,后一個(gè)的nonpointer變成了 1, magic變成了 59,
-
其中的 59 是十進(jìn)制的體現(xiàn), 把 isa 指針從 47 位(x86 下,前面的位域占47 位)開始讀取 6 位,在轉(zhuǎn)成 10 進(jìn)制,就是 59
未命名文件.png
isa 與類的關(guān)聯(lián)
cls 與 isa 關(guān)聯(lián)的原理,就是 isa 中的 shiftcls 位域存儲(chǔ)了類信息,其中initInstanceIsa的過(guò)程是將calloc 指針與當(dāng)前類關(guān)聯(lián)起來(lái),有以下幾種驗(yàn)證方式
- 1.通過(guò) initIsa 中的newisa.shiftcls = (uintptr_t)cls >> 3驗(yàn)證
- 2.通過(guò) isa 指針地址與 ISA_MASK 值 & 驗(yàn)證
- 3.通過(guò) runtime 的方法 object_getClass 驗(yàn)證
- 4.通過(guò)位運(yùn)算驗(yàn)證
1.通過(guò) initIsa
- 運(yùn)行到shiftcls = (uintptr_t)newCls >> 3,其中 shiftcls 存儲(chǔ)的是當(dāng)前類的值信息
- 查看 newCls 信息是 SATest類
-
shiftCls 賦值的邏輯是將編碼后的 SATest 數(shù)據(jù)右移3 位
image.png
-
執(zhí)行 lldb 指令, 打印p (uintptr_t)newCls >> 3得到值存儲(chǔ)到 shiftCls 中
image.png -
繼續(xù)執(zhí)行到isa = newisa; 打印 p newisa
image.png
與bits 賦值結(jié)果對(duì)比,bits 位域中有兩處變化
- cls 由默認(rèn)值變成了 SATest, isa 與類完美關(guān)聯(lián)
- shiftCls 從 0 變成了有值
為什么在shiftcls 賦值時(shí)需要強(qiáng)轉(zhuǎn)
因?yàn)閮?nèi)存存儲(chǔ)時(shí),不能存儲(chǔ)字符串,機(jī)器碼只能識(shí)別 0 和 1 這兩種數(shù)字,所以需要將其轉(zhuǎn)換為uintptr_t數(shù)據(jù),這樣 shiftcls 中的數(shù)據(jù)才能被機(jī)器識(shí)別,其中uintptr_t為 long
為什么需要右移 3 位
因?yàn)?shiftcls 處于 isa 中間部分,前面還有 3 個(gè)位域,為了不影響前面 3 個(gè)位域,需要右移將其抹零
方式 2:通過(guò) isa & ISA_MASK
- 在 main 中斷點(diǎn)到 SATest 創(chuàng)建時(shí),按照下圖方式進(jìn)行打印
arm64 中 ISA_MASK 為0x0000000ffffffff8ULL
x86 中 ISA_MASK 為0x00007ffffffffff8ULL
image.png
方式 3 通過(guò) runtime中的函數(shù) object_getClass
-
查找 object_getClass源碼實(shí)現(xiàn)
image.png -
進(jìn)入getIsa實(shí)現(xiàn)
image.png -
進(jìn)入 ISA()實(shí)現(xiàn)
image.png -
進(jìn)入getDecodedClass實(shí)現(xiàn)
image.png -
進(jìn)入getClass實(shí)現(xiàn)
image.png
方式 4:通過(guò)位運(yùn)算
-
在 main 中 SATest 創(chuàng)建處加一個(gè)斷點(diǎn),通過(guò)x/4gx test打印test 存儲(chǔ)信息,當(dāng)前類的信息存儲(chǔ)在 isa 指針中,切此時(shí)的 shiftcls 占 44 位(因?yàn)樵?macos 環(huán)境下)
image.png -
想要讀取中間的 44 位信息,就需要經(jīng)過(guò)位運(yùn)算,將 shiftcls 右邊的 3 位和左邊的 17 位都要抹零,相對(duì)位置不能變,分為如下幾步
- 1.先將 isa >> 3: 將前三位抹零
- 2. 然后用第一步的結(jié)果 << 20 (本身左邊是 17 位,但是經(jīng)過(guò)第一步以后,左邊變成了 20 位)
- 3.第二步的結(jié)果 >> 17(回到最初 shiftcls 在 isa 中的初始位置,此時(shí)左右已經(jīng)全部抹零)
image.png















