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

iOS之武功秘籍 文章匯總

寫在前面

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

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

一评矩、對象的本質(zhì)

① Clang的了解

  • Clang是?個由Apple主導(dǎo)編寫拭嫁,基于LLVMC/C++/Objective-C輕量級編譯器.源代碼發(fā)布于LLVM BSD協(xié)議下.Clang將?持其普通lambda表達式肚豺、返回類型的簡化處理以及更好的處理constexpr關(guān)鍵字筷狼。

  • 它與GNU C語?規(guī)范?乎完全兼容(當然翩概,也有部分不兼容的內(nèi)容实幕,
    包括編譯命令選項也會有點差異)卸奉,并在此基礎(chǔ)上增加了額外的語法特性,?如C函數(shù)重載
    (通過__attribute__((overloadable))來修飾函數(shù))庐船,其?標(之?)就是超越GCC.

  • 它主要是用于底層編譯,將一些OC文件輸出成C++文件嘲更,例如main.m 輸出成main.cpp筐钟,其目的是為了更好的觀察底層的一些結(jié)構(gòu)實現(xiàn)的邏輯,方便理解底層原理

② Clang操作指令

// 把?標?件編譯成c++?件 -- 將 main.m 編譯成 main.cpp
clang -rewrite-objc main.m -o main.cpp 

// UIKit報錯問題 -- 將 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`安裝的時候順帶安裝了`xcrun`命令赋朦,`xcrun`命令在`clang`的基礎(chǔ)上進?了?些封裝篓冲,要更好??些
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 (?機) 

③ 探索對象本質(zhì)

  • 構(gòu)建測試代碼
  • 通過終端,利用clangmain.m編譯成 main.cpp,在終端輸入以下命令

    • xcrun -sdk iphonesimulator clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp
  • 打開編譯好的main-arm64.cpp宠哄,找TCJPerson的定義壹将,發(fā)TCJPerson在底層會被編譯成struct 結(jié)構(gòu)體

通過編譯好的main-arm64.cpp我們可以看到:

  • NSObject的底層實現(xiàn)其實就是一個包含一個isa指針的結(jié)構(gòu)體.
  • Class其實就是一個指針,指向了objc_class類型的結(jié)構(gòu)體.
  • TCJPerson_IMPL結(jié)構(gòu)體內(nèi)有三個成員變量:
    • isa 繼承自父類NSObject
    • helloName
    • _name
  • 對于屬性name:底層編譯會生成相應(yīng)的setter(_I_TCJPerson_setName_,setter方法內(nèi)調(diào)用objc_setProperty方法)毛嫉、getter(_I_TCJPerson_name)方法,且?guī)臀覀冝D(zhuǎn)化為_name
  • 對于成員變量helloName:底層編譯不會生成相應(yīng)的setter诽俯、getter方法,且沒有轉(zhuǎn)化為_helloName

通過上述分析,理解了OC對象的本質(zhì) -- 結(jié)構(gòu)體承粤,但是看到NSObject的定義暴区,會產(chǎn)生一個疑問:為什么isa的類型是Class?

  • iOS之武功秘籍①:OC對象原理-上(alloc & init & new)文章中,提及過alloc方法的核心之一的initInstanceIsa方法,通過查看這個方法的源碼實現(xiàn)辛臊,我們發(fā)現(xiàn)仙粱,在初始化isa指針時,是通過isa_t類型初始化的
  • 而在NSObject定義中isa的類型是Class彻舰,其根本原因是由于isa對外反饋的是類信息伐割,為了讓開發(fā)人員更加清晰明確候味,需要在isa返回時做了一個類型強制轉(zhuǎn)換,類似于swift中的 as 的強轉(zhuǎn).源碼中isa的強轉(zhuǎn)如下圖所示

④ 探究屬性get隔心、set方法

通過上文的分析我們知道:對于屬性name:底層編譯會生成相應(yīng)的settergetter方法,且?guī)臀覀冝D(zhuǎn)化為_name成員變量,而對于成員變量helloName:底層編譯不會生成相應(yīng)的setter白群、getter方法,且沒有轉(zhuǎn)化為_helloName.這其中的setter方法的實現(xiàn)依賴于runtime中的objc_setProperty.

接下來我們來看看objc_setProperty的底層實現(xiàn)

  • objc4源碼中全局搜索objc_setProperty,找到objc_setProperty的源碼實現(xiàn)

  • 進入reallySetProperty的源碼實現(xiàn)济炎,其方法的原理就是新值retain川抡,舊值release

總結(jié):
通過對objc_setProperty的底層源碼探索,有以下幾點說明:

  • objc_setProperty方法的目的適用于關(guān)聯(lián)上層set方法以及底層set方法须尚,其本質(zhì)就是一個接口

  • 這么設(shè)計的原因是崖堤,上層的set方法有很多,如果直接調(diào)用底層set方法耐床,會產(chǎn)生很多的臨時變量密幔,當你想查找一個sel時,會非常麻煩

  • 基于上述原因撩轰,蘋果采用了適配器設(shè)計模式(即將底層接口適配為客戶端需要的接口)胯甩,對外提供一個接口,供上層的set方法使用堪嫂,對內(nèi)調(diào)用底層的set方法偎箫,使其相互不受影響,即無論上層怎么變皆串,下層都是不變的淹办,或者下層的變化也無法影響上層,主要是達到上下層接口隔離的目的.

下圖是上層恶复、隔離層怜森、底層之間的關(guān)系

  • 外部set方法: 上層 - 個性化定制層(例如setName、setAge等)
  • objc_setProperty:接口隔離層 (將外界信息轉(zhuǎn)化為對內(nèi)存地址和值的操作)
  • reallySetProperty:底層實現(xiàn)層 (賦值和內(nèi)存管理)

二谤牡、isa底層原理

iOS之武功秘籍①:OC對象原理-上(alloc & init & new)iOS之武功秘籍②:OC對象原理-中(內(nèi)存對齊和malloc源碼分析)中分別分析了alloc中3核心的前兩個副硅,今天來探索initInstanceIsa是如何將clsisa關(guān)聯(lián)的.

在此之前,需要先了解什么是聯(lián)合體翅萤,為什么isa的類型isa_t是使用聯(lián)合體定義的.那么什么是聯(lián)合體?什么又是位域?

①. 位域

①.1 定義

有些信息在存儲時恐疲,并不需要占用一個完整的字節(jié)套么,而只需占一個或幾個二進制位.例如在存放一個開關(guān)量時流纹,只有0和1兩種狀態(tài),用1位二進位即可.為了節(jié)省存儲空間并使處理簡便违诗,C語言提供了一種數(shù)據(jù)結(jié)構(gòu)漱凝,稱為位域位段.

所謂位域就是把一個字節(jié)中的二進位劃分為幾個不同的區(qū)域,并說明每個區(qū)域的位數(shù).每個域有一個域名诸迟,允許在程序中按域名進行操作——這樣就可以把幾個不同的對象用一個字節(jié)的二進制位域來表示.

①.2 與結(jié)構(gòu)體比較

位域的使用與結(jié)構(gòu)體相仿茸炒,它本身也是結(jié)構(gòu)體的一種.

// 結(jié)構(gòu)體
struct TCJStruct {
    // (類型說明符 元素);
    char a;
    int b;
} TCJStr;

// 位域
struct TCJBitArea {
    // (類型說明符 位域名: 位域長度);
    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 定義

當多個數(shù)據(jù)需要共享內(nèi)存或者多個數(shù)據(jù)每次只取其一時愕乎,可以利用聯(lián)合體(union)

  • 聯(lián)合體是一個結(jié)構(gòu)
  • 它的所有成員相對于基地址的偏移量都為0
  • 此結(jié)構(gòu)空間要大到足夠容納最"寬"的成員
  • 各變量是“互斥”的——共用一個內(nèi)存首地址,聯(lián)合變量可被賦予任一成員值,但每次只能賦一種值, 賦入新值則沖去舊值.

②.2 與結(jié)構(gòu)體比較

結(jié)構(gòu)體每個成員依次存儲壁公,聯(lián)合體中所有成員的偏移地址都是0感论,也就是所有成員是疊在一起的,所以在聯(lián)合體中在某一時刻紊册,只有一個成員有效——結(jié)構(gòu)體內(nèi)存大小取決于所有元素杖挣,聯(lián)合體取決于最大那個

②.3 補充知識--位運算符

在計算機語言中春瞬,除了加、減、乘路召、除等這樣的算術(shù)運算符之外還有很多運算符凤覆,這里只為大家簡單講解一下位運算符.
位運算符用來對二進制位進行操作似扔,當然租冠,操作數(shù)只能為整型和字符型數(shù)據(jù)C語言中六種位運算符:&按位與遏片、|按位或嘹害、^按位異或、~非吮便、<<左移和>>右移笔呀。
我們依舊引用上面的電燈開關(guān)論,只不過現(xiàn)在我們有兩個開關(guān):開關(guān)A和開關(guān)B髓需,1代表開凿可,0代表關(guān).

1)按位與&

有0出0,全1出1.

A B &
0 0 0
1 0 0
0 1 0
1 1 1

我們可以理解為在按位與運算中,兩個開關(guān)是串聯(lián)的,如果我們想要燈亮,需要兩個開關(guān)都打開燈才會亮,所以是1 & 1 = 1. 如果任意一個開關(guān)沒有打開,燈都不會亮,所以其他運算都是0.

2)按位或 |

有1出1,全0出0.

A B I
0 0 0
1 0 1
0 1 1
1 1 1

在按位或運算中,我們可以理解為兩個開關(guān)是并聯(lián)的,即一個開關(guān)開,燈就會亮.只有當兩個開關(guān)都是關(guān)的.燈才不會亮.

3)按位異或^

相同為0,不同為1.

A B ^
0 0 0
1 0 1
0 1 1
1 1 0
4)非 ~

非運算即取反運算,在二進制中 1 變 0 授账,0 變 1。例如110101進行非運算后為001010惨驶,即1010.

5)左移 <<

左移運算就是把<<左邊的運算數(shù)的各二進位全部左移若干位白热,移動的位數(shù)即<<右邊的數(shù)的數(shù)值,高位丟棄粗卜,低位補0.
左移n位就是乘以2的n次方.例如:a<<4是指把a的各二進位向左移動4位.如a=00000011(十進制3)屋确,左移4位后為00110000(十進制48).

6)右移 >>

右移運算就是把>>左邊的運算數(shù)的各二進位全部右移若干位,>>右邊的數(shù)指定移動的位數(shù).例如:設(shè) a=15续扔,a>>2 表示把00001111右移為00000011(十進制3).

②.4 位運算符的運用

1)取值

可以利用按位與 &運算取出指定位的值,具體操作是想取出哪一位的值就將那一位置為1,其它位都為0,然后同原數(shù)據(jù)進行按位與計算,即可取出特定的位.

例: 0000 0011取出倒數(shù)第三位的值

// 想取出倒數(shù)第三位的值攻臀,就將倒數(shù)第三位的值置為1,其它位為0纱昧,跟原數(shù)據(jù)按位與運算
  0000 0011
& 0000 0100
------------
  0000 0000  // 得出按位與運算后的結(jié)果刨啸,即可拿到原數(shù)據(jù)中倒數(shù)第三位的值為0

上面的例子中,我們從0000 0011中取值,則有0000 0011被稱之為源碼.進行按位與操作設(shè)定的0000 0100稱之為掩碼.

2)設(shè)值

可以通過按位或 |運算符將某一位的值設(shè)為1或0.具體操作是:
想將某一位的值置為1的話,那么就將掩碼中對應(yīng)位的值設(shè)為1识脆,掩碼其它位為0设联,將源碼與掩碼進行按位或操作即可.

例: 將0000 0011倒數(shù)第三位的值改為1

// 改變倒數(shù)第三位的值善已,就將掩碼倒數(shù)第三位的值置為1,其它位為0离例,跟源碼按位或運算
  0000 0011
| 0000 0100
------------
  0000 0111  // 即可將源碼中倒數(shù)第三位的值改為1

想將某一位的值置為0的話换团,那么就將掩碼中對應(yīng)位的值設(shè)為0,掩碼其它位為1宫蛆,將源碼與掩碼進行按位或操作即可.

例: 將0000 0011倒數(shù)第二位的值改為0

// 改變倒數(shù)第二位的值艘包,就將掩碼倒數(shù)第二位的值置為0,其它位為1耀盗,跟源碼按位或運算
  0000 0011
| 1111 1101
------------
  0000 0001  // 即可將源碼中倒數(shù)第二位的值改為0

到這里相信大家對位運算符有了一定的了解.

③. 結(jié)構(gòu)體位域與聯(lián)合體的使用

我們來看下面的??:我們聲明一個TCJCar類,類中有四個BOOL類型的屬性,分別為front想虎、backleft袍冷、right,通過這四個屬性來判斷這輛小車的行駛方向.

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

我們看到,一個TCJCar類的對象占據(jù)16個字節(jié).其中包括一個isa指針和四個BOOL類型的屬性,8+1+1+1+1=12,根據(jù)內(nèi)存對齊原則,所以一個TCJCar類的對象占16個字節(jié).

我們知道,BOOL值只有兩種情況:01磷醋,占據(jù)一個字節(jié)的內(nèi)存空間.而一個字節(jié)的內(nèi)存空間中又有8個二進制位,并且二進制同樣只有01胡诗,那么我們完全可以使用1個二進制位來表示一個BOOL值.也就是說我們上面聲明的四個BOOL值最終只使用4個二進制位就可以邓线,這樣就節(jié)省了內(nèi)存空間.那我們?nèi)绾螌崿F(xiàn)呢?
想要實現(xiàn)四個BOOL值存放在一個字節(jié)中煌恢,我們可以通過char類型的成員變量來實現(xiàn).char類型占一個字節(jié)內(nèi)存空間骇陈,也就是8個二進制位.可以使用其中最后四個二進制位來存儲4個BOOL值.
當然我們不能把char類型寫成屬性,因為一旦寫成屬性,系統(tǒng)會自動幫我們添加成員變量,自動實現(xiàn)setget方法.

@interface TCJCar(){
    char _frontBackLeftRight;
}

如果我們賦值_frontBackLeftRight1,即0b 0000 0001,只使用8個二進制位中的最后4個分別用0或者1來代表frontback瑰抵、left你雌、right的值.那么此時frontback二汛、left婿崭、right的狀態(tài)為:

我們可以分別聲明frontback肴颊、left氓栈、right的掩碼,來方便我們進行下一步的位運算取值和賦值:

#define TCJDirectionFrontMask 0b00001000 //此二進制數(shù)對應(yīng)十進制數(shù)為 8
#define TCJDirectionBackMask  0b00000100 //此二進制數(shù)對應(yīng)十進制數(shù)為 4
#define TCJDirectionLeftMask  0b00000010 //此二進制數(shù)對應(yīng)十進制數(shù)為 2
#define TCJDirectionRightMask 0b00000001 //此二進制數(shù)對應(yīng)十進制數(shù)為 1

通過對位運算符的左移<<和右移>>的了解,我們可以將上面的代碼優(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婿着,將源碼和掩碼進行按位或運算
        _frontBackLeftRight |= TCJDirectionFrontMask;
    } else {// 如果需要將值置為0 // 將源碼和按位取反后的掩碼進行按位與運算
        _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);
}

此處需要注意的是授瘦,代碼中!為邏輯運算符非,因為_frontBackLeftRight & TCJDirectionFrontMask代碼執(zhí)行后竟宋,返回的肯定是一個整型數(shù)提完,如當frontYES時,說明二進制數(shù)為0b 0000 1000丘侠,對應(yīng)的十進制數(shù)為8徒欣,那么進行一次邏輯非運算后,!(8)的值為0蜗字,對0再進行一次邏輯非運算!(0)帚称,結(jié)果就成了1官研,那么正好跟frontYES對應(yīng).所以此處進行兩次邏輯非運算,!!.
當然,還要實現(xiàn)初始化方法:

- (instancetype)init
{
    self = [super init];
    if (self) {
        _frontBackLeftRight = 0b00001000;
    }
    return self;
}

通過測試驗證,我們完成了取值和賦值:

③.1 使用結(jié)構(gòu)體位域優(yōu)化代碼

我們在上文講到了位域的概念,那么我們就可以使用結(jié)構(gòu)體位域來優(yōu)化一下我們的代碼.這樣就不用再額外聲明上面代碼中的掩碼部分了.位域聲明格式是位域名: 位域長度.
在使用位域的過程中需要注意以下幾點:

  1. 如果一個字節(jié)所蚀扯茫空間不夠存放另一位域時戏羽,應(yīng)從下一單元起存放該位域.
  2. 位域的長度不能大于數(shù)據(jù)類型本身的長度,比如int類型就不能超過32位二進位.
  3. 位域可以無位域名楼吃,這時它只用來作填充或調(diào)整位置.無名的位域是不能使用的.

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

來測試看一下是否正確,這次我們將front設(shè)為YES始花、back設(shè)為NOleft設(shè)為NO孩锡、right設(shè)為YES:

依舊能完成賦值和取值.
但是代碼這樣優(yōu)化后我們?nèi)サ袅搜诖a和初始化的代碼,可讀性很差,我們繼續(xù)使用聯(lián)合體進行優(yōu)化:

③.2 使用聯(lián)合體優(yōu)化代碼

我們可以使用比較高效的位運算來進行賦值和取值酷宵,使用union聯(lián)合體來對數(shù)據(jù)進行存儲。這樣不僅可以增加讀取效率躬窜,還可以增強代碼可讀性.

#import "TCJCar.h"

//#define TCJDirectionFrontMask 0b00001000 //此二進制數(shù)對應(yīng)十進制數(shù)為 8
//#define TCJDirectionBackMask  0b00000100 //此二進制數(shù)對應(yīng)十進制數(shù)為 4
//#define TCJDirectionLeftMask  0b00000010 //此二進制數(shù)對應(yīng)十進制數(shù)為 2
//#define TCJDirectionRightMask 0b00000001 //此二進制數(shù)對應(yīng)十進制數(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)體僅僅是為了增強代碼可讀性
        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

來我們測試看一下是否正確,這次我們依舊將front設(shè)為YES浇垦、back設(shè)為NOleft設(shè)為NO荣挨、right設(shè)為YES:

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

④. isa_t聯(lián)合體

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

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

從圖中可以看到ISA_MASK的值轉(zhuǎn)化為二進制中有33位都為1,上文講到按位與運算是可以取出這33位中的值.那么就說明同ISA_MASK進行按位與運算就可以取出類對象和元類對象的內(nèi)存地址信息了.

不同架構(gòu)下isa所占內(nèi)存均為8字節(jié)——64位口锭,但內(nèi)部分布有所不同朦前,arm64架構(gòu)isa內(nèi)部成員分布如下圖

  • nonpointer:表示是否對isa指針開啟指針優(yōu)化 —— 0純isa指針1:不止是類對象地址鹃操,isa 中包含了類信息韭寸、對象的引用計數(shù)

  • has_assoc關(guān)聯(lián)對象標志位0沒有荆隘,1存在

  • has_cxx_dtor:該對象是否有 C++ 或者 Objc 的析構(gòu)器恩伺,如果有析構(gòu)函數(shù),則需要做析構(gòu)邏輯, 如果沒有臭胜,則可以更快的釋放對象

  • shiftcls:存儲類指針的值(類的地址),即類癞尚、元類對象的內(nèi)存地址信息.在開啟指針優(yōu)化的情況下耸三,在 arm64 架構(gòu)中有 33 位用來存儲類指針

  • magic:用于調(diào)試器判斷當前對象是真的對象還是沒有初始化的空間

  • weakly_referenced:對象是否被指向或者曾經(jīng)指向一個 ARC 的弱變量,沒有弱引用的對象可以更快釋放

  • deallocating:標志對象是否正在釋放內(nèi)存

  • has_sidetable_rc:當對象引用技術(shù)大于 10 時浇揩,則需要借用該變量存儲進位

  • extra_rc:當表示該對象的引用計數(shù)值仪壮,實際上是引用計數(shù)值減 1, 例如胳徽,如果對象的引用計數(shù)為 10积锅,那么 extra_rc 為 9爽彤。如果引用計數(shù)大于 10, 則需要使用到下面的 has_sidetable_rc

上面所說的當對象引用技術(shù)大于 10 時,那是一個例如, 不是具體的10.

至此我們已經(jīng)對isa指針有了新的認識缚陷,arm64架構(gòu)之后适篙,isa指針不單單只存儲了類對象和元類對象的內(nèi)存地址,而是使用聯(lián)合體的方式存儲了更多信息箫爷,其中shiftcls存儲了類對象和元類對象的內(nèi)存地址嚷节,需要同ISA_MASK進行按位與 &運算才可以取出其內(nèi)存地址值.

⑤. isa原理探索

⑤.1 isa初始化

在之前的iOS之武功秘籍①:OC對象原理-上(alloc & init & new)一文中輕描淡寫的提了一句obj->initInstanceIsa(cls, hasCxxDtor) —— 只知道內(nèi)部調(diào)用initIsa(cls, true, hasCxxDtor)初始化isa,并沒有對isa進行細說.

⑤.2 initIsa分析

  • isa_t newisa(0)相當于初始化isa這個東西虎锚,newisa.相當于給isa賦值屬性.
  • SUPPORT_INDEXED_ISA適用于WatchOS,isa作為聯(lián)合體具有互斥性硫痰,而clsbitsisa的元素窜护,所以當!nonpointer=true時對cls進行賦值操作效斑,為false是對bits進行賦值操作(反正都是一家人,共用一塊內(nèi)存地址).

⑤.3 驗證isa指針 位域(0-64)

根據(jù)前文提及的0-64位域柱徙,可以在這里通過initIsa方法證明isa指針中有這些位域(目前是處于macOS缓屠,所以使用的是x86_64).

  • 首先通過main中的TCJPerson 斷點 --> initInstanceIsa --> initIsa --> isa_t newisa(0)完成 isa初始化.
  • 執(zhí)行LLDB指令: p newisa,得到newisa的詳細信息
  • 繼續(xù)往下執(zhí)行,走到newisa.bits = ISA_MAGIC_VALUE;下一行坐搔,表示為isabits成員賦值藏研,重新執(zhí)行LLDB命令p newisa,得到的結(jié)果如下

通過與前一個newsize的信息對比概行,發(fā)現(xiàn)isa指針中有一些變化蠢挡,如下圖所示

  • 其中magic是59是由于將isa指針地址轉(zhuǎn)換為二進制,從47(因為前面有4個位域凳忙,共占用47位业踏,地址是從0開始)位開始讀取6位,再轉(zhuǎn)換為十進制涧卵,如下圖所示

⑥. isa與類的關(guān)聯(lián)

clsisa 關(guān)聯(lián)原理就是isa指針中的shiftcls位域中存儲了類信息勤家,其中initInstanceIsa的過程是將 calloc返回的指針 和當前的 類cls 關(guān)聯(lián)起來,有以下幾種驗證方式:

  • 【方式一】通過initIsa方法中的newisa.setClass(cls, this);方法里面的 shiftcls = (uintptr_t)newCls >> 3驗證
  • 【方式二】通過isa指針地址與ISA_MSAK 的值 &來驗證
  • 【方式三】通過runtime的方法object_getClass驗證
  • 【方式四】通過位運算驗證

方式一:通過 initIsa 方法

  • 運行至newisa.setClass(cls, this);方法中shiftcls = (uintptr_t)newCls >> 3;前一步柳恐,其中 shiftcls存儲當前類的值信息

    • 此時查看cls伐脖,是TCJPerson
    • shiftcls賦值的邏輯是將 TCJPerson進行編碼后,右移3位
  • 執(zhí)行LLDB命令p (uintptr_t)cls乐设,結(jié)果為(uintptr_t) $2 = 4295000336,再右移三位讼庇,有以下兩種方式(任選其一),將得到536875042存儲到newisashiftcls

    • p (uintptr_t)cls >> 3
    • 通過上一步的結(jié)果$2近尚,執(zhí)行LLDB命令p $2 >> 3
  • 繼續(xù)執(zhí)行程序到isa = newisa;部分蠕啄,此時執(zhí)行p newisa

bits賦值結(jié)果的對比,bits的位域中有兩處變化

  • cls 由默認值,變成了TCJPerson歼跟,將isacls完美關(guān)聯(lián)
  • shiftcls0變成了536875042

所以isa中通過初始化后的成員的值變化過程和媳,如下圖所示

為什么在shiftcls賦值時需要類型強轉(zhuǎn)?
因為內(nèi)存的存儲不能存儲字符串哈街,機器碼只能識別 0 留瞳、1這兩種數(shù)字,所以需要將其轉(zhuǎn)換為uintptr_t數(shù)據(jù)類型叹卷,這樣shiftcls中存儲的類信息才能被機器碼理解撼港, 其中uintptr_tlong類型.

為什么需要右移3位?
主要是由于shiftcls處于isa指針地址的中間部分骤竹,前面還有3個位域帝牡,為了不影響前面的3個位域的數(shù)據(jù),需要右移將其抹零.

方式二:通過 isa & ISA_MSAK

方式三:通過 object_getClass

通過查看object_getClass的源碼實現(xiàn)蒙揣,同樣可以驗證isa與類關(guān)聯(lián)的原理靶溜,有以下幾步:

  • main.m中導(dǎo)入#import <objc/runtime.h>
  • 通過runtimeapi,即object_getClass函數(shù)獲取類信息
object_getClass(<#id  _Nullable obj#>)
  • 查看object_getClass函數(shù) 源碼的實現(xiàn)

  • 點擊進入object_getClass 底層實現(xiàn)

  • 進入getIsa的源碼實現(xiàn)

  • 點擊ISA()懒震,進入源碼罩息,在點擊getDecodedClass

  • 接著點擊getClass

  • 這與方式二中的原理是一致的,獲得當前的類信息,從這里也可以得出 cls 與 isa 已經(jīng)完美關(guān)聯(lián)

方式四:通過位運算

  • 回到_class_createInstanceFromZone方法.通過x/4gx obj 得到obj的存儲信息个扰,當前類的信息存儲在isa指針中瓷炮,且isa中的shiftcls此時占44位(因為處于macOS環(huán)境)

  • 想要讀取中間的44位 類信息,就需要經(jīng)過位運算 递宅,將右邊3位娘香,和左邊除去44位以外的部分抹零,其相對位置是不變的.其位運算過程如圖所示办龄,其中shiftcls即為需要讀取的類信息

    • isa地址右移3位:p/x 0x011d800100008111 >> 3 烘绽,得到0x0023b00020001022
    • 在將得到的0x0023b00020001022``左移20位:p/x 0x0023b00020001022 << 20 ,得到0x0002000102200000
    • 為什么是左移20位?因為先右移了3位俐填,相當于向右偏移了3位安接,而左邊需要抹零的位數(shù)有17位,所以一共需要移動20位
    • 將得到的0x0002000041d00000右移17位p/x 0x0002000102200000 >> 17 得到新的0x0000000100008110
  • 獲取cls的地址 與 上面的進行驗證 :p/x cls 也得出0x0000000100008110英融,所以由此可以證明 clsisa 是關(guān)聯(lián)的.

三盏檐、isa走位分析

③.1 類在內(nèi)存中只會存在一份

我們都知道對象可以創(chuàng)建多個,那么類是否也可以創(chuàng)建多個呢? 答案是一個.怎么驗證它呢? 來我們看下面代碼及打印結(jié)果:

通過運行結(jié)果證明了類在內(nèi)存中只會存在一份.

③.2.1 通過對象/類查看isa走向

其實和實例對象一樣,都是由上級實例化出來的——類的上級叫做元類.
我們先用p/x打印類的內(nèi)存地址驶悟,再用x/4gx打印內(nèi)存結(jié)構(gòu)取到對應(yīng)的isa胡野,再用& ISA_MASK進行偏移得到isa指向的上級(等同于object_getClass)依次循環(huán).

①打印TCJPerson類取得isa

②由TCJPerson類進行偏移得到TCJPerson元類指針,打印TCJPerson元類取得isa

③由TCJPerson元類進行偏移得到NSObject根元類指針撩银,打印NSObject根元類取得isa

④由NSObject根元類進行偏移得到NSObject根元類本身指針

⑤打印NSObject根類取得isa

⑥由NSObject根類進行偏移得到NSObject根元類指針

結(jié)論:
實例對象-> 類對象 -> 元類 -> 根元類 -> 根元類(本身)

NSObject(根類) -> 根元類 -> 根元類(本身)

指向根元類的isa都是一樣的

③.2.2 通過NSObject查看isa走向

因為是NSObject(根類)它的元類就是根元類——輸出可得根元類指向自己

③.2.3 證明類给涕、元類是系統(tǒng)創(chuàng)建的

①運行時偽證法

main之前TCJPerson類TCJPerson元類已經(jīng)存在在內(nèi)存中豺憔,不過此時程序已經(jīng)在運行了额获,并沒有什么說服力.

②查看MachO文件法

編譯項目后,使用MachoView打開程序二進制可執(zhí)行文件查看:

結(jié)論:

  • 對象是程序員(猿)根據(jù)類實例化來的
  • 類是代碼編寫的够庙,內(nèi)存中只有一份,是系統(tǒng)創(chuàng)建的
  • 元類是系統(tǒng)編譯時抄邀,系統(tǒng)編譯器創(chuàng)建的耘眨,便于方法的編譯

③.3 isa走位圖

我們對上圖進行總結(jié)一波:圖中實線是 super_class指針,它代表著繼承鏈的關(guān)系.虛線是isa指針.
isa走位(虛線):實例對象-> 類對象 -> 元類 -> 根元類 -> 根元類(本身)
繼承關(guān)系(實線):NSObject父類為nil,根元類的父類為NSObject

1.Root class (class)其實就是NSObject境肾,NSObject是沒有超類的剔难,所以Root class(class)superclass指向nil(NSObject父類是nil).

2.每個Class都有一個isa指針指向唯一的Meta class.

3.Root class(meta)superclass指向Root class(class),也就是NSObject奥喻,形成一個回路.這說明Root class(meta)是繼承至Root class(class)(根元類的父類是NSObject).

4.每個Meta classisa指針都指向Root class (meta)

  • instance對象的isa指向class對象
  • class對象的isa指向meta-class對象
  • meta-class對象的isa指向基類的meta-class對象

寫在后面

和諧學(xué)習(xí),不急不躁.我還是我,顏色不一樣的煙火.

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末偶宫,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子环鲤,更是在濱河造成了極大的恐慌纯趋,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,204評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件冷离,死亡現(xiàn)場離奇詭異吵冒,居然都是意外死亡,警方通過查閱死者的電腦和手機西剥,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,091評論 3 395
  • 文/潘曉璐 我一進店門痹栖,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人瞭空,你說我怎么就攤上這事揪阿。” “怎么了匙铡?”我有些...
    開封第一講書人閱讀 164,548評論 0 354
  • 文/不壞的土叔 我叫張陵图甜,是天一觀的道長。 經(jīng)常有香客問我鳖眼,道長黑毅,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,657評論 1 293
  • 正文 為了忘掉前任钦讳,我火速辦了婚禮矿瘦,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘愿卒。我一直安慰自己缚去,他們只是感情好,可當我...
    茶點故事閱讀 67,689評論 6 392
  • 文/花漫 我一把揭開白布琼开。 她就那樣靜靜地躺著易结,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上搞动,一...
    開封第一講書人閱讀 51,554評論 1 305
  • 那天躏精,我揣著相機與錄音,去河邊找鬼鹦肿。 笑死矗烛,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的箩溃。 我是一名探鬼主播瞭吃,決...
    沈念sama閱讀 40,302評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼涣旨!你這毒婦竟也來了歪架?” 一聲冷哼從身側(cè)響起霹陡,我...
    開封第一講書人閱讀 39,216評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎惠呼,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體峦耘,經(jīng)...
    沈念sama閱讀 45,661評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡辅髓,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,851評論 3 336
  • 正文 我和宋清朗相戀三年泣崩,在試婚紗的時候發(fā)現(xiàn)自己被綠了洛口。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,977評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡买优,死狀恐怖杀赢,靈堂內(nèi)的尸體忽然破棺而出湘纵,到底是詐尸還是另有隱情,我是刑警寧澤砌左,帶...
    沈念sama閱讀 35,697評論 5 347
  • 正文 年R本政府宣布汇歹,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏取视。R本人自食惡果不足惜作谭,卻給世界環(huán)境...
    茶點故事閱讀 41,306評論 3 330
  • 文/蒙蒙 一奄毡、第九天 我趴在偏房一處隱蔽的房頂上張望吼过。 院中可真熱鬧,春花似錦酱床、人聲如沸扇谣。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,898評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽簸淀。三九已至,卻和暖如春囊蓝,著一層夾襖步出監(jiān)牢的瞬間令蛉,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,019評論 1 270
  • 我被黑心中介騙來泰國打工弟劲, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留纫版,地道東北人。 一個月前我還...
    沈念sama閱讀 48,138評論 3 370
  • 正文 我出身青樓,卻偏偏與公主長得像淡溯,于是被迫代替她去往敵國和親咱娶。 傳聞我的和親對象是個殘疾皇子强品,可洞房花燭夜當晚...
    茶點故事閱讀 44,927評論 2 355

推薦閱讀更多精彩內(nèi)容

  • 用于記錄iOS底層學(xué)習(xí)的榛,以備后續(xù)回顧 OC對象底層探索alloc創(chuàng)建對象由字節(jié)對齊到對象內(nèi)存大小探索isa的初始化...
    Dezi閱讀 497評論 0 14
  • isa結(jié)構(gòu)及初始化分析 什么是isa,首先我們先看一下isa的結(jié)構(gòu): 由源碼我們可以看出:isa的本質(zhì)就是一個聯(lián)合...
    e521閱讀 295評論 1 1
  • 在探索alloc&init一篇中表伦,我們對alloc&init有了初步的了解慷丽,其中包括對isa的初始化要糊,本篇我們進一...
    TRACER_閱讀 314評論 0 0
  • 今天感恩節(jié)哎锄俄,感謝一直在我身邊的親朋好友。感恩相遇鱼填!感恩不離不棄毅戈。 中午開了第一次的黨會,身份的轉(zhuǎn)變要...
    迷月閃星情閱讀 10,566評論 0 11
  • 彩排完宦言,天已黑
    劉凱書法閱讀 4,218評論 1 3