iOS之武功秘籍③:OC對(duì)象原理-下(isa的初始化和指向分析與對(duì)象的本質(zhì))

iOS之武功秘籍 文章匯總

寫(xiě)在前面

iOS之武功秘籍②:OC對(duì)象原理-中(內(nèi)存對(duì)齊和malloc源碼分析)一文中講了對(duì)象中的屬性在內(nèi)存中的排列 -- 內(nèi)存對(duì)齊 和malloc源碼分析,那么接下我們就來(lái)分析一下isa的初始化和指向分析與對(duì)象的本質(zhì)

本節(jié)可能用到的秘籍Demo

一、對(duì)象的本質(zhì)

① Clang的了解

  • Clang是?個(gè)由Apple主導(dǎo)編寫(xiě),基于LLVMC/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ò)終端,利用clangmain.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)的settergetter方法,且?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是如何將clsisa關(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、leftright,通過(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值只有兩種情況:01,占據(jù)一個(gè)字節(jié)的內(nèi)存空間.而一個(gè)字節(jié)的內(nèi)存空間中又有8個(gè)二進(jìn)制位,并且二進(jìn)制同樣只有01,那么我們完全可以使用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)setget方法.

@interface TCJCar(){
    char _frontBackLeftRight;
}

如果我們賦值_frontBackLeftRight1,即0b 0000 0001,只使用8個(gè)二進(jìn)制位中的最后4個(gè)分別用0或者1來(lái)代表frontback、left、right的值.那么此時(shí)front、back、left、right的狀態(tài)為:

我們可以分別聲明front、backleft、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)frontYES時(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,那么正好跟frontYES對(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):

  1. 如果一個(gè)字節(jié)所??臻g不夠存放另一位域時(shí),應(yīng)從下一單元起存放該位域.
  2. 位域的長(zhǎng)度不能大于數(shù)據(jù)類(lèi)型本身的長(zhǎng)度,比如int類(lèi)型就不能超過(guò)32位二進(jìn)位.
  3. 位域可以無(wú)位域名,這時(shí)它只用來(lái)作填充或調(diào)整位置.無(wú)名的位域是不能使用的.

使用位域優(yōu)化后的代碼:

來(lái)測(cè)試看一下是否正確,這次我們將front設(shè)為YESback設(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è)為YESback設(shè)為NO、left設(shè)為NO、right設(shè)為YES:

通過(guò)結(jié)果我們看到依舊能完成賦值和取值.
這其中_frontBackLeftRight聯(lián)合體只占用一個(gè)字節(jié),因?yàn)榻Y(jié)構(gòu)體中front、backleft、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、bitsisa的元素,所以當(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;下一行,表示為isabits成員賦值,重新執(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)

clsisa 關(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位
  • 執(zhí)行LLDB命令p (uintptr_t)cls,結(jié)果為(uintptr_t) $2 = 4295000336,再右移三位,有以下兩種方式(任選其一),將得到536875042存儲(chǔ)到newisashiftcls

    • 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,將isacls完美關(guān)聯(lián)
  • shiftcls0變成了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_tlong類(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ò)runtimeapi,即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,所以由此可以證明 clsisa 是關(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 classisa指針都指向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í),不急不躁.我還是我,顏色不一樣的煙火.

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容