iOS底層原理探索 — isa原理與對象的本質


iOS底層原理探索篇 主要是圍繞底層進行源碼分析-LLDB調試-源碼斷點-彙編調試,讓本身之後回顧複習的😀😀html

目錄以下:c++

iOS底層原理探索 — 開篇程序員

iOS底層原理探索 — alloc&init探索xcode

iOS底層原理探索 — 內存對齊&malloc源碼分析bash

iOS底層原理探索 一 isa原理與對象的本質數據結構


isa底層原理

聯合體位域

咱們在iOS底層原理探索 — 內存對齊&malloc源碼分析一文中講解到NObject的底層實現其實就是一個包含一個isa指針的結構體:架構

struct NSObject_IMPL {
    Class isa;
};
複製代碼

arm64架構以前,isa僅是一個指針,保存着類對象(Class)或元類對象(Meta-Class)的內存地址,在arm64架構以後,蘋果對isa進行了優化,變成了一個isa_t類型的聯合體(union)結構,同時使用位域來存儲更多的信息:app

也就是說,咱們以前熟知的OC對象的 isa指針並非直接指向類對象或者元類對象的內存地址,而是須要 & ISA_MASK經過位運算才能獲取類對象或者元類對象的地址.

如今你們可能心存疑問,什麼是聯合體?什麼是位域?位運算又是什麼?不要着急,接下來一一爲你們解答.

1.位域

位域是指信息在存儲時,並不須要佔用一個完整的字節, 而只需佔一個或幾個二進制位。例如生活中的電燈開關,它只有「開」、「關」兩種狀態,那咱們就能夠用10來分別表明這兩種狀態,這樣咱們就僅僅用了一個二進制位就保存了開關的狀態。這樣一來不只節省存儲空間,還使處理更加簡便。iphone

2.位運算符

在計算機語言中,除了加、減、乘、除等這樣的算術運算符以外還有不少運算符,這裏只爲你們簡單講解一下位運算符。 位運算符用來對二進制位進行操做,固然,操做數只能爲整型和字符型數據C語言中六種位運算符:&按位與、|按位或、^按位異或、~非、<<左移和>>右移。 咱們依舊引用上面的電燈開關論,只不過如今咱們有兩個開關:開關A和開關B,1表明開,0表明關。ide

1)按位與&

有0出0,全1出1.

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

咱們能夠理解爲在按位與運算中,兩個開關是串聯的,若是咱們想要燈亮,須要兩個開關都打開燈纔會亮,因此是1 & 1 = 1. 若是任意一個開關沒有打開,燈都不會亮,因此其餘運算都是0.

2)按位或 |

有1出1,全0出0.

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

在按位或運算中,咱們能夠理解爲兩個開關是並聯的,即一個開關開,燈就會亮.只有當兩個開關都是關的.燈纔不會亮.

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)左移 <<

左移運算就是把<<左邊的運算數的各二進位所有左移若干位,移動的位數即<<右邊的數的數值,高位丟棄,低位補0。 左移n位就是乘以2的n次方。例如:a<<4是指把a的各二進位向左移動4位。如a=00000011(十進制3),左移4位後爲00110000(十進制48)。

6)右移 >>

右移運算就是把>>左邊的運算數的各二進位所有右移若干位,>>右邊的數指定移動的位數。例如:設 a=15,a>>2 表示把00001111右移爲00000011(十進制3)

位運算符的運用

1)取值

能夠利用按位與 &運算取出指定位的值,具體操做是想取出哪一位的值就將那一位置爲1,其它位都爲0,而後同原數據進行按位與計算,便可取出特定的位.

例: 0000 0011取出倒數第三位的值

// 想取出倒數第三位的值,就將倒數第三位的值置爲1,其它位爲0,跟原數據按位與運算
  0000 0011
& 0000 0100
------------
  0000 0000  // 得出按位與運算後的結果,便可拿到原數據中倒數第三位的值爲0
複製代碼

上面的例子中,咱們從0000 0011中取值,則有0000 0011被稱之爲源碼.進行按位與操做設定的0000 0100稱之爲掩碼.

2)設值

能夠經過按位或 |運算符將某一位的值設爲1或0.具體操做是: 想將某一位的值置爲1的話,那麼就將掩碼中對應位的值設爲1,掩碼其它位爲0,將源碼與掩碼進行按位或操做便可.

例: 將0000 0011倒數第三位的值改成1

// 改變倒數第三位的值,就將掩碼倒數第三位的值置爲1,其它位爲0,跟源碼按位或運算
  0000 0011
| 0000 0100
------------
  0000 0111  // 便可將源碼中倒數第三位的值改成1
複製代碼

想將某一位的值置爲0的話,那麼就將掩碼中對應位的值設爲0,掩碼其它位爲1,將源碼與掩碼進行按位或操做便可.

例: 將0000 0011倒數第二位的值改成0

// 改變倒數第二位的值,就將掩碼倒數第二位的值置爲0,其它位爲1,跟源碼按位或運算
  0000 0011
| 1111 1101
------------
  0000 0001  // 便可將源碼中倒數第二位的值改成0
複製代碼

到這裏相信你們對位運算符有了必定的瞭解,下面咱們經過OC代碼的一個例子,來將位運算符運用到實際代碼開發中. 咱們聲明一個TCJCar類,類中有四個BOOL類型的屬性,分別爲frontbackleftright,經過這四個屬性來判斷這輛小車的行駛方向.

而後咱們來查看一下這個 TCJCar類對象所佔據的內存大小:
咱們看到,一個 TCJCar類的對象佔據16個字節.其中包括一個 isa指針和四個 BOOL類型的屬性,8+1+1+1+1=12,根據 內存對齊原則,因此一個 TCJCar類的對象佔16個字節.

咱們知道,BOOL值只有兩種狀況:01,佔據一個字節的內存空間.而一個字節的內存空間中又有8個二進制位,而且二進制一樣只有01,那麼咱們徹底可使用1個二進制位來表示一個BOOL值。也就是說咱們上面聲明的四個BOOL值最終只使用4個二進制位就能夠,這樣就節省了內存空間。那咱們如何實現呢? 想要實現四個BOOL值存放在一個字節中,咱們能夠經過char類型的成員變量來實現.char類型佔一個字節內存空間,也就是8個二進制位.可使用其中最後四個二進制位來存儲4個BOOL值. 固然咱們不能把char類型寫成屬性,由於一旦寫成屬性,系統會自動幫咱們添加成員變量,自動實現setget方法.

@interface TCJCar(){
    char _frontBackLeftRight;
}
複製代碼

若是咱們賦值_frontBackLeftRight1,即0b 0000 0001,只使用8個二進制位中的最後4個分別用0或者1來表明frontbackleftright的值.那麼此時frontbackleftright的狀態爲:

結合咱們上文講的6種位運算符以及使用場景,咱們能夠分別聲明 frontbackleftright的掩碼,來方便咱們進行下一步的位運算取值和賦值:

#define TCJDirectionFrontMask 0b00001000 //此二進制數對應十進制數爲 8
#define TCJDirectionBackMask 0b00000100 //此二進制數對應十進制數爲 4
#define TCJDirectionLeftMask 0b00000010 //此二進制數對應十進制數爲 2
#define TCJDirectionRightMask 0b00000001 //此二進制數對應十進制數爲 1
複製代碼

經過對位運算符的左移<<和右移>>的瞭解,咱們能夠將上面的代碼優化成:

#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代碼執行後,返回的確定是一個整型數,如當frontYES時,說明二進制數爲0b 0000 1000,對應的十進制數爲8,那麼進行一次邏輯非運算後,!(8)的值爲0,對0再進行一次邏輯非運算!(0),結果就成了1,那麼正好跟frontYES對應.因此此處進行兩次邏輯非運算,!!. 固然,還要實現初始化方法:

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

經過測試驗證,咱們完成了取值和賦值:

使用結構體位域優化代碼

咱們在上文講到了位域的機率,那麼咱們就可使用結構體位域來優化一下咱們的代碼.這樣就不用再額外聲明上面代碼中的掩碼部分了.位域聲明格式是位域名: 位域長度. 在使用位域的過程當中須要注意如下幾點:

  1. 若是一個字節所剩空間不夠存放另外一位域時,應從下一單元起存放該位域.
  2. 位域的長度不能大於數據類型自己的長度,好比int類型就不能超過32位二進位.
  3. 位域能夠無位域名,這時它只用來做填充或調整位置。無名的位域是不能使用的.

使用位域優化後的代碼:

來測試看一下是否正確,此次咱們將 front設爲 YESback設爲 NOleft設爲 NOright設爲 YES:
依舊能完成賦值和取值. 可是代碼這樣優化後咱們去掉了掩碼和初始化的代碼,可讀性不好,咱們繼續使用聯合體進行優化:

使用聯合體優化代碼

咱們可使用比較高效的位運算來進行賦值和取值,使用union聯合體來對數據進行存儲。這樣不只能夠增長讀取效率,還能夠加強代碼可讀性.

#import "TCJCar.h"

//#define TCJDirectionFrontMask 0b00001000 //此二進制數對應十進制數爲 8
//#define TCJDirectionBackMask 0b00000100 //此二進制數對應十進制數爲 4
//#define TCJDirectionLeftMask 0b00000010 //此二進制數對應十進制數爲 2
//#define TCJDirectionRightMask 0b00000001 //此二進制數對應十進制數爲 1

#define TCJDirectionFrontMask (1 << 3)
#define TCJDirectionBackMask (1 << 2)
#define TCJDirectionLeftMask (1 << 1)
#define TCJDirectionRightMask (1 << 0)

@interface TCJCar()
{
    union{
        char bits;
        // 結構體僅僅是爲了加強代碼可讀性
        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設爲YESback設爲NOleft設爲NOright設爲YES:

經過結果咱們看一看到依舊能完成賦值和取值. 這其中 _frontBackLeftRight聯合體只佔用一個字節,由於結構體中 frontbackleftright都只佔一位二進制空間,因此結構體只佔一個字節,而 char類型的 bits也只佔一個字節.他們都在聯合體中,所以共用一個字節的內存便可. 並且咱們在 setget方法中的賦值和取值經過使用掩碼進行位運算來增長效率,總體邏輯也就很清晰了.可是若是咱們在平常開發中這樣寫代碼的話,極可能會被同事打死.雖然代碼已經很清晰了,可是總體閱讀起來仍是很吃力的.咱們在這裏學習了位運算以及聯合體這些知識,更多的是爲了方便咱們閱讀OC底層的代碼.下面咱們來回到本文主題,查看一下 isa_t聯合體的源碼.

isa_t聯合體

經過源碼咱們發現 isa它是一個聯合體,聯合體是一個結構佔8個字節,它的特性就是共用內存,或者說是 互斥,好比說若是 cls賦值了就不在對 bits進行賦值.在 isa_t聯合體內使用宏 ISA_BITFIELD定義了位域,咱們進入位域內查看源碼:
咱們看到,在內部分別定義了 arm64位架構和 x86_64架構的掩碼和位域.咱們只分析 arm64爲架構下的部份內容(真機環境下). 能夠清楚的看到 ISA_BITFIELD位域的內容以及掩碼 ISA_MASK的值: 0x0000000ffffffff8ULL.咱們重點看一下 uintptr_t shiftcls : 33;,在 shiftcls中存儲着類對象和元類對象的內存地址信息,咱們上文講到,對象的 isa指針須要同 ISA_MASK通過一次按位與運算才能得出真正的類對象地址.那麼咱們將 ISA_MASK的值 0x0000000ffffffff8ULL轉化爲二進制數分析一下:
從圖中能夠看到 ISA_MASK的值轉化爲二進制中有33位都爲1,上文講到按位與運算是能夠取出這33位中的值.那麼就說明同 ISA_MASK進行按位與運算就能夠取出類對象和元類對象的內存地址信息. 咱們繼續分析一下結構體位域中其餘的內容表明的含義:
至此咱們已經對 isa指針有了新的認識, arm64架構以後, isa指針不僅僅只存儲了類對象和元類對象的內存地址,而是使用聯合體的方式存儲了更多信息,其中 shiftcls存儲了類對象和元類對象的內存地址,須要同 ISA_MASK進行 按位與 &運算才能夠取出其內存地址值.

isa關聯對象與類

isa是OC對象的第一個屬性,由於這一屬性是來自於繼承,要早於對象的成員變量,屬性列表,方法列表以及所遵循的協議列表. 在iOS底層原理探索 — alloc&init探索這篇文章中,當時咱們在探索對象的初始化的時候還有一個很是重要的點沒有細說就是:通過calloc申請內存的時候,這個指針是怎麼和TCJPerson這個類所關聯的呢? 下面咱們就能夠直接定位到:obj->initInstanceIsa(cls, hasCxxDtor)

  • 經過前面兩篇文章的學習,咱們知道了obj裏面只有一個指針
  • 下面的代碼就能夠分析對象與類直接的聯繫
  • initIsa(cls, true, hasCxxDtor)
inline void 
objc_object::initIsa(Class cls, bool nonpointer, bool hasCxxDtor) 
{ 
    assert(!isTaggedPointer()); 
    
    if (!nonpointer) {
        isa.cls = 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; } } 複製代碼
  • 上面第一層判斷是isTaggedPointer的斷言,這會在後續文章中重點分析

  • 接下來是nonpointer的判斷,由於nonpointer優化,它是和普通結構不同的!經過上文咱們知道內存優化的isa_t結構:它採用的是聯合體和位域的搭配.(目前咱們的類都是nonpointer了)

    1. 若是是非nonpointer,表明普通的指針,存儲着ClassMeta-Class對象的內存地址信息
    2. 若是是nonpointer,則會進行一系列的初始化操做.其中的newisa.shiftcls = (uintptr_t)cls >> 3;中的shiftcls存儲着ClassMeta-Class對象的內存地址信息,咱們來驗證一下:

    來咱們來對上面LLDB相關的指令進行一波解析: 3. x/4gx obj:表明打印obj的4段內存信息 4. p/t:表明打印二進制信息(還有p/op/dp/x分別表明八進制、十進制和十六進制打印) 5. p/t (uintptr_t)obj.class將類信息進行二進制打印獲得:$3 6. 對第一個屬性isa進行二進制打印p/t 0x001d8001000013f1獲得:$1 7. 由於此時咱們是在x86_64環境下進行打印的,經過上文咱們知道在x86_64環境下isaISA_BITFIELD位域結構中:前3位是nonpointerhas_assochas_cxx_dtor,中間44位是shiftcls,後面17位是剩餘的內容,同時由於iOS是小端模式,那麼咱們就須要去掉右邊的3位和左邊的17位,因此就會採用$1>>3<<3而後$4<<17>>17的操做了.

    經過這個測試,咱們就知道了isa實現了對象與類之間的關聯. 在上文中咱們提獲得OC對象的isa指針並非直接指向類對象或者元類對象的內存地址,而是須要& ISA_MASK經過位運算才能獲取類對象或者元類對象的地址.來咱們也來驗證一波:

來咱們來對上面`LLDB`相關的指令進行一波解析:
1. 打印對象的內存信息:`x/4gx obj`
2. 打印類的信息:`p/x obj.class`獲得`$7`
3. 經過對象的`isa & ISA_MASK`操做:`p/x 0x001d8001000013f1 & 0x00007ffffffffff8ULL`獲得`$8`
4. 對比`$7`和`$8`他們是一模模同樣樣的

在此也驗證了`isa`實現了對象與類之間的關聯.
複製代碼
  • 一個8字節指針64位下 其實能夠存儲不少內容,咱們能夠優化內存,在不一樣的位上,放不一樣的東西! 在這咱們還須要補充一下StructUnion的區別:
    1. structunion都是由多個不一樣的數據類型成員組成,但在任何同一時刻,union 中只存放了一個被選中的成員, 而struct的全部成員都存在。在struct中,各成員都佔有本身的內存空間,它們是同時存在的。一個struct變量的總長度等於全部成員長度之和。在Union中,全部成員不能同時佔用它的內存空間,它們不能同時存在。Union變量的長度等於最長的成員的長度
    2. 對於union的不一樣成員賦值, 將會對其它成員重寫, 原來成員的值就不存在了, 而對於struct的不一樣成員賦值是互不影響的.

isa的指向走位分析

咱們都知道對象能夠建立多個,那麼類是否也能夠建立多個呢? 答案是一個.怎麼驗證它呢? 來咱們看下面代碼及打印結果:

經過運行結果證實了 類在內存中只會存在一份.

經過上面的打印,咱們發現類的內存結構裏面的第一個位置是: 0x1000013f0-TCJPerson他指向元類,是由系統建立的. 咱們來看一下對象-類-元類他們之間的關係:

  1. 對象是由程序員根據類進行實例化來的
  2. 類代碼寫出來的,內存只有一份,不是咱們建立的,是由系統建立的
  3. 元類是根據系統在編譯的時候發現有這麼一個類,也是由系統建立的,咱們是實例化不出來的.在編譯階段就會產生的.

到此咱們知道對象的isa指向類,類的isa指向元類,那麼元類的isa指向哪呢?

接下來,咱們一塊兒來看一下 isa的走位:

至此咱們獲得的結論是:

咱們在來看下面代碼打印:

經過運行結果咱們知道:元類-根元類-根根元類是一模模同樣樣的. 到此咱們用蘋果官方提供的一張圖看瞅一瞅:
咱們對上圖進行總結一波:圖中實線是 super_class指針,它表明着繼承鏈的關係.虛線是isa指針. 1. Root class (class)其實就是 NSObjectNSObject是沒有超類的,因此 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對象

對象的本質

OC中,類對象(class對象)和元類對象(meta-class對象)的本質結構都是struct objc_class指針,即在內存中就是結構體

Class clas = [NSObject class];
複製代碼

來到class底層源碼,咱們能夠看到:

typedef struct objc_class *Class;
複製代碼

class對象實際上是一個objc_class結構體的指針.所以咱們能夠說類對象或元類對象在內存中其實就是objc_class結構體. 來咱們來看一下源碼:

咱們發現 objc_class結構體繼承 objc_object而且結構體內有一些函數,由於這是 c++結構體,在 C的基礎之上作了擴展.所以結構體中能夠包含函數.注意觀察註釋掉的 Class ISA這一行代碼. 咱們來到 objc_object內,繼續截取部分代碼:

咱們發現 objc_object中有一個 isa指針,那麼 objc_class繼承 objc_object,也就一樣擁有一個 isa指針。繼承來了 isa指針,因此上文咱們提到了 Class ISA也就被註釋掉了. 再來看第二行代碼 Class superclass:咱們來打印一下來看結果:
也就是說 objc_class內存中第一個位置是 isa,第二個位置是 superclass,其餘位置咱們後續文章在分析.到此,咱們要怎樣繼續進行分析呢? 咱們都知道咱們平時編寫的 Objective-C代碼,其底層的實現都是 C/C++代碼.

因此 Objective-C的面向對象都是基於 C/C++的數據結構實現的.那麼 Objective-C的對象、類主要是基於 C/C++的什麼數據結構實現的呢?--結構體. 所以,咱們能夠經過將建立好的 OC文件,轉化爲 C++文件來看一下 OC對象的底層結構.

OC代碼轉換爲C/C++代碼

iOS底層原理探索 — 內存對齊&malloc源碼分析一文中,咱們提到過若是將OC代碼轉化爲C/C++了,這裏咱們在複習一下: 經過命令行將OC的main.m文件轉換成C++文件,生成main.cpp.

clang -rewrite-objc main.m -o main.cpp 
/***rewrite表明 重寫
   *-o表明 輸出
   *cpp表明 c++(c plus plus)
**/
複製代碼

須要注意這種方式沒有指定運行平臺和架構模式,咱們能夠經過命令行設置參數,來指定運行平臺和架構模式

xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main.cpp 
/***xcrun表明 xcode
   * iphoneos表明 運行在iPhone上
   *-arch表明 架構模式,arm64參數表明64位的架構模式
**/
複製代碼

生成的main.cpp 文件就是main.m轉換c++後的文件,直接拖拽到工程中,就能夠查看底層實現了. 咱們的OC文件爲main.m:

將其轉化爲 C++文件 main.cpp後,咱們在 main.cpp 文件中搜索 TCJPerson,能夠找到 TCJPerson_IMPLIMPLimplementation 的縮寫,表明實現).

經過上文咱們能夠看到:

  • NSObject的底層實現就是一個結構體.
  • Class其實就是一個指針,指向了objc_class類型的結構體.
  • TCJPerson_IMPL結構體內有三個成員變量:
    1. isa繼承自父類NSObject
    2. helloName
    3. _name
  • 對於屬性name:底層編譯會生成相應的settergetter方法,且幫咱們轉化爲_name
  • 對於成員變量helloName:底層編譯不會生成相應的settergetter方法,且沒有轉化爲_helloName

接下來咱們來看看main.cpp文件中的method_list_t:

其中 (struct objc_selector *)"name"對應 SEL, "@16@0:8"對應的就是方法簽名, (void *)_I_TCJPerson_name對應的方法實現(即 IMP). 其中的 "@16@0:8"方法簽名中對應一個返回值和兩個參數: 1. @ 返回值類型: id 返回16,表明總共的量 2. @ 參數一類型: id 0-7 3. : 參數二類型: sel 8-15

咱們來打印一下@:具體表明啥:

查看打印結果能夠看到對應的typeEncode. 固然咱們也能夠去蘋果官方文檔中查看 TypeEncode
相關文章
相關標籤/搜索