本文的目的主要是理解類與 isa 是如何關聯(lián)的
在介紹正文之前,首先要理解一個概念:oc 對象的本質(zhì)是什么
OC 對象的本質(zhì)
在探索本質(zhì)之前,先了解一個編譯器 clang
Clang
-
是有 Apple 主導編寫,基于 LLVM 和 c/c++/oc 的編譯器
- 主要是用于底層編譯,將一些文件輸出成 c++文件,例如 main.m 輸出成 main.cpp,其目的是為了更好的觀察底層實現(xiàn)邏輯.
探索對象本質(zhì)
- 在 main 中自定義一個 LGPerson 類,寫一個 name 屬性
@interface LGPerson : NSObject
@property(nonatomic,copy)NSString * name;
@end
@implementation LGPerson
@end
- 通過終端,將 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
//以下兩種方式是通過指定架構模式的命令行,使用xcode工具 xcrun
//3、模擬器文件編譯
- xcrun -sdk iphonesimulator clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp
//4、真機文件編譯
- xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main- arm64.cpp
- 打開 cpp 文件,找到 LGPerson 的定義,發(fā)現(xiàn)被定義成了一個結(jié)構體
- LGPerson_IMPL 中的第一個屬性其實就是 isa,是繼承自 NSObject,屬于偽繼承,偽繼承的方式就是把NSObject_IMPL定義為 LGPerson_IMPL 的第一個屬性,意味著 LGPerson 就有了 NSObject 的所有屬性
struct LGPerson_IMPL {
struct NSObject_IMPL NSObject_IVARS;
NSString *_name;
};
struct NSObject_IMPL {
Class isa;
};
通過以上分析,理解了 oc 對象的本質(zhì),但是有一個疑問,為什么 isa 的類型是 Class???
- 在底層 2中分析過,alloc 核心方法之一initInstanceIsa,通過這個方法的實現(xiàn),isa 是一個 isa_t 類型
- 而在 NSObject 中定義的 isa 是 class 類型,根本原因在于,isa 對外反饋的是類信息,為了讓開發(fā)人員更加清晰明確,在 isa 返回時做了一個強制類型轉(zhuǎn)換,源碼中的強轉(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 對象的本質(zhì)是結(jié)構體
- LGPerson 中 isa 繼承自 NSObject 中的 isa
objc_setProperty 源碼探索
除了 LGPerson 的底層定義,我們還發(fā)現(xiàn)了 name 的 set 和 get 方法定義,其中 set 方法依賴objc_setProperty

下面就來探索 objc_setProperty 源碼
- 在源碼中搜索 objc_setProperty,找到源碼實現(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);
}
-
進入reallySetProperty源碼實現(xiàn),其方法主要是新值 retain,舊值 release
image.png
總結(jié)
通過 objc_setProperty 源碼探索,有幾下幾點說明
- object_setProperty 主要作用就是關聯(lián)上層 set 方法以及底層reallySetProperty方法,作為一個中間層
- 設計的原因.上層的 set 方法有很多,如果直接調(diào)用底層方法,會產(chǎn)生很多的臨時變量,當你想查找一個 sel 的時候,會非常麻煩
-
所以蘋果采用了適配器設計模式(將底層接口適配為客戶端需要的接口),對外提供一個接口,供上層使用,對內(nèi)調(diào)用底層的 set 方法,使其相互不受影響,無論上層怎么變,下層都不變,主要達到一個上下層隔離的目的
下圖代表,上層,隔離層,底層的關系
未命名文件.png
cls 與類的關聯(lián)原理
探索出發(fā)點就是initInstanceIsa函數(shù), 探究 isa 與類是如何關聯(lián)到一起的
在這之前需要了解一個聯(lián)合體, 為什么 isa 的類型 isa_t 是聯(lián)合體類型
聯(lián)合體union
構造數(shù)據(jù)類型的方式有兩種
- 結(jié)構體(struct)
- 聯(lián)合體(union,也叫共用體)
結(jié)構體
結(jié)構體是指把不同的數(shù)據(jù)組合成一個整體,其變量是共存的,變量不管是否使用,都會分配內(nèi)存
- 缺點:所有變量都分配內(nèi)存,比較浪費內(nèi)存,假設有 4 個 int 成員,一共分配了 16 字節(jié)的空間,但在使用的時候,你只用了 4 個, 就會有 12 字節(jié)浪費掉了
- 優(yōu)點:存儲量大,包容性強, 互相之間不影響
聯(lián)合體
聯(lián)合體也是由不同的數(shù)據(jù)類型組成,但其變量是互斥的,所有成員共占同一塊內(nèi)存,而且共用體采用了內(nèi)存覆蓋覆蓋技術,同一時刻只能保存一個成員的值,如果對新的成員賦值,就會把原來成員的值覆蓋掉
- 缺點:包容性弱
- 優(yōu)點:所有成員共用一段內(nèi)存,節(jié)省了內(nèi)存空間
兩者的區(qū)別
- 內(nèi)存占用情況
- 結(jié)構體的各個成員占用不同的內(nèi)存,互相不影響
- 共用體各個成員占用同一段內(nèi)存,修改其中一個成員,會影響其他所有成員 - 內(nèi)存分配大小
- 結(jié)構體的內(nèi)存 >= 內(nèi)部所有成員內(nèi)存相加(中間會有間隙)
- 共用體占用的內(nèi)存 == 其內(nèi)部最大成員占用的內(nèi)存
isa 的類型 isa_t
以下是 isa 指針的類型 isa_t 的定義,從定義可以看出是通過聯(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 指針中通過char+位域(即二進制中的每一位都可以代表不同的信息)的原理實現(xiàn),通常來說 isa 指針占用的內(nèi)存大小是 8 字節(jié),也就是 64 位,足夠存儲很多信息了,這樣可以極高的節(jié)省內(nèi)存
從 isa 的定義中可以看出
- 提供了兩個成員 cls 和 bits,由聯(lián)合體的定義可知,這兩個成員是互斥的,也就意味著,初始化 isa 指針,有兩種方式
- 通過 cls 初始化,bits 無值
- 通過 bits 初始化,cls 無值
還提供了一個結(jié)構體定義的位域,用于存儲類信息和其他信息,結(jié)構體的成員ISA_BITFIELD這是一個宏定義,有兩個版本,arm64(對應 ios 移動端)和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; //是否對 isa 指針開啟了指針優(yōu)化
uintptr_t has_assoc : 1; //是否有關聯(lián)對象
uintptr_t has_cxx_dtor : 1; //是否有 c++實現(xiàn)
uintptr_t shiftcls : 33; //存儲類信息
uintptr_t magic : 6; //調(diào)試器判斷對象是真對象還是未初始化空間
uintptr_t weakly_referenced : 1; //對象是否被指向或者曾經(jīng)指向一個ARC 弱變量
uintptr_t unused : 1; \
uintptr_t has_sidetable_rc : 1; //是否有外掛的散列表
uintptr_t extra_rc : 19//額外的引用計數(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有兩個值,標識自定義的類等,占 1 位
- 0:純 isa 指針
- 1:不只是類對象地址,還包含了類信息,對象的引用計數(shù)等
- has_assoc:標識關聯(lián)對象標志,占一位
- 0:沒有關聯(lián)對象
- 1:有關聯(lián)對象
- has_cxx_dtor:表示該對象是否有 c++/oc 的析構器(dealloc),占 1 位
- 如果有,做析構邏輯
- 如果沒有,可以更快的釋放對象
- shiftcls:表示存儲類的指針值,即類信息
- arm64 占 33 位,開啟指針優(yōu)化的情況下,在 arm64 架構下有 33 位存儲類指針
- x86_64 占 44 位,
- magic:用于調(diào)試器判斷當前對象是真對象還是未初始化空間,占 6 位
- weakly_referenced:代表對象是否被指向或者曾經(jīng)指向一個ARC 的弱變量
- 沒有弱引用的變量可以更快的釋放
- has_sidetable_rc:表示當對象的引用計數(shù)大于 10(舉例而已,不一定是 10),則需要借用該變量存儲進位
- extra_rc:額外的引用計數(shù),表示該對象的引用計數(shù)值,實際是引用計數(shù)值減 1,
-
如果對象的引用計數(shù)為 10,那么 extra_rc 的值為 9(舉例而已),實際上 iphone 真機上的exra_rc 是使用 19 位來存儲引用計數(shù)的
針對 arm64 平臺 isa存儲情況如下
未命名文件-2.png
-
原理探索
- 通過alloc->_objc_rootAlloc->callAlloc->_objc_rootAllocWithZone->_class_createInstanceFromZone->initInstanceIsa進入查看實現(xiàn)
objc_object::initInstanceIsa(Class cls, bool hasCxxDtor)
{
ASSERT(!cls->instancesRequireRawIsa());
ASSERT(hasCxxDtor == cls->hasCxxDtor());
initIsa(cls, true, hasCxxDtor);
}
-
進入 initIsa 實現(xiàn),主要是初始化 isa 指針
image.png
該方法的邏輯主要分為兩部分
- 1.通過 cls 初始化 isa
- 2.通過 bits 初始化 isa
驗證 isa 指針 位域(0-64)
根據(jù)前面提到的位域信息,可以在這里驗證一下位域是真的存在的,在newisa.bits 處打一個斷點

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

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















