1. 前言
上一篇我們了解到了一個對象的屬性內(nèi)存分配和占用情況彩掐。并且額外引入了兩個結(jié)構(gòu)體
做了對比裁厅。我們發(fā)現(xiàn)類
跟結(jié)構(gòu)體
好像有什么相似的地方。那到底有什么相似的呢哑芹。話不多說,肝著捕透。
1.1Clang
首先clang
是一個由Apple
主導(dǎo)編寫聪姿,基于LLVM
的C/C++/OC
的編譯器,這貨干啥的呢?
主要用途可以將你編寫的類輸出成較為低一級別的代碼乙嘀,第一天玩人(Person)
末购。第二天玩狗(Dog)
,今天我們來當(dāng)許仙。一起來玩蛇(Snake)
??,例如將你Snake.m
輸出為Snake.cpp
,這樣一來就可以更直觀的觀察到代碼還做了哪些你不知道的事情虎谢。直接上碼
@interface Snake ()
@property (nonatomic, copy) NSString *name;
@end
@implementation Snake
@end
通過終端盟榴,利用 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`這個目錄一定要跟你本地的目錄對應(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
//以下兩種方式是通過指定架構(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
之后我們會在同級文件看到Snake.cpp
文件银觅。打開之后是不是很驚喜有上萬行代碼。驚不驚喜坏为,意不意外究驴。
我們?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
//手動添加的注釋匀伏,對應(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);
//手動添加的注釋洒忧,對應(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
類以及屬性居然被注釋了,等價的被替換成了C++
代碼够颠。并且我們的類變成了結(jié)構(gòu)體熙侍,我們都知道萬物皆NSObject
,我們的這個Snake
類也是繼承NSObject
履磨,但是定義的 Snake
類只有一個name
屬性蛉抓,為什么結(jié)構(gòu)體里還有 NSObject_IMPL
的結(jié)構(gòu)體呢?
其實這樣的定義同OC
剃诅,也是繼承自 NSObject
的意思 巷送,屬于偽繼承
,偽繼承
的方式是直接將 NSObject
結(jié)構(gòu)體定義為 Snake
中的第一個屬性矛辕,意味著 Snake
擁有 NSObject
中的所有成員變量
Snake
中的第一個屬性 NSObject_IVARS
等效于 NSObject
中的 isa
我們多次聽到了這個 isa
笑跛。這個 isa
到底是做啥的付魔,平時開發(fā)好像也沒怎么用到它,為什么會被多次提及飞蹂,引用大佬的一句話簡單來說就是很重要
抒抬,裝逼的來說不要試圖去理解它。試著去感受它
晤柄。
還記得我們提及過 alloc
三大核心方法的核心之一的 initInstanceIsa
方法嗎?忘記了沒關(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;
}
}
我們看到這個方法有點懵逼。那我們一層脫下它的衣服芥颈』堇眨看看它里面穿了啥
1、 通過cls
初始化isa
2爬坑、如果是非 nonpointer
,代表普通的指針,存儲著 Class
纠屋、Meta-Class
對象的內(nèi)存地址信息。
3盾计、然后就發(fā)現(xiàn) 定義了一個newisa
售担,然后對它瘋狂賦值。足已證明它多重要了署辉。我們看看里面是什么
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ù)類型的方式有以下兩種:
- 結(jié)構(gòu)體(
struct
) - 聯(lián)合體(
union
族铆,也稱為共用體)
之前我們已經(jīng)講過struct
,現(xiàn)在又出現(xiàn)一種union
我們來好好科普一下這兩個東西
結(jié)構(gòu)體
結(jié)構(gòu)體
是指把不同的數(shù)據(jù)組合成一個整體哭尝,其變量
是共存
的哥攘,變量
不管是否使用
,都會分配內(nèi)存
材鹦。
缺點:所有屬性都分配內(nèi)存逝淹,比較浪費內(nèi)存,假設(shè)有 `4` 個 `int` 成員桶唐,一共分配了 `16` 字節(jié)的內(nèi)存栅葡,但是在使用時,你只使用了 `4` 字節(jié)尤泽,剩余的 `12` 字節(jié)就是屬于內(nèi)存的浪費
優(yōu)點:存儲容量較大欣簇,包容性強(qiáng),且成員之間不會相互影響
聯(lián)合體
聯(lián)合體
也是由不同的數(shù)據(jù)類型組成安吁,但其變量是互斥
的醉蚁,所有的成員共占一段內(nèi)存。而且共用體采用了內(nèi)存覆蓋
技術(shù)鬼店,同一時刻只能保存一個成員的值网棍,如果對新的成員賦值,就會將原來成員的值覆蓋掉
缺點:包容性弱
優(yōu)點:所有成員共用一段內(nèi)存妇智,使內(nèi)存的使用更為精細(xì)靈活滥玷,同時也節(jié)省了內(nèi)存空間
兩者的區(qū)別
1氏身、內(nèi)存占用情況
結(jié)構(gòu)體的各個成員會占用不同的內(nèi)存,互相之間沒有影響
共用體的所有成員占用同一段內(nèi)存惑畴,修改一個成員會影響其余所有成員
2蛋欣、內(nèi)存分配大小
結(jié)構(gòu)體內(nèi)存 >= 所有成員占用的內(nèi)存總和(成員之間可能會有縫隙)
共用體占用的內(nèi)存等于最大的成員占用的內(nèi)存
我們剛才的那個isa_t
就是一個union
,為什么使用它來定義。通過剛才優(yōu)缺點也自然不言而喻了如贷。我們來分析一下isa_t
這個里面定義了什么陷虎?
-
cls
:是Class
類型的指針變量,指向的是對象的類杠袱。 -
bits
:是結(jié)構(gòu)體位域指針尚猿。 -
ISA_BITFIELD
:宏 ISA_BITFIELD,用來定義位域楣富,用于存儲類信息及其他信息凿掂。
ISA_BITFIELD
ISA_BITFIELD
宏在內(nèi)部分別定義了arm64
位架構(gòu)(iOS
)和x86_64
架構(gòu)(macOS
)的掩碼和位域.。
其isa
的存儲情況如圖所示
現(xiàn)在也就理解剛才代碼中newisa
賦值都是干啥的了吧纹蝴。
1庄萎、cls
與 isa
關(guān)聯(lián)原理就是isa
指針中的shiftcls
位域中存儲了類信息,
2塘安、initInstanceIsa
的過程是將創(chuàng)建對象的指針和當(dāng)前的 類cls
關(guān)聯(lián)起來
最后
說了這么多糠涛。我們是否能裝逼反響驗證一波上面所說的呢?
1耙旦、【方式一】通過initIsa
方法中的newisa.shiftcls = (uintptr_t)cls >> 3
來驗證
2脱羡、【方式二】通過isa
指針地址與ISA_MSAK
的值 & 來驗證
3、【方式三】通過runtime
的方法object_getClass
驗證
4免都、【方式四】通過位運算驗證
方式一:通過 initIsa 方法
newisa.shiftcls = (uintptr_t)cls >> 3;
isa = newisa;
我們用源代碼在這兩行代碼加入斷點。確保調(diào)用傳遞進(jìn)來的cls是我們要研究的Snake
類
運行至此時帆竹。在lldb
做以下操作
聰明的你是不是已經(jīng)發(fā)現(xiàn)绕娘,我們p (uintptr_t)cls
,結(jié)果為(uintptr_t) $5 = 4294976016
,再右移三位栽连,p (uintptr_t)cls >> 3
得到(uintptr_t) $6 = 536872002
险领,我們再試將$5的值右移3位p 4294976016 >> 3
,得到也是536872002
秒紧,最后從左邊變量看shiftcls
還是我們來直接暴力的看一下p newisa.shiftcls
得到也是536872002
cls
也變成了我們的Snake
方式二:通過 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;
}
我們走完剛才的方法返回到這里時在要return obj
的之前的地方打個斷點绢陌。執(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
方式三:通過 object_getClass
通過查看·object_getClass·的源碼實現(xiàn)秤掌,最終發(fā)現(xiàn)核心處理與我們的方法二一樣愁铺。這里就不過多復(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
}
方式四:通過位運算
我們用方法二在返回obj
之前斷點執(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
我們之所以左移右移孟岛,是因為知道shiftcls
所在位于的位置瓶竭。所有的操作都是為了精準(zhǔn)讀取到shiftcls
那為什么是左移20
位?因為先右移了3
位渠羞,相當(dāng)于向右偏移了3
位在验,而左邊需要抹零的位數(shù)有17
位,所以一共需要移動20
位
獲取cls
的地址堵未,或者直接po
與上面的進(jìn)行驗證 得到
p/x cls
0x0000000100002210 `Snake`
po 0x0000000100002210 `Snake`
(注:部分圖片來自“style_月月”的博客) 傳送門->Style_月月