iOS底層探索之isa

一、前置知識

1.1 C 共用體 || 聯合體

共用體是一種特殊的數據類型,允許您在相同的內存位置存儲不同的數據類型。您可以定義一個帶有多成員的共用體,但是任何時候只能有一個成員帶有值。共用體提供了一種使用相同的內存位置的有效方式。

定義
爲了定義結構體,您必須使用 union 語句,方式與定義結構類似。union 語句定義了一個新的數據類型,帶有多個成員。union 語句的格式如下:

 union [union tag]
    {
       member definition;
       member definition;
       ...
       member definition;
    } [one or more union variables];
// [one or more union variables] 是可選的
--
eg:
  union uData {
    int age;        // 4字節
    long height;    // 8字節
    char sex;       // 1字節
  };

現在,Data 類型的變量可以存儲一個整數、一個浮點數,或者一個字符串。這意味着一個變量(相同的內存位置)可以存儲多個多種類型的數據。您可以根據需要在一個共用體內使用任何內置的或者用戶自定義的數據類型。

共用體佔用的內存應足夠存儲共用體中最大的成員。例如,在上面的實例中,Data 將佔用 8 個字節的內存空間,因爲在各個成員中,height 所佔用的空間是最大的。

訪問共用體成員

        union uData u = {};
        u.height = 100;
        NSLog(@"u: %lu", sizeof(u));
        NSLog(@"age:%d, height: %ld, age: %hhd", u.age, u.height, u.sex);
        u.age = 25;
        NSLog(@"age:%d, height: %ld, age: %hhd", u.age, u.height, u.sex);
        u.sex = 'f';
        NSLog(@"age:%d, height: %ld, age: %hhd", u.age, u.height, u.sex); 

可驗證最大空間爲公用體中,最大的成員所佔空間。也可知成員變量是訪問的同一片內存空間。

1.2 C 位域

如果程序的結構中包含多個開關量,只有 TRUE/FALSE 變量,如下:

    struct
    {
      unsigned int widthValidated;
      unsigned int heightValidated;
    } status;

這種結構需要 8 字節的內存空間,但在實際上,在每個變量中,我們只存儲 0 或 1。在這種情況下,C 語言提供了一中更好的利用內存空間的方式。如果您在結構內使用這樣的變量,您可以定義變量的寬度來告訴編譯器,您將只使用這些字節。例如,上面的結構可以重寫成:

   struct
    {
      unsigned int widthValidated : 1;
      unsigned int heightValidated : 1;
    } status;

現在,上面的結構中,status 變量將佔用 4 個字節的內存空間,但是隻有 2 位被用來存儲值。如果您用了 32 個變量,每一個變量寬度爲 1 位,那麼 status 結構將使用 4 個字節。

位域聲明

    struct
    {
      type [member_name] : width ;
    };

下面是有關位域中變量元素的描述:

元素 描述
type 整數類型,決定了如何解釋位域的值。類型可以是整型、有符號整型、無符號整型。
member_name 位域的名稱。
width 位域中位的數量。寬度必須小於或等於指定類型的位寬度。

帶有預定義寬度的變量被稱爲位域。位域可以存儲多於 1 位的數,例如,需要一個變量來存儲從 0 到 7 的值,您可以定義一個寬度爲 3 位的位域,如下:

 struct
    {
      unsigned int age : 3;
    } Age;

上面的結構定義指示 C 編譯器,age 變量將只使用 3 位來存儲這個值,如果您試圖使用超過 3 位,則無法完成。


二、isa探索

探索alloc 的時候,最後有個方法 initInstanceIsa 初始化 isa 並關聯類。在此繼續深入

inline void 
objc_object::initInstanceIsa(Class cls, bool hasCxxDtor)
{
    ASSERT(!cls->instancesRequireRawIsa());
    ASSERT(hasCxxDtor == cls->hasCxxDtor());

    initIsa(cls, true, hasCxxDtor);
}

inline void 
objc_object::initIsa(Class cls, bool nonpointer, bool hasCxxDtor) 
{ 
    ASSERT(!isTaggedPointer()); 
    
    if (!nonpointer) {
        isa = isa_t((uintptr_t)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;
        newisa.has_cxx_dtor = hasCxxDtor;
        newisa.indexcls = (uintptr_t)cls->classArrayIndex();
#else
        newisa.bits = ISA_MAGIC_VALUE;
        newisa.has_cxx_dtor = hasCxxDtor;
        newisa.shiftcls = (uintptr_t)cls >> 3;
#endif
        isa = newisa;
    }
}

查看下 SUPPORT_INDEXED_ISA 的定義

#if __ARM_ARCH_7K__ >= 2  ||  (__arm64__ && !__LP64__)
#   define SUPPORT_INDEXED_ISA 1
#else
#   define SUPPORT_INDEXED_ISA 0
#endif

__ARM_ARCH_7K__: 是代表手錶的宏,不滿足
__arm64__: 表示64位ARM架構 滿足
__LP64__: 表示指針長度爲64位 滿足
!__LP64__: 就不滿足了

所以 SUPPORT_INDEXED_ISA 爲 0

由此分析上面的主要代碼爲

        isa_t newisa(0);  // 聲明並初始化一個isa_t
        newisa.bits = ISA_MAGIC_VALUE;
        newisa.has_cxx_dtor = hasCxxDtor;
        newisa.shiftcls = (uintptr_t)cls >> 3; // 重點,賦值了cls
        isa = newisa; // 賦值isa
2.1 isa_t結構
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
};

通過源碼可知,isa_t 是一個聯合體,裏面有3個成員 clsbitsstruct,它們佔用同一片內存區域。

然後找到 ISA_BITFIELD 的定義

# if __arm64__
#   define ISA_MASK        0x0000000ffffffff8ULL
#   define ISA_MAGIC_MASK  0x000003f000000001ULL
#   define ISA_MAGIC_VALUE 0x000001a000000001ULL
#   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
#   define RC_ONE   (1ULL<<45)
#   define RC_HALF  (1ULL<<18)

# elif __x86_64__
#   define ISA_MASK        0x00007ffffffffff8ULL
#   define ISA_MAGIC_MASK  0x001f800000000001ULL
#   define ISA_MAGIC_VALUE 0x001d800000000001ULL
#   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
#   define RC_ONE   (1ULL<<56)
#   define RC_HALF  (1ULL<<7)

運行環境是在mac上運行的,不是跑的真機,可以以 __x86_64__ 爲例探索一下。

綜上所述,所以isa_t的聯合體,其中 shiftcls

  • arm64 下佔33位(從第3位到35位)
  • __x86_64__ 下佔44位(從第3位到46位)
成員 介紹
nonpointer 表示是否對 isa 指針開啓指針優化——0:純 isa 指針;1:不止是類對象地址,isa 中包含了類信息、對象的引用計數等
has_assoc 關聯對象標誌位,0沒有,1存在
has_cxx_dtor 該對象是否有 C++ 或者 Objc 的析構器,如果有析構函數,則需要做析構邏輯, 如果沒有,則可以更快的釋放對象
shiftcls 存儲類指針的值,在開啓指針優化的情況下,在 arm64 架構中有 33 位用來存儲類指針
magic 用於調試器判斷當前對象是真的對象還是沒有初始化的空間
weakly_referenced 對象是否被指向或者曾經指向一個 ARC 的弱變量, 沒有弱引用的對象可以更快釋放
deallocating 標誌對象是否正在釋放內存
has_sidetable_rc 當對象引用技術大於 10 時,則需要借用該變量存儲進位
extra_rc 當表示該對象的引用計數值,實際上是引用計數值減 1, 例如,如果對象的引用計數爲 10,那麼 extra_rc 爲 9。如果引用計數大於 10, 則需要使用到下面的 has_sidetable_rc
2.2 shiftcls賦值 & 爲什麼右移3位(重點)

打個斷點,在將要賦值 shiftcls 的時候,現在裏面的所有值,都是因爲 newisa.bits = ISA_MAGIC_VALUE 而來。(原因參考前置知識聯合體)

好的,重點來了,爲什麼賦值 shiftcls 的時候要右移3位?

cls 是一個 Class 對象,注意是對象,點進去看一下定義

typedef struct objc_class *Class;
---
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
    // 省略 ...
}

再繼續看 cache_t class_data_bits_t 發現都是結構體,然後看裏面的成員變量,大部分都是 uintptr_t 類型的,查看定義

typedef unsigned long           uintptr_t;

根據內存對齊原則,可知 Class 肯定是 8 字節對齊的,轉換成二進制後,低三位肯定是 000

繼續上圖的打印:

(lldb) p (uintptr_t)cls
(uintptr_t) $1 = 4294980728
(lldb) p/t (uintptr_t)cls
(uintptr_t) $2 = 0b0000000000000000000000000000000100000000000000000011010001111000
(lldb) 

再聯想聯合體的說明,共用內存,可見蘋果設計優化節省內存的良苦用心
賦值 shiftcls 的時候既沒有改變 cls 的值,也最大的優化了內存使用。

我先開始到分析到這的時候,還是有疑問,存的時候是這樣,但是取的時候呢?取的時候前三位不是 000 啊,,取的時候前三位確實不是 000,包括後面的幾位。但是蘋果在取的時候,又做了操作,接着往下分析

那賦值了 shiftcls 是不是就證明了我們關聯了類呢?通常通過 x/4gx 打印實例的時候,第一個輸出的 8 字節到底是不是 isa 呢?

2.3 object_getClass 驗證isa是否存的是class
        LGPerson *p = [LGPerson alloc];
        // #import <malloc/malloc.h>
        // Returns the class of an object.
        id tp = object_getClass(p); 

可在 objcruntime 裏面的 objc-class.mm 源碼中查到

Class object_getClass(id obj)
{
    if (obj) return obj->getIsa();
    else return Nil;
}

objc_object

inline Class 
objc_object::getIsa() 
{    // oc對象可認爲isTaggedPointer爲false
    if (fastpath(!isTaggedPointer())) return ISA();

    extern objc_class OBJC_CLASS_$___NSUnrecognizedTaggedPointer;
    uintptr_t slot, ptr = (uintptr_t)this;
    Class cls;

    slot = (ptr >> _OBJC_TAG_SLOT_SHIFT) & _OBJC_TAG_SLOT_MASK;
    cls = objc_tag_classes[slot];
    if (slowpath(cls == (Class)&OBJC_CLASS_$___NSUnrecognizedTaggedPointer)) {
        slot = (ptr >> _OBJC_TAG_EXT_SLOT_SHIFT) & _OBJC_TAG_EXT_SLOT_MASK;
        cls = objc_tag_ext_classes[slot];
    }
    return cls;
}

會調用 ISA()

inline Class 
objc_object::ISA() 
{
    ASSERT(!isTaggedPointer()); 
  // 上面分析過 SUPPORT_INDEXED_ISA = 0
#if SUPPORT_INDEXED_ISA
    if (isa.nonpointer) {
        uintptr_t slot = isa.indexcls;
        return classForIndex((unsigned)slot);
    }
    return (Class)isa.bits;
#else
    return (Class)(isa.bits & ISA_MASK);
#endif
}

最終返回的是 (Class)(isa.bits & ISA_MASK)
__x86_64__# define ISA_MASK 0x00007ffffffffff8ULL

知道計算規則後,咱們驗證下

(lldb) x/4gx p
0x10075eab0: 0x001d800100003445 0x0000000000000000
0x10075eac0: 0x0000000000000000 0x0000000000000000
(lldb) p tp
(id) $1 = 0x0000000100003440
(lldb) p 0x001d800100003445 & 0x00007ffffffffff8ULL
(unsigned long long) $2 = 4294980672
(lldb) p (Class)(0x001d800100003445 & 0x00007ffffffffff8ULL)
(Class) $3 = LGPerson
(lldb) p/x 4294980672
(long) $4 = 0x0000000100003440

總結

  1. 先打印p的內存,取前8字節,也就是 isa
  2. 打印 tp,是p實例的Class對象。
  3. isaISA_MASK 相與。得到值用 p/x 16進制打印,驗證和 tp 一樣。
  4. p (Class)(0x001d800100003445 & 0x00007ffffffffff8ULL) 強轉類型輸出也是LGPerson類。
  5. 對應 newisa.shiftcls 賦值時右移三位,0x00007ffffffffff8 (__x86_64__) 轉成2進制,可發現 0~247~63 位都是0,中間 3~46 的44位爲1,所以取的就是 shiftcls
  6. 可驗證上面所述

參考

C語言中文版:https://wiki.jikexueyuan.com/project/c/c-bit-fields.html
union-isa_t存儲圖片來源:https://juejin.im/post/6844904039080001549#heading-13

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章