isa的初始化&指向分析

alloc的初探 中瞭解瞭如何獲取對象大小,內存對齊的原則,對象的 alloc,可是在調用 calloc 在堆上開闢一個內存空間時返回了一個指針地址,這時候咱們如何將這個指針地址和當前對象關聯呢?bash

下方源碼中 initInstanceIsa 就乾的是這些事情。數據結構

//...

obj = (id)calloc(1, size);
if (!obj) return nil;
obj->initInstanceIsa(cls, hasCxxDtor);

//...
複製代碼

一、什麼是 isa?

簡單點來講,其實就是英文 is a 寫到一塊兒了,說明某一個對象是什麼。好比: object is a NSObject,可是 isa 通過發展以後存儲的東西變得至關的龐大。架構

官方對 isa 的解釋爲: 每一個對象都是經過 isa 實例變量鏈接到運行時系統,從 NSObject 類繼承。Isa 標識對象的類;它指向一個結構的類定義編譯。ide

經過 ISA,能夠在運行時找到一個對象的全部信息,如繼承層次結構中的位置,它的實例變量的大小和結構,以及能夠相應消息的方法所實現的位置。函數

二、isa 存儲結構分析

進入 initIsa(Class cls, bool nonpointer, bool hasCxxDtor) 函數以後發現,isa 是一個 isa_t 的類型。post

union isa_t {
    isa_t() { }
    isa_t(uintptr_t value) : bits(value) { }

    Class cls;
    uintptr_t bits;
#if defined(ISA_BITFIELD)
    struct {
        ISA_BITFIELD;  // defined in isa.h
    };
#endif
};
複製代碼
# if __arm64__
# define ISA_BITFIELD \
      uintptr_t nonpointer        : 1;                                       \
      uintptr_t has_assoc         : 1;                                       \
      uintptr_t has_cxx_dtor      : 1;                                       \
      uintptr_t shiftcls          : 33; /*MACH_VM_MAX_ADDRESS 0x1000000000*/ \
      uintptr_t magic             : 6;                                       \
      uintptr_t weakly_referenced : 1;                                       \
      uintptr_t deallocating      : 1;                                       \
      uintptr_t has_sidetable_rc  : 1;                                       \
      uintptr_t extra_rc          : 19

# elif __x86_64__
# define ISA_BITFIELD \
      uintptr_t nonpointer        : 1;                                         \
      uintptr_t has_assoc         : 1;                                         \
      uintptr_t has_cxx_dtor      : 1;                                         \
      uintptr_t shiftcls          : 44; /*MACH_VM_MAX_ADDRESS 0x7fffffe00000*/ \
      uintptr_t magic             : 6;                                         \
      uintptr_t weakly_referenced : 1;                                         \
      uintptr_t deallocating      : 1;                                         \
      uintptr_t has_sidetable_rc  : 1;                                         \
      uintptr_t extra_rc          : 8
複製代碼

上方代碼能夠看到 isa_t 類型是一個 union 聯合體。ISA_BITFIELD 是位域。ui

一、什麼聯合體?

聯合體又被稱爲共用體,顧名思義就是在 union 定義下的變量共用一塊內存單元,賦值時相互覆蓋,這種結構被稱爲聯合體。spa

二、什麼是位域?

有些信息在存儲時,並不須要佔用一個完整的字節, 而只需佔幾個或一個二進制位。例如在存放一個開關量時,只有 0 和 1 兩種狀態, 用一位二進位便可。debug

爲了節省存儲空間,並使處理簡便,C語言又提供了一種數據結構,稱爲 位域位段指針

所謂 位域 是把一個字節中的二進位劃分爲幾 個不一樣的區域,並說明每一個區域的位數。每一個域有一個域名,容許在程序中按域名進行操做。 這樣就能夠把幾個不一樣的對象用一個字節的二進制位域來表示。

位段成員必須聲明爲 intunsigned intsigned int 類型(short char long)。

三、位域如何定義?

位域列表定義的形式爲: 類型說明符 位域名:位域長度

struct 位域結構名     
{ 
    位域列表 
};  
複製代碼

例如:

struct bits     
{     
    int a:8;     
    int b:2;     
    int c:6;     
};  
複製代碼

說明 bits,共佔兩個字節。其中位域 a8 位,位域 b2 位,位域 c6 位。

四、isa_t 的位域

isa_tbitunsigned long 類型,佔用 8 個字節,也就是 8byte = 64 bit ,因此在 __arm64____x86_64__ 下 isa_t 的位域大小都是 64bit,只是由於架構不一樣因此佔得位數不一樣罷了。

image.png

三、isa 的初始化

inline void 
objc_object::initIsa(Class cls, bool nonpointer, bool hasCxxDtor) 
{ 
    //...

    if (!nonpointer) {
        isa.cls = cls;
    } else {
        isa_t newisa(0);

        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;

        //...

        isa = newisa;
    }
}
複製代碼

在初始化 isa 的時候會將 alloc 出來的對象和類進行綁定 obj -> isa -> shiftcls

當執行 TestClass *object = [TestClass alloc]; 建立一個對象的時候,objectisa 已經建立完畢了,而且將對象的類寫入了 shiftcls

一、對 shiftcls 使用二進制驗證

x/4gx 以16進制打印4個8字節的內存值,那麼第一個必定是 isa

object 內存分佈

  • p/t 以二進制打印 isa 的內存值 0x001d800100001139 ,獲取到64個 bit 的二進制。
  • p/x TestClass.class 打印 TestClass 的類, 獲取指針地址。
  • 根據 (uintptr_t)cls >> 3 還原。

還原推導過程

最後比對結果是同樣的。 $2 = 0b0000000000000000000000000000000100000000000000000001000100111000 $7 = 0b0000000000000000000000000000000100000000000000000001000100111000

二、對 shiftcls 使用16進制蒙版驗證

objc_class() 函數裏就有對類的返回,使用的蒙版取 3~48 位。

inline Class 
objc_object::ISA() 
{
    //...

    //這裏是返回類對象須要用 isa 的指針 & ISA_MASK
    return (Class)(isa.bits & ISA_MASK);
}
複製代碼

驗證以下:

image.png

四、類在內存中存在的個數

既然已經知道了對象的 isa 指向了類,那類究竟是怎樣的呢?對象在實例化的時候,每一個對象都是不一樣的,其指針地址也是不相同的,那麼類呢?可否在內存中存在多份?

下方驗證一下:

Class class1 = [TestClass class];
Class class2 = [TestClass alloc].class;
Class class3 = object_getClass([TestClass alloc]);
Class class4 = [TestClass alloc].class;
        
NSLog(@"\n%p \n%p \n%p \n%p \n",class1,class2,class3,class4);

打印結果過下:
2019-12-22 15:33:01.539549+0800 objc-debug[3353:135796] 
0x100001170 
0x100001170 
0x100001170 
0x100001170
複製代碼

發現類對象的地址都是相同的,說明類對象在內存中有且只能存在一個。

五、isa 的指向分析

在對象返回前,會給 isa 進行賦值,標識對象屬於什麼類,對象指向的類其實也是一個對象,這種對象被稱爲類對象。

既然是對象那一定存在 isa ,那麼類對象的 isa 又指向什麼呢?

x/4gx打印類的內存結構:

類的內存結構

打印出來的內存結構,其實都是內存的值,真正指向當前對象的指針地址是最前面的 0x100001138po 0x100001138 發現這個對象居然也是 TestClass 類型的,這個 TestClass實際上是指向類對象的類,又稱爲元類。

image.png

元類是系統建立的,當程序中有一個類被定義,在編譯器編譯時,會相應的生成一個指向類對象的元類,以便以保存類的一些相關信息,好比:類方法等。

咱們知道了對象的 isa 指向類對象,類對象的 isa 指向元類,那麼元類的 isa 指向什麼呢?

接下來使用上方證實對象的 isa 指向的方法來推導一下元類的 isa 的指向。

推導過程以下:

image.png

從上圖能看到 TestClass 的類對象的 isa 指向 TestClass的元類。

image.png

繼續查看 TestClass 元類的 isa 指向的是 NSObject ,那這個 NSObject 究竟是元類仍是類對象呢?

由於內存中類對象只有一個,類對象指針地址惟一,因此若是 NSObject.class 的指針地址和 TestClass 元類的 isa 指向地址相同則說明是 isa 指向 NSObject 類,不然不是。

image.png

繼續查看 NSObject 類的內存地址, p/x NSObject.class = 0x0000000100b38140 NSObject,很遺憾並非 NSObject 的類對象。

查看 NSObject 類對象的元類,發現了 NSObject 元類的指針地址和 TestClass 元類 isa 指向的地址是相同的,這就說明了 TestClass 元類的 isa 指向的是 NSObject 元類。

繼續查看 NSObject 元類 isa 的指向,發現指向的是本身。

image.png

因此就有了蘋果老大給的下方的圖。

isa 的指向圖

六、isa 的指向補充

在上方的指向中,subclassmeta class 直接指向了 NSObjectmeta class ,是由於元類保存的就是類的信息,能夠說已經到頭了, 若是再次繼承 Super meta class 已經沒有什麼意義,而且還會讓繼承關係更加複雜,使得繼承樹更加難以維護,因此蘋果將 subclassmeta class 直接指向了 NSObjectmeta class ,再將 NSObjectmeta class 指向了本身造成了一個閉環。

以上就是 isa 的初始化過程和指向分析。

相關文章
相關標籤/搜索