isa 與類關(guān)聯(lián)原理探究
在探究isa之前,先要理清一個(gè)概念:對(duì)象是什么?對(duì)象的本質(zhì)是什么?
首先來(lái)了解一下Clang:
Clang是由Apple主導(dǎo)編寫,是一個(gè)C語(yǔ)言、C++、Objective-C語(yǔ)言的輕量級(jí)編譯器。
因?yàn)?code>Objective-C是C、C++的超集,所以想要看到OC底層源碼結(jié)構(gòu),我們需要借助Clang 編譯器來(lái)查看底層實(shí)現(xiàn)。
1.對(duì)象的本質(zhì)探究
創(chuàng)建工程,在main.m中添加LGPerson 類,如下圖:

打開終端,定位到當(dāng)前main.m文件位置,在終端輸入clang -rewrite-objc main.m -o main.cpp,可以看到在main.m文件位置發(fā)現(xiàn)多了一個(gè)main.cpp文件:

打開main.cpp 文件,查找我們的LGPerson,如下所示:

- 從上圖可知,
LGPerson類對(duì)象在底層中被編譯成了一個(gè)struct結(jié)構(gòu)體。 - 因?yàn)樵?code>C++中結(jié)構(gòu)體是可以繼承的,
LGPerson_IMPL中的第一個(gè)屬性其實(shí)就是isa,是繼承自NSObject,屬于偽繼承,偽繼承的方式是直接將NSObject結(jié)構(gòu)體定義為LGPerson中的第一個(gè)屬性,意味著LGPerson擁有NSObject中的所有成員變量。
//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;
};
總結(jié)
-
OC對(duì)象的本質(zhì)就是結(jié)構(gòu)體. -
LGPerson中的isa就是繼承自NSObject中的isa.
2.objc_setProperty 探究
上面所展示的main.cpp中LGPerson相關(guān)信息中,我們還看到屬性變量name的set和get方法,如下圖所示:

可以看到,
set方法是通過(guò)Runtime 中的objc_setProperty方法實(shí)現(xiàn)的。現(xiàn)在我們來(lái)看看這個(gè)
objc_setProperty是如何一步一步實(shí)現(xiàn)的:
- 在
objc4_781源碼中全局搜索objc_setProperty,找到如下:
objc_setProperty實(shí)現(xiàn) - 接著,我們繼續(xù)進(jìn)入
reallySetProperty方法:reallySetProperty實(shí)現(xiàn)
通過(guò)查看objc_setProperty方法的實(shí)現(xiàn),發(fā)現(xiàn)上層屬性的set方法到底層的set方法經(jīng)過(guò)objc_setProperty方法處理之后,已經(jīng)失去了痕跡,只是帶進(jìn)來(lái)了每個(gè)set方法特有的_cmd,可想而知,objc_setProperty就是上層set和下層set的一個(gè)中間關(guān)聯(lián)層。
- objc_setProperty 是關(guān)聯(lián)上層set和下層set的一個(gè)中間接口。
- 這么設(shè)計(jì)的原因是,大量的上層
set會(huì)產(chǎn)生大量的臨時(shí)變量。 - 基于上述原因,蘋果采用了
適配器設(shè)計(jì)模式(即將底層接口適配為客戶端需要的接口),對(duì)外提供一個(gè)接口,供上層的set方法使用,對(duì)內(nèi)調(diào)用底層的set方法,使其相互不受影響,即無(wú)論上層怎么變,下層都是不變的,或者下層的變化也無(wú)法影響上層,主要是達(dá)到上下層接口隔離的目的。
大致可以用下圖表示上層、接口隔離層、底層關(guān)系:
上層、接口隔離層、底層關(guān)系
3. isa類型isa_t分析
在分析isa_t之前我們先了解一下聯(lián)合體和結(jié)構(gòu)體的區(qū)別:
結(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ì)將原來(lái)成員的值覆蓋掉。
缺點(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ì)影響其余所有成員
-
-
內(nèi)存分配大小
- 結(jié)構(gòu)體內(nèi)存
>=所有成員占用的內(nèi)存總和(成員之間可能會(huì)有縫隙) - 共用體占用的內(nèi)存
等于最大的成員占用的內(nèi)存
- 結(jié)構(gòu)體內(nèi)存
下面我們就來(lái)分析分析isa到底是啥:
union isa_t {
isa_t() { }
isa_t(uintptr_t value) : bits(value) { }
Class cls;
uintptr_t bits;
#if defined(ISA_BITFIELD)
struct {
ISA_BITFIELD; // defined in isa.h
};
#endif
};
isa的類型isa_t上圖所示,是一個(gè)union聯(lián)合體。使用聯(lián)合體的原因,是為了節(jié)省內(nèi)存,這里的內(nèi)存優(yōu)化是指在isa指針中通過(guò)char + 位域(即二進(jìn)制中每一位均可表示不同的信息)。
-
isa_t聯(lián)合體提供了兩個(gè)成員,cls和bits,他們的關(guān)系的互斥的,但是根據(jù)聯(lián)合體的定義(從前往后排列),初始化isa時(shí),會(huì)有兩種方式:- 通過(guò)
cls初始化,bits無(wú)默認(rèn)值。 - 通過(guò)
bits初始化,cls有默認(rèn)值。
- 通過(guò)
-
isa_t還提供了一個(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),以下是它們的一些宏定義,如下圖所示
ISA_BITFIELD結(jié)構(gòu)圖 -
nonpointer:表示是否對(duì)isa指針開啟指針優(yōu)化-
0:純isa指針 -
1:不止是類對(duì)象地址,isa中包含了類信息、對(duì)象的引用計(jì)數(shù)等
-
-
has_assoc:關(guān)聯(lián)對(duì)象標(biāo)志位-
0:沒有關(guān)聯(lián)對(duì)象 -
1:存在關(guān)聯(lián)對(duì)象
-
has_cxx_dtor:該對(duì)象是否有C++或者Objc的析構(gòu)器,如果有析構(gòu)函數(shù),在對(duì)象釋放的時(shí)候就需要做析構(gòu)處理,沒有的話則會(huì)釋放的更快shiftcls:存儲(chǔ)類指針的值。開啟指針優(yōu)化的情況下,在arm64架構(gòu)中有33位用來(lái)存儲(chǔ)類指針magic:用于調(diào)試器判斷當(dāng)前對(duì)象是真的對(duì)象還是沒有初始化的空間weakly_referenced:此對(duì)象是否指向或者曾經(jīng)指向一個(gè)ARC的弱變量,沒有弱引用的話,能更快地釋放對(duì)象deallocation:標(biāo)志對(duì)象是否正在釋放內(nèi)存has_sidetable_rc:當(dāng)對(duì)象引用計(jì)數(shù)大于10時(shí),則需要借用該變量存儲(chǔ)計(jì)數(shù)extra_rc:當(dāng)表示該對(duì)象的引用計(jì)數(shù)值,實(shí)際上是引用計(jì)數(shù)值減1,例如,如果對(duì)象的引用計(jì)數(shù)為10,那么extra_rc為9。如果引用計(jì)數(shù)大于10,則需要使用到下面的has_sidetable_rc。
4.isa初始化探究
下面我們就來(lái)看看isa_t類型的isa初始化情況,跟蹤源碼alloc底層實(shí)現(xiàn)我們來(lái)到initInstanceIsa方法,
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_t((uintptr_t)cls);//通過(guò) cls 初始化isa
} else {
ASSERT(!DisableNonpointerIsa);
ASSERT(!cls->instancesRequireRawIsa());
isa_t newisa(0);//通過(guò)
#if SUPPORT_INDEXED_ISA // 0
ASSERT(cls->classArrayIndex() > 0);
newisa.bits = ISA_INDEX_MAGIC_VALUE;
// isa.magic is part of ISA_MAGIC_VALUE
// isa.nonpointer is part of ISA_MAGIC_VALUE
newisa.has_cxx_dtor = hasCxxDtor;
newisa.indexcls = (uintptr_t)cls->classArrayIndex();
#else //進(jìn)入下面代碼
newisa.bits = ISA_MAGIC_VALUE; //初始化 bits 信息
// isa.magic is part of ISA_MAGIC_VALUE
// isa.nonpointer is part of ISA_MAGIC_VALUE
newisa.has_cxx_dtor = hasCxxDtor;
newisa.shiftcls = (uintptr_t)cls >> 3;//初始化 shiftcls信息
#endif
// This write must be performed in a single store in some cases
// (for example when realizing a class because other threads
// may simultaneously try to use the class).
// fixme use atomics here to guarantee single-store and to
// guarantee memory order w.r.t. the class index table
// ...but not too atomic because we don't want to hurt instantiation
isa = newisa;
}
}
initIsa 方法就是通過(guò)兩種不同的初始化方式來(lái)初始化isa,根據(jù)nonpointer來(lái)判斷。
5.isa與類的關(guān)聯(lián)
調(diào)試跟蹤上圖的isa初始化過(guò)程,可以分析處isa是如何一步一步與類關(guān)聯(lián)起來(lái)的,首先打下斷點(diǎn),一步一步調(diào)試:

斷點(diǎn)p打印結(jié)果分別為:
(lldb) p newisa
(isa_t) $0 = {
cls = nil
bits = 0
= {
nonpointer = 0
has_assoc = 0
has_cxx_dtor = 0
shiftcls = 0
magic = 0
weakly_referenced = 0
deallocating = 0
has_sidetable_rc = 0
extra_rc = 0
}
}
(lldb) p newisa
(isa_t) $1 = {
cls = 0x001d800000000001
bits = 8303511812964353
= {
nonpointer = 1
has_assoc = 0
has_cxx_dtor = 0
shiftcls = 0
magic = 59
weakly_referenced = 0
deallocating = 0
has_sidetable_rc = 0
extra_rc = 0
}
}
(lldb) p newisa
(isa_t) $2 = {
cls = LGPerson
bits = 8303516107940081
= {
nonpointer = 1
has_assoc = 0
has_cxx_dtor = 0
shiftcls = 536871966
magic = 59
weakly_referenced = 0
deallocating = 0
has_sidetable_rc = 0
extra_rc = 0
}
}
三步打印結(jié)果為下圖所示:

綜上所述,最后一個(gè)斷點(diǎn)即isa初始化結(jié)束時(shí),已經(jīng)可以看出isa的cls即為L(zhǎng)GPerson,且isa中的指針的shiftcls中存儲(chǔ)了類的信息,那接下來(lái)我們可以通過(guò)幾種方式驗(yàn)證一下:
1.通過(guò)initIsa方法中的newisa.shiftcls = (uintptr_t)cls >> 3;驗(yàn)證
2.通過(guò)isa指針地址與ISA_MSAK 的值 & 來(lái)驗(yàn)證
3.通過(guò)runtime的方法object_getClass驗(yàn)證
4.通過(guò)位運(yùn)算驗(yàn)證
方式一:斷點(diǎn)停在1位置,lldb命令p打印(uintptr_t)cls及 (uintptr_t)cls >> 3,然后繼續(xù)停在斷點(diǎn)3位置,p newisa發(fā)現(xiàn)此時(shí)isa中的shiftcls的值剛好與(uintptr_t)cls >> 3 相等,而且cls與LGPerson綁定上了。

拓展:
為什么在shiftcls賦值時(shí)需要類型強(qiáng)轉(zhuǎn)?
因?yàn)閮?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ù),需要右移將其抹零。
方式二:回到_class_createInstanceFromZone方法中,我們?cè)?code>return obj前打下斷點(diǎn):

利用
x/4gx 獲取當(dāng)前obj的指針地址,然后進(jìn)行& ISA_MASK 操作。
方式三:通過(guò)runtime的api,即object_getClass函數(shù)獲取類信息
查看object_getClass函數(shù) 源碼的實(shí)現(xiàn):
Class object_getClass(id obj)
{
if (obj) return obj->getIsa();
else return Nil;
}
inline Class
objc_object::getIsa()
{
if (fastpath(!isTaggedPointer())) return ISA();
extern objc_class OBJC_CLASS_$___NSUnrecognizedTaggedPointer;
uintptr_t slot, ptr = (uintptr_t)this;
Class cls;
slot = (ptr >> _OBJC_TAG_SLOT_SHIFT) & _OBJC_TAG_SLOT_MASK;
cls = objc_tag_classes[slot];
if (slowpath(cls == (Class)&OBJC_CLASS_$___NSUnrecognizedTaggedPointer)) {
slot = (ptr >> _OBJC_TAG_EXT_SLOT_SHIFT) & _OBJC_TAG_EXT_SLOT_MASK;
cls = objc_tag_ext_classes[slot];
}
return cls;
}
inline Class
objc_object::ISA()
{
ASSERT(!isTaggedPointer());
#if SUPPORT_INDEXED_ISA
if (isa.nonpointer) {
uintptr_t slot = isa.indexcls;
return classForIndex((unsigned)slot);
}
return (Class)isa.bits;
#else
return (Class)(isa.bits & ISA_MASK);
#endif
}
(Class)(isa.bits & ISA_MASK);, 強(qiáng)轉(zhuǎn) 而且 此步還進(jìn)行了 & 操作,這與方式二中的原理是一致的,獲得當(dāng)前的類信息
方式四:位運(yùn)算
回到_class_createInstanceFromZone方法中,x/4gx打印此時(shí)obj的存儲(chǔ)信息,此時(shí)isa中的shiftcls此時(shí)占44位(因?yàn)樘幱?code>macOS環(huán)境

- 首先,通過(guò)
x/4gx obj獲取當(dāng)前obj的存儲(chǔ)信息,拿到指針地址0x001d8001000020f1 -
p/x 0x001d8001000020f1 >> 3右移三位,右邊三位抹0 -
p/x 0x0003b0002000041e << 20左移20位,因?yàn)橹坝乙?code>3位后,最左邊補(bǔ)了3位0,加上之前的17位,所以想要抹掉左邊的,必須左移20位 -
p/x 0x0002000041e00000 >> 17最后右移17,使存儲(chǔ)信息的shiftcls回到原來(lái)的位置。
最后利用p/x cls打印結(jié)果地址剛好與上方位運(yùn)算結(jié)果一致。



