1. 前言
上一篇我們了解到了一個(gè)對(duì)象的屬性?xún)?nèi)存分配和占用情況。并且額外引入了兩個(gè)結(jié)構(gòu)體做了對(duì)比。我們發(fā)現(xiàn)類(lèi)跟結(jié)構(gòu)體好像有什么相似的地方。那到底有什么相似的呢。話(huà)不多說(shuō),肝著。
1.1Clang
首先clang是一個(gè)由Apple主導(dǎo)編寫(xiě),基于LLVM的C/C++/OC的編譯器,這貨干啥的呢?
主要用途可以將你編寫(xiě)的類(lèi)輸出成較為低一級(jí)別的代碼,第一天玩人(Person)。第二天玩狗(Dog),今天我們來(lái)當(dāng)許仙。一起來(lái)玩蛇(Snake)??,例如將你Snake.m 輸出為Snake.cpp,這樣一來(lái)就可以更直觀的觀察到代碼還做了哪些你不知道的事情。直接上碼
@interface Snake ()
@property (nonatomic, copy) NSString *name;
@end
@implementation Snake
@end
通過(guò)終端,利用 clang 將 Snake.m 編譯成 Snake.cpp,有以下幾種編譯命令,這里使用的是第一種
//1、將 Snake.m 編譯成 Snake.cpp
clang -rewrite-objc Snake.m -o Snake.cpp
//2、將 ViewController.m 編譯成 ViewController.cpp
**這里要注意`iPhoneSimulator13.7`這個(gè)目錄一定要跟你本地的目錄對(duì)應(yīng)上**
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 Snake.m -o Snake-arm64.cpp
//4、真機(jī)文件編譯
- xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc Snake.m -o Snake- arm64.cpp
之后我們會(huì)在同級(jí)文件看到Snake.cpp文件。打開(kāi)之后是不是很驚喜有上萬(wàn)行代碼。驚不驚喜,意不意外。
我們?nèi)炙阉髦豢次覀冴P(guān)心部分。
extern "C" unsigned long OBJC_IVAR_$_Snake$_name;
struct Snake_IMPL {
struct NSObject_IMPL NSObject_IVARS;
NSString *_name;
};
/* @end */
// @interface Snake ()
// @property (nonatomic, copy) NSString *name;
/* @end */
// @implementation Snake
//手動(dòng)添加的注釋?zhuān)瑢?duì)應(yīng)name的geet方法
static NSString * _I_Snake_name(Snake * self, SEL _cmd) { return (*(NSString **)((char *)self + OBJC_IVAR_$_Snake$_name)); }
extern "C" __declspec(dllimport) void objc_setProperty (id, SEL, long, id, bool, bool);
//手動(dòng)添加的注釋?zhuān)瑢?duì)應(yīng)name的set方法
static void _I_Snake_setName_(Snake * self, SEL _cmd, NSString *name) { objc_setProperty (self, _cmd, __OFFSETOFIVAR__(struct Snake, _name), (id)name, 0, 1); }
// @end
1.2分析
我們剛才OC代碼定義的Snake類(lèi)以及屬性居然被注釋了,等價(jià)的被替換成了C++代碼。并且我們的類(lèi)變成了結(jié)構(gòu)體,我們都知道萬(wàn)物皆NSObject,我們的這個(gè)Snake類(lèi)也是繼承NSObject,但是定義的 Snake 類(lèi)只有一個(gè)name屬性,為什么結(jié)構(gòu)體里還有 NSObject_IMPL的結(jié)構(gòu)體呢?
其實(shí)這樣的定義同OC,也是繼承自 NSObject的意思 ,屬于偽繼承,偽繼承的方式是直接將 NSObject 結(jié)構(gòu)體定義為 Snake 中的第一個(gè)屬性,意味著 Snake 擁有 NSObject 中的所有成員變量
Snake 中的第一個(gè)屬性 NSObject_IVARS 等效于 NSObject 中的 isa
我們多次聽(tīng)到了這個(gè) isa。這個(gè) isa 到底是做啥的,平時(shí)開(kāi)發(fā)好像也沒(méi)怎么用到它,為什么會(huì)被多次提及,引用大佬的一句話(huà)簡(jiǎn)單來(lái)說(shuō)就是很重要,裝逼的來(lái)說(shuō)不要試圖去理解它。試著去感受它。
還記得我們提及過(guò) alloc 三大核心方法的核心之一的 initInstanceIsa 方法嗎?忘記了沒(méi)關(guān)系,上祖?zhèn)鞔a
obj->initInstanceIsa(cls, hasCxxDtor);
-------------------------------------------------
inline void
objc_object::initIsa(Class cls, bool nonpointer, bool hasCxxDtor)
{
ASSERT(!isTaggedPointer());
if (!nonpointer) {
isa = isa_t((uintptr_t)cls);
} else {
ASSERT(!DisableNonpointerIsa);
ASSERT(!cls->instancesRequireRawIsa());
isa_t newisa(0);
#if SUPPORT_INDEXED_ISA
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
newisa.bits = ISA_MAGIC_VALUE;
// 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;
#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;
}
}
我們看到這個(gè)方法有點(diǎn)懵逼。那我們一層脫下它的衣服??纯此锩娲┝松?br>
1、 通過(guò)cls初始化isa
2、如果是非 nonpointer,代表普通的指針,存儲(chǔ)著 Class、Meta-Class 對(duì)象的內(nèi)存地址信息。
3、然后就發(fā)現(xiàn) 定義了一個(gè)newisa,然后對(duì)它瘋狂賦值。足已證明它多重要了。我們看看里面是什么
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
};
1.3 結(jié)構(gòu)體(struct)&&聯(lián)合體(union)
構(gòu)造數(shù)據(jù)類(lèi)型的方式有以下兩種:
- 結(jié)構(gòu)體(
struct) - 聯(lián)合體(
union,也稱(chēng)為共用體)
之前我們已經(jīng)講過(guò)struct,現(xiàn)在又出現(xiàn)一種union我們來(lái)好好科普一下這兩個(gè)東西
結(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ù)類(lèi)型組成,但其變量是互斥的,所有的成員共占一段內(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ū)別
1、內(nèi)存占用情況
結(jié)構(gòu)體的各個(gè)成員會(huì)占用不同的內(nèi)存,互相之間沒(méi)有影響
共用體的所有成員占用同一段內(nèi)存,修改一個(gè)成員會(huì)影響其余所有成員
2、內(nèi)存分配大小
結(jié)構(gòu)體內(nèi)存 >= 所有成員占用的內(nèi)存總和(成員之間可能會(huì)有縫隙)
共用體占用的內(nèi)存等于最大的成員占用的內(nèi)存
我們剛才的那個(gè)isa_t就是一個(gè)union,為什么使用它來(lái)定義。通過(guò)剛才優(yōu)缺點(diǎn)也自然不言而喻了。我們來(lái)分析一下isa_t這個(gè)里面定義了什么?
-
cls:是Class類(lèi)型的指針變量,指向的是對(duì)象的類(lèi)。 -
bits:是結(jié)構(gòu)體位域指針。 -
ISA_BITFIELD:宏 ISA_BITFIELD,用來(lái)定義位域,用于存儲(chǔ)類(lèi)信息及其他信息。
ISA_BITFIELD
ISA_BITFIELD 宏在內(nèi)部分別定義了arm64位架構(gòu)(iOS)和x86_64架構(gòu)(macOS)的掩碼和位域.。

其isa的存儲(chǔ)情況如圖所示

現(xiàn)在也就理解剛才代碼中newisa賦值都是干啥的了吧。
1、cls 與 isa關(guān)聯(lián)原理就是isa指針中的shiftcls位域中存儲(chǔ)了類(lèi)信息,
2、initInstanceIsa的過(guò)程是將創(chuàng)建對(duì)象的指針和當(dāng)前的 類(lèi)cls 關(guān)聯(lián)起來(lái)
最后
說(shuō)了這么多。我們是否能裝逼反響驗(yàn)證一波上面所說(shuō)的呢?
1、【方式一】通過(guò)initIsa方法中的newisa.shiftcls = (uintptr_t)cls >> 3來(lái)驗(yàn)證
2、【方式二】通過(guò)isa指針地址與ISA_MSAK 的值 & 來(lái)驗(yàn)證
3、【方式三】通過(guò)runtime的方法object_getClass驗(yàn)證
4、【方式四】通過(guò)位運(yùn)算驗(yàn)證
方式一:通過(guò) initIsa 方法
newisa.shiftcls = (uintptr_t)cls >> 3;
isa = newisa;
我們用源代碼在這兩行代碼加入斷點(diǎn)。確保調(diào)用傳遞進(jìn)來(lái)的cls是我們要研究的Snake類(lèi)
運(yùn)行至此時(shí)。在lldb做以下操作

聰明的你是不是已經(jīng)發(fā)現(xiàn),我們p (uintptr_t)cls,結(jié)果為(uintptr_t) $5 = 4294976016,再右移三位,p (uintptr_t)cls >> 3得到(uintptr_t) $6 = 536872002,我們?cè)僭噷?5的值右移3位p 4294976016 >> 3,得到也是536872002,最后從左邊變量看shiftcls還是我們來(lái)直接暴力的看一下p newisa.shiftcls得到也是536872002
cls也變成了我們的Snake
方式二:通過(guò) isa & ISA_MSAK
if (!zone && fast) {
obj->initInstanceIsa(cls, hasCxxDtor);
} else {
// Use raw pointer isa on the assumption that they might be
// doing something weird with the zone or RR.
obj->initIsa(cls);
}
if (fastpath(!hasCxxCtor)) {
return obj;
}
我們走完剛才的方法返回到這里時(shí)在要return obj的之前的地方打個(gè)斷點(diǎn)。執(zhí)行x/4gx obj,得到isa指針的地址0x001d800100002215,再將isa指針地址 & ISA_MASK (處于macOS,使用x86_64中的宏定義),即 po 0x001d800100002215 & 0x00007ffffffffff8 ,得出Snake
-
arm64中,ISA_MASK宏定義的值為0x0000000ffffffff8ULL -
x86_64中,ISA_MASK宏定義的值為0x00007ffffffffff8ULL

方式三:通過(guò) object_getClass
通過(guò)查看·object_getClass·的源碼實(shí)現(xiàn),最終發(fā)現(xiàn)核心處理與我們的方法二一樣。這里就不過(guò)多復(fù)述
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
}
方式四:通過(guò)位運(yùn)算
我們用方法二在返回obj之前斷點(diǎn)執(zhí)行如下操作

1、將isa地址右移3位:p/x 0x001d800100002215 >> 3 ,得到0x0003b00020000442
2、再將得到的0x0003b00020000442左移20位:p/x 0x0003b00020000442 << 20 ,得到0x0002000044200000
3、將得到的0x0002000044200000 再右移17位:p/x 0x0002000041d00000 >> 17 得到新的0x0000000100002210
我們之所以左移右移,是因?yàn)橹?code>shiftcls所在位于的位置。所有的操作都是為了精準(zhǔn)讀取到shiftcls
那為什么是左移20位?因?yàn)橄扔乙屏?code>3位,相當(dāng)于向右偏移了3位,而左邊需要抹零的位數(shù)有17位,所以一共需要移動(dòng)20位
獲取cls的地址,或者直接po 與上面的進(jìn)行驗(yàn)證 得到
p/x cls
0x0000000100002210 `Snake`
po 0x0000000100002210 `Snake`
(注:部分圖片來(lái)自“style_月月”的博客) 傳送門(mén)->Style_月月