類的底層原理

這篇文件來探索類的底層c++


類探索

一個自定義類的類名是咱們決定的,因此咱們想進行類探索,就得看看咱們定義的類,在底層是如何被定義的。緩存

一、將OC代碼轉換成c++代碼

先準備一個自定義類DZPerson,在類中定義一個成員變量(_nick),一個屬性(nameStr)。實現了一個方法(-saySomethine)和一個類方法(+sayHello)markdown

@interface DZPerson : NSObject
{
    NSString *_nick;
}
@property (copy, nonatomic) NSString *nameStr;
@end

@implementation DZPerson
- (void)saySomething {
    NSLog(@"%s", __func__);
}

+ (void)sayHello {
    NSLog(@"%s", __func__);
}
@end
複製代碼

在main中進行初始化。函數

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        NSLog(@"Hello, World!");
        
        DZPerson *person = [DZPerson alloc];
    }
    return 0;
}
複製代碼

使用終端,進入到main.m文件所在的路徑,使用clang命令,編譯main.m文件ui

clang -rewrite-objc main.m -o main.cpp
複製代碼

輸出main.cpp,打開這個文件,在裏面進行搜索DZPerson,做爲線索的出發點:atom

typedef struct objc_object DZPerson;

⏬⏬⏬

struct DZPerson_IMPL {
	struct NSObject_IMPL NSObject_IVARS;
	NSString *_nick;
};

⏬⏬⏬

struct NSObject_IMPL {
	Class isa;
};

⏬⏬⏬

typedef struct objc_class *Class;
複製代碼
  • 自定義類的類名是struct objc_object結構體的別名。
  • 找到了一個struct DZPerson_IMPL結構體,包含兩個成員屬性:
    • 另外一個結構體struct NSObject_IMPL,經過名字中的字段,瞭解到這個應該是父類NSObject
    • 咱們定義的成員_nick
  • 查看NSObject_IMPL結構體,裏面有一個成員isa,是Class類型。
  • 最後在文件中找到Classstruct objc_class *結構體指針類型。

經過以上邏輯,咱們的目標轉移到struct objc_objectstruct objc_class這兩個結構體上了。spa

二、struct objc_object & struct objc_class

看看這兩個結構體在源碼中的定義,代碼比較長,我只截獲了須要咱們研究的部分:3d

struct objc_object {
private:
    isa_t isa;

public:
    //這裏定義了一些函數,先省略
    ...
}

struct objc_class : objc_object {
    // Class ISA;
    Class superclass;
    cache_t cache;             // formerly cache pointer and vtable
    class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags

    class_rw_t *data() const {
        return bits.data();
    }
    void setData(class_rw_t *newData) {
        bits.setData(newData);
    }
    //省略後續代碼
    ...
}
複製代碼
  • objc_object中只有一個成員:isa
  • objc_class繼承自objc_object
    • 第一個成員就是父類objc_object中的isa(蘋果的註釋仍是挺人性的)
    • superclass,字面意思很清楚,super類
    • cache,緩存相關信息
    • bits,裏面存放類的相關數據(成員、屬性、方法、協議等相關數據信息)
    • 後面還有不少函數,能夠自行去查看一下。(此處記錄bits的getter和setter函數,由於後面要用到)

經過以上的源碼查看和相關的簡要分析,咱們能夠確認自定義類在底層會被轉化成objc_class類型的結構體。實例對象在底層就是objc_object類型。經過下面的兩行源碼,能夠直接證實:指針

typedef struct objc_class *Class;
typedef struct objc_object *id;
複製代碼

此時看文章的你可能會有個疑惑:objc_class繼承自objc_object,==那麼類也是對象?==調試

三、萬物皆對象

OC中類也是對象,如何證實呢?咱們知道,實例對象中的isa指向的是類,類中也有isa,咱們經過lldb來看看類的isa: 咱們以文章開頭的代碼爲例:

DZPerson *person = [DZPerson alloc];
複製代碼

lldb執行截圖:

  1. 打印person的內存狀況x/4gx perosn:首地址中的第一個值是person對象的isa。
  2. 從person的isa中讀取類信息地址(經過一個系統定義好的宏,進行獲取)。
  3. po獲得的值,也就是圖片中第一個紅框,打印是DZPerosn,地址是:0x0000000100002378
  4. 再用與查看person內存相同的方式,產看獲得的0x0000000100002378的內存狀況,也就是查看DZPerson類的內存
  5. 獲取DZPersonisa,並po一下,打印的結果頁是DZPerson,可是地址和前面打印的地址不一樣。這個第二個DZPerosn元類
  6. 繼續查看元類isa,打印出來的是NSObject,可是它不是NSObject類,而是NSObject的元類,也就是根元類。而根元類的isa指向仍是根元類

簡單來講,==實例對象isa指向類,類isa指向元類,元類isa指向根元類,根元類的isa指向根元類。==經過一張圖,能夠更好的理解isa的走位指向: 圖中虛線表明isa走位,實現表明superclass走位。

==所以:類是元類的實例對象,而元類是根元類的示例對象。因此說,類也是對象,萬物接對象==。

四、lldb調試打印類中的信息

接下來咱們用lldb來看看類中存儲的屬性、方法,前文說到這些信息都存儲在class_data_bits_t bits中,使用指針偏移的方式就能夠獲取到bits。 此處放一張objc_class的源碼截圖: 拿到class在內存中的首地址,分別加上屬性isa、superclass和cache佔用的內存大小,就能夠獲得bits的地址。

isasuperclass都是指針類型,因此分別佔用8字節。

那麼如今須要計算cache佔用多少字節,看看cache_t的源碼:

struct cache_t {
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_OUTLINED
    explicit_atomic<struct bucket_t *> _buckets;
    explicit_atomic<mask_t> _mask;
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
    explicit_atomic<uintptr_t> _maskAndBuckets;
    mask_t _mask_unused;
    
    //省略靜態成員變量
    ...
#else
#error Unknown cache mask storage type.
#endif
    
#if __LP64__
    uint16_t _flags;
#endif
    uint16_t _occupied;

public:
    //省略後面的函數
    ...
}
複製代碼
  • 首先是編譯條件的公式#if和#elif,裏面都是有兩個成員變量,分別是指針類型和mask_t類型。指針佔用8個字節,mask_tuint32_t的別名,也就是佔中4個字節。
  • _flags_occupied分別佔用2個字節,由於是uint16_t類型。
  • 其他的就是靜態成員和函數,在結構體中的靜態成員和函數是不佔用結構體內存空間的。所以得出cache_t cache成員在結構體中佔用16字節

==那麼想要獲得class_data_bits_t bits的偏移量,就是isa+superclass+cache=8+8+16=32,轉換成十六進制就是20==

條件知足,咱們開始打印類中的信息:

  • 打印類地址:直接獲取DZPerson的首地址

首地址的第一個元素就是類的isa,因此咱們直接以十六進制打印便可。

  • 獲取bits,拿到首地址進行+20偏移,20是前面計算出來的(0x0000000100002398 = 0x0000000100002378 + 20)。而且進行強制類型轉換

  • 調用class_rw_t *data() const函數(前文中,objc_class源碼中的函數),獲得地址,打印地址中的值:

  • 接下來用class_rw_t中的methods()函數,能夠獲取到方法列表:

  • list函數獲取列表的首地址,打印地址的值後,用get查看到類中的全部方法。經過下圖能看到類中定義的saySomething方法、屬性nameStr的setter和getter方法,還有一個系統生成的.cxx_destruct方法:

  • 類中還定義了一個類方法+ (void)sayHello,在這裏是看不到的。由於類方法存在元類中。

經過上面的方式咱們就能夠產看到類中方法,也能夠證實,方法是存在類中。一樣,經過class_rw_t中的properties()能夠獲取到屬性、protocols()獲取協議。

成員變量存在ro中,經過函數ro()獲取:


總結

  1. 經過查看c++代碼,咱們找到了類的底層定義objc_class,而且知道類中也存在isa指針。
  2. 經過isa走位圖,知道類也是對象,並推到出‘萬物皆對象’。
  3. 經過lldb一步步看到類中的屬性、方法、協議以及成員在類中如何存儲的。

但願本文能對你有所幫組,若是有些的很差的地方,還請指出,謝謝!

相關文章
相關標籤/搜索