寫(xiě)在前面
在iOS之武功秘籍②:OC對(duì)象原理-中(內(nèi)存對(duì)齊和malloc源碼分析)一文中講了對(duì)象中的屬性在內(nèi)存中的排列 -- 內(nèi)存對(duì)齊 和malloc源碼分析,那么接下我們就來(lái)分析一下isa的初始化和指向分析與對(duì)象的本質(zhì)
一、對(duì)象的本質(zhì)
① Clang的了解
Clang是?個(gè)由Apple主導(dǎo)編寫(xiě),基于LLVM的C/C++/Objective-C輕量級(jí)編譯器.源代碼發(fā)布于LLVM BSD協(xié)議下.Clang將?持其普通lambda表達(dá)式、返回類(lèi)型的簡(jiǎn)化處理以及更好的處理constexpr關(guān)鍵字。它與
GNU C語(yǔ)?規(guī)范?乎完全兼容(當(dāng)然,也有部分不兼容的內(nèi)容,
包括編譯命令選項(xiàng)也會(huì)有點(diǎn)差異),并在此基礎(chǔ)上增加了額外的語(yǔ)法特性,?如C函數(shù)重載
(通過(guò)__attribute__((overloadable))來(lái)修飾函數(shù)),其?標(biāo)(之?)就是超越GCC.它主要是用于
底層編譯,將一些OC文件輸出成C++文件,例如main.m輸出成main.cpp,其目的是為了更好的觀察底層的一些結(jié)構(gòu)及實(shí)現(xiàn)的邏輯,方便理解底層原理
② Clang操作指令
// 把?標(biāo)?件編譯成c++?件 -- 將 main.m 編譯成 main.cpp
clang -rewrite-objc main.m -o main.cpp
// UIKit報(bào)錯(cuò)問(wèn)題 -- 將 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/iPhoneSimulator14.0.sdk ViewController.m
// `xcode`安裝的時(shí)候順帶安裝了`xcrun`命令,`xcrun`命令在`clang`的基礎(chǔ)上進(jìn)?了?些封裝,要更好??些
xcrun -sdk iphonesimulator clang -arch arm64 -rewrite-objc main.m -o
main-arm64.cpp (模擬器)
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main?arm64.cpp (?機(jī))
③ 探索對(duì)象本質(zhì)
-
構(gòu)建測(cè)試代碼
-
通過(guò)終端,利用
clang將main.m編譯成main.cpp,在終端輸入以下命令- xcrun -sdk iphonesimulator clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp
-
打開(kāi)編譯好的
main-arm64.cpp,找TCJPerson的定義,發(fā)TCJPerson在底層會(huì)被編譯成struct結(jié)構(gòu)體
通過(guò)編譯好的main-arm64.cpp我們可以看到:
-
NSObject的底層實(shí)現(xiàn)其實(shí)就是一個(gè)包含一個(gè)isa指針的結(jié)構(gòu)體. -
Class其實(shí)就是一個(gè)指針,指向了objc_class類(lèi)型的結(jié)構(gòu)體. -
TCJPerson_IMPL結(jié)構(gòu)體內(nèi)有三個(gè)成員變量:-
isa繼承自父類(lèi)NSObject helloName_name
-
- 對(duì)于屬性
name:底層編譯會(huì)生成相應(yīng)的setter(_I_TCJPerson_setName_,setter方法內(nèi)調(diào)用objc_setProperty方法)、getter(_I_TCJPerson_name)方法,且?guī)臀覀冝D(zhuǎn)化為_name - 對(duì)于成員變量
helloName:底層編譯不會(huì)生成相應(yīng)的setter、getter方法,且沒(méi)有轉(zhuǎn)化為_helloName
通過(guò)上述分析,理解了OC對(duì)象的本質(zhì) -- 結(jié)構(gòu)體,但是看到NSObject的定義,會(huì)產(chǎn)生一個(gè)疑問(wèn):為什么isa的類(lèi)型是Class?
- 在iOS之武功秘籍①:OC對(duì)象原理-上(alloc & init & new)文章中,提及過(guò)
alloc方法的核心之一的initInstanceIsa方法,通過(guò)查看這個(gè)方法的源碼實(shí)現(xiàn),我們發(fā)現(xiàn),在初始化isa指針時(shí),是通過(guò)isa_t類(lèi)型初始化的 - 而在
NSObject定義中isa的類(lèi)型是Class,其根本原因是由于isa對(duì)外反饋的是類(lèi)信息,為了讓開(kāi)發(fā)人員更加清晰明確,需要在isa返回時(shí)做了一個(gè)類(lèi)型強(qiáng)制轉(zhuǎn)換,類(lèi)似于swift中的as的強(qiáng)轉(zhuǎn).源碼中isa的強(qiáng)轉(zhuǎn)如下圖所示
④ 探究屬性get、set方法
通過(guò)上文的分析我們知道:對(duì)于屬性name:底層編譯會(huì)生成相應(yīng)的setter和getter方法,且?guī)臀覀冝D(zhuǎn)化為_name成員變量,而對(duì)于成員變量helloName:底層編譯不會(huì)生成相應(yīng)的setter、getter方法,且沒(méi)有轉(zhuǎn)化為_helloName.這其中的setter方法的實(shí)現(xiàn)依賴(lài)于runtime中的objc_setProperty.

接下來(lái)我們來(lái)看看objc_setProperty的底層實(shí)現(xiàn)
-
在
objc4源碼中全局搜索objc_setProperty,找到objc_setProperty的源碼實(shí)現(xiàn) -
進(jìn)入
reallySetProperty的源碼實(shí)現(xiàn),其方法的原理就是新值retain,舊值release
總結(jié):
通過(guò)對(duì)objc_setProperty的底層源碼探索,有以下幾點(diǎn)說(shuō)明:
objc_setProperty方法的目的適用于關(guān)聯(lián)上層的set方法以及底層的set方法,其本質(zhì)就是一個(gè)接口這么設(shè)計(jì)的原因是,上層的
set方法有很多,如果直接調(diào)用底層set方法,會(huì)產(chǎn)生很多的臨時(shí)變量,當(dāng)你想查找一個(gè)sel時(shí),會(huì)非常麻煩基于上述原因,蘋(píng)果采用了
適配器設(shè)計(jì)模式(即將底層接口適配為客戶(hù)端需要的接口),對(duì)外提供一個(gè)接口,供上層的set方法使用,對(duì)內(nèi)調(diào)用底層的set方法,使其相互不受影響,即無(wú)論上層怎么變,下層都是不變的,或者下層的變化也無(wú)法影響上層,主要是達(dá)到上下層接口隔離的目的.
下圖是上層、隔離層、底層之間的關(guān)系
- 外部
set方法: 上層 - 個(gè)性化定制層(例如setName、setAge等) -
objc_setProperty:接口隔離層 (將外界信息轉(zhuǎn)化為對(duì)內(nèi)存地址和值的操作) -
reallySetProperty:底層實(shí)現(xiàn)層 (賦值和內(nèi)存管理)
二、isa底層原理
在iOS之武功秘籍①:OC對(duì)象原理-上(alloc & init & new)與iOS之武功秘籍②:OC對(duì)象原理-中(內(nèi)存對(duì)齊和malloc源碼分析)中分別分析了alloc中3核心的前兩個(gè),今天來(lái)探索initInstanceIsa是如何將cls與isa關(guān)聯(lián)的.
在此之前,需要先了解什么是聯(lián)合體,為什么isa的類(lèi)型isa_t是使用聯(lián)合體定義的.那么什么是聯(lián)合體?什么又是位域?
①. 位域
①.1 定義
有些信息在存儲(chǔ)時(shí),并不需要占用一個(gè)完整的字節(jié),而只需占一個(gè)或幾個(gè)二進(jìn)制位.例如在存放一個(gè)開(kāi)關(guān)量時(shí),只有0和1兩種狀態(tài),用1位二進(jìn)位即可.為了節(jié)省存儲(chǔ)空間并使處理簡(jiǎn)便,C語(yǔ)言提供了一種數(shù)據(jù)結(jié)構(gòu),稱(chēng)為位域或位段.
所謂位域就是把一個(gè)字節(jié)中的二進(jìn)位劃分為幾個(gè)不同的區(qū)域,并說(shuō)明每個(gè)區(qū)域的位數(shù).每個(gè)域有一個(gè)域名,允許在程序中按域名進(jìn)行操作——這樣就可以把幾個(gè)不同的對(duì)象用一個(gè)字節(jié)的二進(jìn)制位域來(lái)表示.
①.2 與結(jié)構(gòu)體比較
位域的使用與結(jié)構(gòu)體相仿,它本身也是結(jié)構(gòu)體的一種.
// 結(jié)構(gòu)體
struct TCJStruct {
// (類(lèi)型說(shuō)明符 元素);
char a;
int b;
} TCJStr;
// 位域
struct TCJBitArea {
// (類(lèi)型說(shuō)明符 位域名: 位域長(zhǎng)度);
char a: 1;
int b: 3;
} TCJBit;
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSLog(@"Struct:%lu——BitArea:%lu", sizeof(TCJStr), sizeof(TCJBit));
}
return 0;
}
輸出Struct:8——BitArea:4.
②. 聯(lián)合體
②.1 定義
當(dāng)多個(gè)數(shù)據(jù)需要共享內(nèi)存或者多個(gè)數(shù)據(jù)每次只取其一時(shí),可以利用聯(lián)合體(union)
- 聯(lián)合體是一個(gè)結(jié)構(gòu)
- 它的所有成員相對(duì)于基地址的偏移量都為0
- 此結(jié)構(gòu)空間要大到足夠容納最"寬"的成員
- 各變量是“互斥”的——共用一個(gè)內(nèi)存首地址,聯(lián)合變量可被賦予任一成員值,但每次只能賦一種值, 賦入新值則沖去舊值.
②.2 與結(jié)構(gòu)體比較
結(jié)構(gòu)體每個(gè)成員依次存儲(chǔ),聯(lián)合體中所有成員的偏移地址都是0,也就是所有成員是疊在一起的,所以在聯(lián)合體中在某一時(shí)刻,只有一個(gè)成員有效——結(jié)構(gòu)體內(nèi)存大小取決于所有元素,聯(lián)合體取決于最大那個(gè)

②.3 補(bǔ)充知識(shí)--位運(yùn)算符
在計(jì)算機(jī)語(yǔ)言中,除了加、減、乘、除等這樣的算術(shù)運(yùn)算符之外還有很多運(yùn)算符,這里只為大家簡(jiǎn)單講解一下位運(yùn)算符.
位運(yùn)算符用來(lái)對(duì)二進(jìn)制位進(jìn)行操作,當(dāng)然,操作數(shù)只能為整型和字符型數(shù)據(jù)。C語(yǔ)言中六種位運(yùn)算符:&按位與、|按位或、^按位異或、~非、<<左移和>>右移。
我們依舊引用上面的電燈開(kāi)關(guān)論,只不過(guò)現(xiàn)在我們有兩個(gè)開(kāi)關(guān):開(kāi)關(guān)A和開(kāi)關(guān)B,1代表開(kāi),0代表關(guān).
1)按位與&
有0出0,全1出1.
| A | B | & |
|---|---|---|
| 0 | 0 | 0 |
| 1 | 0 | 0 |
| 0 | 1 | 0 |
| 1 | 1 | 1 |
我們可以理解為在按位與運(yùn)算中,兩個(gè)開(kāi)關(guān)是串聯(lián)的,如果我們想要燈亮,需要兩個(gè)開(kāi)關(guān)都打開(kāi)燈才會(huì)亮,所以是1 & 1 = 1. 如果任意一個(gè)開(kāi)關(guān)沒(méi)有打開(kāi),燈都不會(huì)亮,所以其他運(yùn)算都是0.
2)按位或 |
有1出1,全0出0.
| A | B | I |
|---|---|---|
| 0 | 0 | 0 |
| 1 | 0 | 1 |
| 0 | 1 | 1 |
| 1 | 1 | 1 |
在按位或運(yùn)算中,我們可以理解為兩個(gè)開(kāi)關(guān)是并聯(lián)的,即一個(gè)開(kāi)關(guān)開(kāi),燈就會(huì)亮.只有當(dāng)兩個(gè)開(kāi)關(guān)都是關(guān)的.燈才不會(huì)亮.
3)按位異或^
相同為0,不同為1.
| A | B | ^ |
|---|---|---|
| 0 | 0 | 0 |
| 1 | 0 | 1 |
| 0 | 1 | 1 |
| 1 | 1 | 0 |
4)非 ~
非運(yùn)算即取反運(yùn)算,在二進(jìn)制中 1 變 0 ,0 變 1。例如110101進(jìn)行非運(yùn)算后為001010,即1010.
5)左移 <<
左移運(yùn)算就是把<<左邊的運(yùn)算數(shù)的各二進(jìn)位全部左移若干位,移動(dòng)的位數(shù)即<<右邊的數(shù)的數(shù)值,高位丟棄,低位補(bǔ)0.
左移n位就是乘以2的n次方.例如:a<<4是指把a(bǔ)的各二進(jìn)位向左移動(dòng)4位.如a=00000011(十進(jìn)制3),左移4位后為00110000(十進(jìn)制48).
6)右移 >>
右移運(yùn)算就是把>>左邊的運(yùn)算數(shù)的各二進(jìn)位全部右移若干位,>>右邊的數(shù)指定移動(dòng)的位數(shù).例如:設(shè) a=15,a>>2 表示把00001111右移為00000011(十進(jìn)制3).
②.4 位運(yùn)算符的運(yùn)用
1)取值
可以利用按位與 &運(yùn)算取出指定位的值,具體操作是想取出哪一位的值就將那一位置為1,其它位都為0,然后同原數(shù)據(jù)進(jìn)行按位與計(jì)算,即可取出特定的位.
例:
0000 0011取出倒數(shù)第三位的值
// 想取出倒數(shù)第三位的值,就將倒數(shù)第三位的值置為1,其它位為0,跟原數(shù)據(jù)按位與運(yùn)算
0000 0011
& 0000 0100
------------
0000 0000 // 得出按位與運(yùn)算后的結(jié)果,即可拿到原數(shù)據(jù)中倒數(shù)第三位的值為0
上面的例子中,我們從0000 0011中取值,則有0000 0011被稱(chēng)之為源碼.進(jìn)行按位與操作設(shè)定的0000 0100稱(chēng)之為掩碼.
2)設(shè)值
可以通過(guò)按位或 |運(yùn)算符將某一位的值設(shè)為1或0.具體操作是:
想將某一位的值置為1的話,那么就將掩碼中對(duì)應(yīng)位的值設(shè)為1,掩碼其它位為0,將源碼與掩碼進(jìn)行按位或操作即可.
例: 將
0000 0011倒數(shù)第三位的值改為1
// 改變倒數(shù)第三位的值,就將掩碼倒數(shù)第三位的值置為1,其它位為0,跟源碼按位或運(yùn)算
0000 0011
| 0000 0100
------------
0000 0111 // 即可將源碼中倒數(shù)第三位的值改為1
想將某一位的值置為0的話,那么就將掩碼中對(duì)應(yīng)位的值設(shè)為0,掩碼其它位為1,將源碼與掩碼進(jìn)行按位或操作即可.
例: 將
0000 0011倒數(shù)第二位的值改為0
// 改變倒數(shù)第二位的值,就將掩碼倒數(shù)第二位的值置為0,其它位為1,跟源碼按位或運(yùn)算
0000 0011
| 1111 1101
------------
0000 0001 // 即可將源碼中倒數(shù)第二位的值改為0
到這里相信大家對(duì)位運(yùn)算符有了一定的了解.
③. 結(jié)構(gòu)體位域與聯(lián)合體的使用
我們來(lái)看下面的??:我們聲明一個(gè)TCJCar類(lèi),類(lèi)中有四個(gè)BOOL類(lèi)型的屬性,分別為front、back、left、right,通過(guò)這四個(gè)屬性來(lái)判斷這輛小車(chē)的行駛方向.

然后我們來(lái)查看一下這個(gè)TCJCar類(lèi)對(duì)象所占據(jù)的內(nèi)存大小:

我們看到,一個(gè)TCJCar類(lèi)的對(duì)象占據(jù)16個(gè)字節(jié).其中包括一個(gè)isa指針和四個(gè)BOOL類(lèi)型的屬性,8+1+1+1+1=12,根據(jù)內(nèi)存對(duì)齊原則,所以一個(gè)TCJCar類(lèi)的對(duì)象占16個(gè)字節(jié).
我們知道,BOOL值只有兩種情況:0或1,占據(jù)一個(gè)字節(jié)的內(nèi)存空間.而一個(gè)字節(jié)的內(nèi)存空間中又有8個(gè)二進(jìn)制位,并且二進(jìn)制同樣只有0或1,那么我們完全可以使用1個(gè)二進(jìn)制位來(lái)表示一個(gè)BOOL值.也就是說(shuō)我們上面聲明的四個(gè)BOOL值最終只使用4個(gè)二進(jìn)制位就可以,這樣就節(jié)省了內(nèi)存空間.那我們?nèi)绾螌?shí)現(xiàn)呢?
想要實(shí)現(xiàn)四個(gè)BOOL值存放在一個(gè)字節(jié)中,我們可以通過(guò)char類(lèi)型的成員變量來(lái)實(shí)現(xiàn).char類(lèi)型占一個(gè)字節(jié)內(nèi)存空間,也就是8個(gè)二進(jìn)制位.可以使用其中最后四個(gè)二進(jìn)制位來(lái)存儲(chǔ)4個(gè)BOOL值.
當(dāng)然我們不能把char類(lèi)型寫(xiě)成屬性,因?yàn)橐坏?xiě)成屬性,系統(tǒng)會(huì)自動(dòng)幫我們添加成員變量,自動(dòng)實(shí)現(xiàn)set和get方法.
@interface TCJCar(){
char _frontBackLeftRight;
}
如果我們賦值_frontBackLeftRight為1,即0b 0000 0001,只使用8個(gè)二進(jìn)制位中的最后4個(gè)分別用0或者1來(lái)代表front、back、left、right的值.那么此時(shí)front、back、left、right的狀態(tài)為:

我們可以分別聲明front、back、left、right的掩碼,來(lái)方便我們進(jìn)行下一步的位運(yùn)算取值和賦值:
#define TCJDirectionFrontMask 0b00001000 //此二進(jìn)制數(shù)對(duì)應(yīng)十進(jìn)制數(shù)為 8
#define TCJDirectionBackMask 0b00000100 //此二進(jìn)制數(shù)對(duì)應(yīng)十進(jìn)制數(shù)為 4
#define TCJDirectionLeftMask 0b00000010 //此二進(jìn)制數(shù)對(duì)應(yīng)十進(jìn)制數(shù)為 2
#define TCJDirectionRightMask 0b00000001 //此二進(jìn)制數(shù)對(duì)應(yīng)十進(jìn)制數(shù)為 1
通過(guò)對(duì)位運(yùn)算符的左移<<和右移>>的了解,我們可以將上面的代碼優(yōu)化成:
#define TCJDirectionFrontMask (1 << 3)
#define TCJDirectionBackMask (1 << 2)
#define TCJDirectionLeftMask (1 << 1)
#define TCJDirectionRightMask (1 << 0)
自定義的set方法如下:
- (void)setFront:(BOOL)front
{
if (front) {// 如果需要將值置為1,將源碼和掩碼進(jìn)行按位或運(yùn)算
_frontBackLeftRight |= TCJDirectionFrontMask;
} else {// 如果需要將值置為0 // 將源碼和按位取反后的掩碼進(jìn)行按位與運(yùn)算
_frontBackLeftRight &= ~TCJDirectionFrontMask;
}
}
- (void)setBack:(BOOL)back
{
if (back) {
_frontBackLeftRight |= TCJDirectionBackMask;
} else {
_frontBackLeftRight &= ~TCJDirectionBackMask;
}
}
- (void)setLeft:(BOOL)left
{
if (left) {
_frontBackLeftRight |= TCJDirectionLeftMask;
} else {
_frontBackLeftRight &= ~TCJDirectionLeftMask;
}
}
- (void)setRight:(BOOL)right
{
if (right) {
_frontBackLeftRight |= TCJDirectionRightMask;
} else {
_frontBackLeftRight &= ~TCJDirectionRightMask;
}
}
自定義的get方法如下:
- (BOOL)isFront
{
return !!(_frontBackLeftRight & TCJDirectionFrontMask);
}
- (BOOL)isBack
{
return !!(_frontBackLeftRight & TCJDirectionBackMask);
}
- (BOOL)isLeft
{
return !!(_frontBackLeftRight & TCJDirectionLeftMask);
}
- (BOOL)isRight
{
return !!(_frontBackLeftRight & TCJDirectionRightMask);
}
此處需要注意的是,代碼中!為邏輯運(yùn)算符非,因?yàn)?code>_frontBackLeftRight & TCJDirectionFrontMask代碼執(zhí)行后,返回的肯定是一個(gè)整型數(shù),如當(dāng)front為YES時(shí),說(shuō)明二進(jìn)制數(shù)為0b 0000 1000,對(duì)應(yīng)的十進(jìn)制數(shù)為8,那么進(jìn)行一次邏輯非運(yùn)算后,!(8)的值為0,對(duì)0再進(jìn)行一次邏輯非運(yùn)算!(0),結(jié)果就成了1,那么正好跟front為YES對(duì)應(yīng).所以此處進(jìn)行兩次邏輯非運(yùn)算,!!.
當(dāng)然,還要實(shí)現(xiàn)初始化方法:
- (instancetype)init
{
self = [super init];
if (self) {
_frontBackLeftRight = 0b00001000;
}
return self;
}
通過(guò)測(cè)試驗(yàn)證,我們完成了取值和賦值:
③.1 使用結(jié)構(gòu)體位域優(yōu)化代碼
我們?cè)谏衔闹v到了位域的概念,那么我們就可以使用結(jié)構(gòu)體位域來(lái)優(yōu)化一下我們的代碼.這樣就不用再額外聲明上面代碼中的掩碼部分了.位域聲明格式是位域名: 位域長(zhǎng)度.
在使用位域的過(guò)程中需要注意以下幾點(diǎn):
- 如果一個(gè)字節(jié)所??臻g不夠存放另一位域時(shí),應(yīng)從下一單元起存放該位域.
- 位域的長(zhǎng)度不能大于數(shù)據(jù)類(lèi)型本身的長(zhǎng)度,比如
int類(lèi)型就不能超過(guò)32位二進(jìn)位. - 位域可以無(wú)位域名,這時(shí)它只用來(lái)作填充或調(diào)整位置.無(wú)名的位域是不能使用的.


來(lái)測(cè)試看一下是否正確,這次我們將front設(shè)為YES、back設(shè)為NO、left設(shè)為NO、right設(shè)為YES:

依舊能完成賦值和取值.
但是代碼這樣優(yōu)化后我們?nèi)サ袅搜诖a和初始化的代碼,可讀性很差,我們繼續(xù)使用聯(lián)合體進(jìn)行優(yōu)化:
③.2 使用聯(lián)合體優(yōu)化代碼
我們可以使用比較高效的位運(yùn)算來(lái)進(jìn)行賦值和取值,使用union聯(lián)合體來(lái)對(duì)數(shù)據(jù)進(jìn)行存儲(chǔ)。這樣不僅可以增加讀取效率,還可以增強(qiáng)代碼可讀性.
#import "TCJCar.h"
//#define TCJDirectionFrontMask 0b00001000 //此二進(jìn)制數(shù)對(duì)應(yīng)十進(jìn)制數(shù)為 8
//#define TCJDirectionBackMask 0b00000100 //此二進(jìn)制數(shù)對(duì)應(yīng)十進(jìn)制數(shù)為 4
//#define TCJDirectionLeftMask 0b00000010 //此二進(jìn)制數(shù)對(duì)應(yīng)十進(jìn)制數(shù)為 2
//#define TCJDirectionRightMask 0b00000001 //此二進(jìn)制數(shù)對(duì)應(yīng)十進(jìn)制數(shù)為 1
#define TCJDirectionFrontMask (1 << 3)
#define TCJDirectionBackMask (1 << 2)
#define TCJDirectionLeftMask (1 << 1)
#define TCJDirectionRightMask (1 << 0)
@interface TCJCar()
{
union{
char bits;
// 結(jié)構(gòu)體僅僅是為了增強(qiáng)代碼可讀性
struct {
char front : 1;
char back : 1;
char left : 1;
char right : 1;
};
}_frontBackLeftRight;
}
@end
@implementation TCJCar
- (instancetype)init
{
self = [super init];
if (self) {
_frontBackLeftRight.bits = 0b00001000;
}
return self;
}
- (void)setFront:(BOOL)front
{
if (front) {
_frontBackLeftRight.bits |= TCJDirectionFrontMask;
} else {
_frontBackLeftRight.bits &= ~TCJDirectionFrontMask;
}
}
- (BOOL)isFront
{
return !!(_frontBackLeftRight.bits & TCJDirectionFrontMask);
}
- (void)setBack:(BOOL)back
{
if (back) {
_frontBackLeftRight.bits |= TCJDirectionBackMask;
} else {
_frontBackLeftRight.bits &= ~TCJDirectionBackMask;
}
}
- (BOOL)isBack
{
return !!(_frontBackLeftRight.bits & TCJDirectionBackMask);
}
- (void)setLeft:(BOOL)left
{
if (left) {
_frontBackLeftRight.bits |= TCJDirectionLeftMask;
} else {
_frontBackLeftRight.bits &= ~TCJDirectionLeftMask;
}
}
- (BOOL)isLeft
{
return !!(_frontBackLeftRight.bits & TCJDirectionLeftMask);
}
- (void)setRight:(BOOL)right
{
if (right) {
_frontBackLeftRight.bits |= TCJDirectionRightMask;
} else {
_frontBackLeftRight.bits &= ~TCJDirectionRightMask;
}
}
- (BOOL)isRight
{
return !!(_frontBackLeftRight.bits & TCJDirectionRightMask);
}
@end
來(lái)我們測(cè)試看一下是否正確,這次我們依舊將front設(shè)為YES、back設(shè)為NO、left設(shè)為NO、right設(shè)為YES:

通過(guò)結(jié)果我們看到依舊能完成賦值和取值.
這其中_frontBackLeftRight聯(lián)合體只占用一個(gè)字節(jié),因?yàn)榻Y(jié)構(gòu)體中front、back、left、right都只占一位二進(jìn)制空間,所以結(jié)構(gòu)體只占一個(gè)字節(jié),而char類(lèi)型的bits也只占一個(gè)字節(jié).他們都在聯(lián)合體中,因此共用一個(gè)字節(jié)的內(nèi)存即可.
而且我們?cè)?code>set、get方法中的賦值和取值通過(guò)使用掩碼進(jìn)行位運(yùn)算來(lái)增加效率,整體邏輯也就很清晰了.但是如果我們?cè)谌粘i_(kāi)發(fā)中這樣寫(xiě)代碼的話,很可能會(huì)被同事打死.雖然代碼已經(jīng)很清晰了,但是整體閱讀起來(lái)還是很吃力的.我們?cè)谶@里學(xué)習(xí)了位運(yùn)算以及聯(lián)合體這些知識(shí),更多的是為了方便我們閱讀OC底層的代碼.下面我們來(lái)回到本文主題,查看一下isa_t聯(lián)合體的源碼.
④. isa_t聯(lián)合體

通過(guò)源碼我們發(fā)現(xiàn)isa它是一個(gè)聯(lián)合體,聯(lián)合體是一個(gè)結(jié)構(gòu)占8個(gè)字節(jié),它的特性就是共用內(nèi)存,或者說(shuō)是互斥,比如說(shuō)如果cls賦值了就不在對(duì)bits進(jìn)行賦值.在isa_t聯(lián)合體內(nèi)使用宏ISA_BITFIELD定義了位域,我們進(jìn)入位域內(nèi)查看源碼:

我們看到,在內(nèi)部分別定義了arm64位架構(gòu)和x86_64架構(gòu)的掩碼和位域.我們只分析arm64為架構(gòu)下的部分內(nèi)容(真機(jī)環(huán)境下).
可以清楚的看到ISA_BITFIELD位域的內(nèi)容以及掩碼ISA_MASK的值:0x0000000ffffffff8ULL.我們重點(diǎn)看一下uintptr_t shiftcls : 33;,在shiftcls中存儲(chǔ)著類(lèi)對(duì)象和元類(lèi)對(duì)象的內(nèi)存地址信息,我們上文講到,對(duì)象的isa指針需要同ISA_MASK經(jīng)過(guò)一次按位與運(yùn)算才能得出真正的類(lèi)對(duì)象地址.那么我們將ISA_MASK的值0x0000000ffffffff8ULL轉(zhuǎn)化為二進(jìn)制數(shù)分析一下:

從圖中可以看到ISA_MASK的值轉(zhuǎn)化為二進(jìn)制中有33位都為1,上文講到按位與運(yùn)算是可以取出這33位中的值.那么就說(shuō)明同ISA_MASK進(jìn)行按位與運(yùn)算就可以取出類(lèi)對(duì)象和元類(lèi)對(duì)象的內(nèi)存地址信息了.
不同架構(gòu)下isa所占內(nèi)存均為8字節(jié)——64位,但內(nèi)部分布有所不同,arm64架構(gòu)isa內(nèi)部成員分布如下圖

nonpointer:表示是否對(duì)isa指針開(kāi)啟指針優(yōu)化 ——0:純isa指針;1:不止是類(lèi)對(duì)象地址,isa中包含了類(lèi)信息、對(duì)象的引用計(jì)數(shù)等has_assoc:關(guān)聯(lián)對(duì)象標(biāo)志位,0沒(méi)有,1存在has_cxx_dtor:該對(duì)象是否有C++或者Objc的析構(gòu)器,如果有析構(gòu)函數(shù),則需要做析構(gòu)邏輯, 如果沒(méi)有,則可以更快的釋放對(duì)象shiftcls:存儲(chǔ)類(lèi)指針的值(類(lèi)的地址),即類(lèi)、元類(lèi)對(duì)象的內(nèi)存地址信息.在開(kāi)啟指針優(yōu)化的情況下,在 arm64 架構(gòu)中有 33 位用來(lái)存儲(chǔ)類(lèi)指針magic:用于調(diào)試器判斷當(dāng)前對(duì)象是真的對(duì)象還是沒(méi)有初始化的空間weakly_referenced:對(duì)象是否被指向或者曾經(jīng)指向一個(gè) ARC 的弱變量,沒(méi)有弱引用的對(duì)象可以更快釋放deallocating:標(biāo)志對(duì)象是否正在釋放內(nèi)存has_sidetable_rc:當(dāng)對(duì)象引用技術(shù)大于 10時(shí),則需要借用該變量存儲(chǔ)進(jìn)位extra_rc:當(dāng)表示該對(duì)象的引用計(jì)數(shù)值,實(shí)際上是引用計(jì)數(shù)值減 1, 例如,如果對(duì)象的引用計(jì)數(shù)為 10,那么extra_rc為 9。如果引用計(jì)數(shù)大于 10, 則需要使用到下面的has_sidetable_rc
上面所說(shuō)的當(dāng)對(duì)象
引用技術(shù)大于 10時(shí),那是一個(gè)例如, 不是具體的10.
至此我們已經(jīng)對(duì)isa指針有了新的認(rèn)識(shí),arm64架構(gòu)之后,isa指針不單單只存儲(chǔ)了類(lèi)對(duì)象和元類(lèi)對(duì)象的內(nèi)存地址,而是使用聯(lián)合體的方式存儲(chǔ)了更多信息,其中shiftcls存儲(chǔ)了類(lèi)對(duì)象和元類(lèi)對(duì)象的內(nèi)存地址,需要同ISA_MASK進(jìn)行按位與 &運(yùn)算才可以取出其內(nèi)存地址值.
⑤. isa原理探索
⑤.1 isa初始化
在之前的iOS之武功秘籍①:OC對(duì)象原理-上(alloc & init & new)一文中輕描淡寫(xiě)的提了一句obj->initInstanceIsa(cls, hasCxxDtor) —— 只知道內(nèi)部調(diào)用initIsa(cls, true, hasCxxDtor)初始化isa,并沒(méi)有對(duì)isa進(jìn)行細(xì)說(shuō).

⑤.2 initIsa分析

-
isa_t newisa(0)相當(dāng)于初始化isa這個(gè)東西,newisa.相當(dāng)于給isa賦值屬性. -
SUPPORT_INDEXED_ISA適用于WatchOS,isa作為聯(lián)合體具有互斥性,而cls、bits是isa的元素,所以當(dāng)!nonpointer=true時(shí)對(duì)cls進(jìn)行賦值操作,為false是對(duì)bits進(jìn)行賦值操作(反正都是一家人,共用一塊內(nèi)存地址).
⑤.3 驗(yàn)證isa指針 位域(0-64)
根據(jù)前文提及的0-64位域,可以在這里通過(guò)initIsa方法證明isa指針中有這些位域(目前是處于macOS,所以使用的是x86_64).
- 首先通過(guò)
main中的TCJPerson斷點(diǎn) -->initInstanceIsa-->initIsa-->isa_t newisa(0)完成isa初始化. - 執(zhí)行
LLDB指令:p newisa,得到newisa的詳細(xì)信息 - 繼續(xù)往下執(zhí)行,走到
newisa.bits = ISA_MAGIC_VALUE;下一行,表示為isa的bits成員賦值,重新執(zhí)行LLDB命令p newisa,得到的結(jié)果如下
通過(guò)與前一個(gè)newsize的信息對(duì)比,發(fā)現(xiàn)isa指針中有一些變化,如下圖所示

- 其中
magic是59是由于將isa指針地址轉(zhuǎn)換為二進(jìn)制,從47(因?yàn)榍懊嬗?個(gè)位域,共占用47位,地址是從0開(kāi)始)位開(kāi)始讀取6位,再轉(zhuǎn)換為十進(jìn)制,如下圖所示
⑥. isa與類(lèi)的關(guān)聯(lián)
cls 與 isa 關(guān)聯(lián)原理就是isa指針中的shiftcls位域中存儲(chǔ)了類(lèi)信息,其中initInstanceIsa的過(guò)程是將 calloc返回的指針 和當(dāng)前的 類(lèi)cls 關(guān)聯(lián)起來(lái),有以下幾種驗(yàn)證方式:
- 【方式一】通過(guò)
initIsa方法中的newisa.setClass(cls, this);方法里面的shiftcls = (uintptr_t)newCls >> 3驗(yàn)證 - 【方式二】通過(guò)
isa指針地址與ISA_MSAK的值&來(lái)驗(yàn)證 - 【方式三】通過(guò)
runtime的方法object_getClass驗(yàn)證 - 【方式四】通過(guò)
位運(yùn)算驗(yàn)證
方式一:通過(guò) initIsa 方法
-
運(yùn)行至
newisa.setClass(cls, this);方法中shiftcls = (uintptr_t)newCls >> 3;前一步,其中shiftcls存儲(chǔ)當(dāng)前類(lèi)的值信息- 此時(shí)查看
cls,是TCJPerson類(lèi) -
shiftcls賦值的邏輯是將TCJPerson進(jìn)行編碼后,右移3位
- 此時(shí)查看
-
執(zhí)行
LLDB命令p (uintptr_t)cls,結(jié)果為(uintptr_t) $2 = 4295000336,再右移三位,有以下兩種方式(任選其一),將得到536875042存儲(chǔ)到newisa的shiftcls中p (uintptr_t)cls >> 3- 通過(guò)上一步的結(jié)果
$2,執(zhí)行LLDB命令p $2 >> 3
-
繼續(xù)執(zhí)行程序到
isa = newisa;部分,此時(shí)執(zhí)行p newisa
與bits賦值結(jié)果的對(duì)比,bits的位域中有兩處變化
-
cls由默認(rèn)值,變成了TCJPerson,將isa與cls完美關(guān)聯(lián) -
shiftcls由0變成了536875042
所以isa中通過(guò)初始化后的成員的值變化過(guò)程,如下圖所示

為什么在shiftcls賦值時(shí)需要類(lèi)型強(qiáng)轉(zhuǎn)?
因?yàn)?code>內(nèi)存的存儲(chǔ)不能存儲(chǔ)字符串,機(jī)器碼只能識(shí)別 0 、1這兩種數(shù)字,所以需要將其轉(zhuǎn)換為uintptr_t數(shù)據(jù)類(lèi)型,這樣shiftcls中存儲(chǔ)的類(lèi)信息才能被機(jī)器碼理解, 其中uintptr_t是long類(lèi)型.
為什么需要右移3位?
主要是由于shiftcls處于isa指針地址的中間部分,前面還有3個(gè)位域,為了不影響前面的3個(gè)位域的數(shù)據(jù),需要右移將其抹零.
方式二:通過(guò) isa & ISA_MSAK

方式三:通過(guò) object_getClass
通過(guò)查看object_getClass的源碼實(shí)現(xiàn),同樣可以驗(yàn)證isa與類(lèi)關(guān)聯(lián)的原理,有以下幾步:
-
main.m中導(dǎo)入#import <objc/runtime.h> - 通過(guò)
runtime的api,即object_getClass函數(shù)獲取類(lèi)信息
object_getClass(<#id _Nullable obj#>)
-
查看
object_getClass函數(shù) 源碼的實(shí)現(xiàn) -
點(diǎn)擊進(jìn)入
object_getClass底層實(shí)現(xiàn) -
進(jìn)入
getIsa的源碼實(shí)現(xiàn) -
點(diǎn)擊
ISA(),進(jìn)入源碼,在點(diǎn)擊getDecodedClass -
接著點(diǎn)擊
getClass 這與方式二中的原理是一致的,獲得當(dāng)前的類(lèi)信息,從這里也可以得出 cls 與 isa 已經(jīng)完美關(guān)聯(lián)
方式四:通過(guò)位運(yùn)算
-
回到
_class_createInstanceFromZone方法.通過(guò)x/4gx obj得到obj的存儲(chǔ)信息,當(dāng)前類(lèi)的信息存儲(chǔ)在isa指針中,且isa中的shiftcls此時(shí)占44位(因?yàn)樘幱?code>macOS環(huán)境) -
想要讀取中間的
44位類(lèi)信息,就需要經(jīng)過(guò)位運(yùn)算,將右邊3位,和左邊除去44位以外的部分都抹零,其相對(duì)位置是不變的.其位運(yùn)算過(guò)程如圖所示,其中shiftcls即為需要讀取的類(lèi)信息- 將
isa地址右移3位:p/x 0x011d800100008111 >> 3,得到0x0023b00020001022 - 在將得到的
0x0023b00020001022``左移20位:p/x 0x0023b00020001022 << 20,得到0x0002000102200000 - 為什么是
左移20位?因?yàn)?code>先右移了3位,相當(dāng)于向右偏移了3位,而左邊需要抹零的位數(shù)有17位,所以一共需要移動(dòng)20位 - 將得到的
0x0002000041d00000再右移17位:p/x 0x0002000102200000 >> 17得到新的0x0000000100008110
- 將
-
獲取
cls的地址 與 上面的進(jìn)行驗(yàn)證 :p/x cls也得出0x0000000100008110,所以由此可以證明cls與isa是關(guān)聯(lián)的.
三、isa走位分析
③.1 類(lèi)在內(nèi)存中只會(huì)存在一份
我們都知道對(duì)象可以創(chuàng)建多個(gè),那么類(lèi)是否也可以創(chuàng)建多個(gè)呢? 答案是一個(gè).怎么驗(yàn)證它呢? 來(lái)我們看下面代碼及打印結(jié)果:
通過(guò)運(yùn)行結(jié)果證明了類(lèi)在內(nèi)存中只會(huì)存在一份.
③.2.1 通過(guò)對(duì)象/類(lèi)查看isa走向
類(lèi)其實(shí)和實(shí)例對(duì)象一樣,都是由上級(jí)實(shí)例化出來(lái)的——類(lèi)的上級(jí)叫做元類(lèi).
我們先用p/x打印類(lèi)的內(nèi)存地址,再用x/4gx打印內(nèi)存結(jié)構(gòu)取到對(duì)應(yīng)的isa,再用& ISA_MASK進(jìn)行偏移得到isa指向的上級(jí)(等同于object_getClass)依次循環(huán).

①打印TCJPerson類(lèi)取得isa
②由TCJPerson類(lèi)進(jìn)行偏移得到TCJPerson元類(lèi)指針,打印TCJPerson元類(lèi)取得isa
③由TCJPerson元類(lèi)進(jìn)行偏移得到NSObject根元類(lèi)指針,打印NSObject根元類(lèi)取得isa
④由NSObject根元類(lèi)進(jìn)行偏移得到NSObject根元類(lèi)本身指針
⑤打印NSObject根類(lèi)取得isa
⑥由NSObject根類(lèi)進(jìn)行偏移得到NSObject根元類(lèi)指針
結(jié)論:
①實(shí)例對(duì)象-> 類(lèi)對(duì)象 -> 元類(lèi) -> 根元類(lèi) -> 根元類(lèi)(本身)
②NSObject(根類(lèi)) -> 根元類(lèi) -> 根元類(lèi)(本身)
③指向根元類(lèi)的isa都是一樣的
③.2.2 通過(guò)NSObject查看isa走向

因?yàn)槭?code>NSObject(根類(lèi))它的元類(lèi)就是根元類(lèi)——輸出可得根元類(lèi)指向自己
③.2.3 證明類(lèi)、元類(lèi)是系統(tǒng)創(chuàng)建的
①運(yùn)行時(shí)偽證法

在main之前TCJPerson類(lèi)和TCJPerson元類(lèi)已經(jīng)存在在內(nèi)存中,不過(guò)此時(shí)程序已經(jīng)在運(yùn)行了,并沒(méi)有什么說(shuō)服力.
②查看MachO文件法
編譯項(xiàng)目后,使用MachoView打開(kāi)程序二進(jìn)制可執(zhí)行文件查看:

結(jié)論:
- 對(duì)象是程序員(猿)根據(jù)類(lèi)實(shí)例化來(lái)的
- 類(lèi)是代碼編寫(xiě)的,內(nèi)存中只有一份,是系統(tǒng)創(chuàng)建的
- 元類(lèi)是系統(tǒng)編譯時(shí),系統(tǒng)編譯器創(chuàng)建的,便于方法的編譯
③.3 isa走位圖

我們對(duì)上圖進(jìn)行總結(jié)一波:圖中實(shí)線是 super_class指針,它代表著繼承鏈的關(guān)系.虛線是isa指針.
isa走位(虛線):實(shí)例對(duì)象-> 類(lèi)對(duì)象 -> 元類(lèi) -> 根元類(lèi) -> 根元類(lèi)(本身)
繼承關(guān)系(實(shí)線):NSObject父類(lèi)為nil,根元類(lèi)的父類(lèi)為NSObject
1.Root class (class)其實(shí)就是NSObject,NSObject是沒(méi)有超類(lèi)的,所以Root class(class)的superclass指向nil(NSObject父類(lèi)是nil).
2.每個(gè)Class都有一個(gè)isa指針指向唯一的Meta class.
3.Root class(meta)的superclass指向Root class(class),也就是NSObject,形成一個(gè)回路.這說(shuō)明Root class(meta)是繼承至Root class(class)(根元類(lèi)的父類(lèi)是NSObject).
4.每個(gè)Meta class的isa指針都指向Root class (meta)
-
instance對(duì)象的isa指向class對(duì)象 -
class對(duì)象的isa指向meta-class對(duì)象 -
meta-class對(duì)象的isa指向基類(lèi)的meta-class對(duì)象
寫(xiě)在后面
和諧學(xué)習(xí),不急不躁.我還是我,顏色不一樣的煙火.























