iOS進階之路 (三)OC物件的原理 - isa 結構 & 走位

Akironer發表於2020-04-05

學習之前,我們先補充下位域聯合體的知識。

位域

位域的定義

所謂位域就是把一個位元組中的二進位劃分為幾個不同的區域,並說明每個區域的位數。每個域有一個域名,允許在程式中按域名進行操作——這樣就可以把幾個不同的物件用一個位元組的二進位制位域來表示。位域是C語言一種資料結構

使用位域的好處是:

  • 有些資訊在儲存時,並不需要佔用一個完整的位元組,而只需佔幾個或一個二進位制位。例如在存放一個開關量時,只有0和1 兩種狀態,用一位二進位即可。這樣節省儲存空間,而且處理簡便。 這樣就可以把幾個不同的物件用一個位元組的二進位制位域來表示。
  • 可以很方便的利用位域把一個變數給按位分解。比如只需要4個大小在0到3的隨即數,就可以只rand()一次,然後每個位域取2個二進位制位即可,省時省空間。

位域的使用

在C語言中,位域的宣告和結構體(struct)類似,但它的成員是一個或多個位的欄位,這些不同長度的欄位實際儲存在一個或多個整型變數中。

在宣告時,位域成員必須是整形或列舉型別(通常是無符號型別),且在成員名的後面是一個冒號和一個整數,整數規定了成員所佔用的位數。

位域不能是靜態型別。不能使用&對位域做取地址運算,因此不存在位域的指標,編譯器通常不支援位域的引用(reference)。

// 結構體
struct Struct {
    // (資料型別 元素);
    char a; // 1位元組 0 補1 2 3
    int b;  // 4位元組 4 5 6 7
} Str;

// 位域
struct BitArea {
    // (資料型別 位域名: 位域長度);
    char a: 1;
    int b: 3;
} Bit;

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSLog(@"Struct:%lu——BitArea:%lu", sizeof(Str), sizeof(Bit));
    }
    return 0;
}
複製程式碼

聯合體

聯合體的定義

聯合體(union,又叫共用體):使幾種不同型別的變數存放到同一段記憶體單元中。即使用覆蓋技術,幾個變數互相覆蓋重疊。

union  MyValue{
    // (資料型別 元素)
    int x;
    int y;
    double z;
};

void main(){
    union MyValue d1;
    d1.x = 90;
    d1.y = 100; 
    d1.z = 23.8; // 最後一次賦值有效

    printf("%d,%d,%lf\n",d1.x,d1.y,d1.z);
}

// 輸出結果:
-858993459,-858993459,23.800000
複製程式碼

聯合體定義使用時注意點:

  • union中可以定義多個成員,union的大小由最大的成員的大小決定。
  • union成員共享同一塊大小的記憶體,一次只能使用其中的一個成員。
  • 對某一個成員賦值,會覆蓋其他成員的值,因為他們共享一塊記憶體。
  • union中各個成員儲存的起始地址都是相對於基地址的偏移都為0。

聯合體和結構體區別:

  1. 結構體的各個成員會佔用不同的記憶體,互相之間沒有影響;而共用體的所有成員佔用同一段記憶體,修改一個成員會影響其餘所有成員。
  2. 結構體佔用的記憶體大於等於所有成員佔用的記憶體的總和(成員之間可能會存在縫隙),共用體佔用的記憶體等於最長的成員佔用的記憶體。共用體使用了記憶體覆蓋技術,同一時刻只能儲存一個成員的值,如果對新的成員賦值,就會把原來成員的值覆蓋掉

isa 的結構

在之前的iOS探索alloc流程中,我們提了一句obj->initInstanceIsa(cls, hasCxxDtor)在內部呼叫initIsa(cls, true, hasCxxDtor)初始化isa,今天就分析下isa。

static ALWAYS_INLINE id
callAlloc(Class cls, bool checkNil, bool allocWithZone=false)
{
    if (fastpath(!cls->ISA()->hasCustomAWZ())) {
        
        if (fastpath(cls->canAllocFast())) {
            // No ctors, raw isa, etc. Go straight to the metal.
            bool dtor = cls->hasCxxDtor();
            id obj = (id)calloc(1, cls->bits.fastInstanceSize());
            if (slowpath(!obj)) return callBadAllocHandler(cls);
            obj->initInstanceIsa(cls, dtor);
            return obj;
        } else {
            // Has ctor or raw isa or something. Use the slower path.
            id obj = class_createInstance(cls, 0);
            if (slowpath(!obj)) return callBadAllocHandler(cls);
            return obj;
        }
    }
}
複製程式碼

isa 的初始化

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;
    }
}
複製程式碼
  • TaggedPointer 專門用來儲存小的物件(8-10),例如NSNumber和NSDate。
  • 建立物件跟著斷點發現nonpointer為true
  • else流程走SUPPORT_INDEXED_ISA,表示isa_t中存放的 Class資訊是Class 的地址,還是一個索引(根據該索引可在類資訊表中查詢該類結構地址)

isa_t 的結構

union isa_t {
    isa_t() { }                                 // 初始化方法1
    isa_t(uintptr_t value) : bits(value) { }    // 初始化方法2

    Class cls;                                  // 成員1
    uintptr_t bits;                             // 成員2
#if defined(ISA_BITFIELD)
    struct {                                    // 成員3
        ISA_BITFIELD;  // defined in isa.h      // 位域巨集定義
    };
#endif
};
複製程式碼

通過原始碼我們發現isa它一個聯合體,8個位元組,它的特性就是共用記憶體,或者說是互斥(比如說如果cls賦值了,再對bits進行賦值時會覆蓋掉cls)。在isa_t聯合體內使用巨集ISA_BITFIELD定義了位域,我們進入位域內檢視原始碼。

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)

# else
#   error unknown architecture for packed isa
# endif
複製程式碼

iOS進階之路 (三)OC物件的原理 - isa 結構 & 走位

  1. nonpointer:是否對isa指標開啟指標優化
  • nonpointer = 0:不優化,純isa指標,當訪問isa指標時,直接通過isa.cls和類進行關聯,返回其成員變數cls
  • nonpointer = 1:優化過的isa指標,指標內容不止是類物件地址,還會使用位域存放類資訊、物件的引用計數,此時建立newisa並初始化後賦值給isa指標。 如果沒有,則可以更快的釋放物件。
  1. has_assoc:是否有關聯物件,0沒有,1存在。
  2. has_cxx_dtor:該物件是否有 C++ 或者 Objc 的析構器,如果有解構函式,則需要做析構邏輯, 如果沒有,則可以更快的釋放物件
  3. shiftcls:儲存類物件和元類物件的指標的值,在開啟指標優化的情況下,在 arm64 架構中用 33 位用來儲存類指標
  4. magic:用於偵錯程式判斷當前物件是真的物件還是沒有初始化的空間
  5. weakly_referenced:物件是否被指向或者曾經指向一個 ARC 的弱變數, 沒有弱引用的物件可以更快釋放
  6. deallocating:標誌物件是否正在釋放記憶體
  7. has_sidetable_rc:當物件引用技術大於 10 時,則需要借用該變數儲存進位(rc = retainCount)
  8. extra_rc:當表示該物件的引用計數值,實際上是引用計數值減 1, 例如,如果物件的引用計數為 10,那麼 extra_rc 為 9。如果引用計數大於 10, 則需要使用到下面的 has_sidetable_rc。

isa_t聯合體有3個成員(Class clsuintptr_t bits、聯合體+位域ISA_BITFIELD),3個成員共同佔用8位元組的記憶體空間,通過ISA_BITFIELD裡面的位域成員,可以對8位元組空間的不同二進位制位進行操作,達到節省記憶體空間的目的。

聯合體所有屬性共用記憶體,記憶體長度等於其最長成員的長度,使程式碼儲存資料高效率的同時,有較強的可讀性;而位域可以容納更多型別

shiftcls關聯類

在shiftcls中儲存著類物件和元類物件的記憶體地址資訊,我們重點看一下newisa.indexcls = (uintptr_t)cls->classArrayIndex()uintptr_t shiftcls : 33這兩行原始碼。

上篇文章中我們提到,在Person例項物件裡面可能因為記憶體優化,屬性的位置可能發生變換(比如ch1和ch2)。但是物件記憶體的第一個屬性必然是isa。因為isa來自於NSObject類,是繼承過來的,根本還沒有編輯屬性列表(關於ro/rw我們後續章節會提到)。

我們就測試下,person的第一個屬性是不是isa。

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Person *person = [Person alloc];
        objc_getClass();
    }
    return 0;
}

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

inline Class 
objc_object::getIsa() 
{
    if (!isTaggedPointer()) return ISA();

    uintptr_t ptr = (uintptr_t)this;
    if (isExtTaggedPointer()) {
        uintptr_t slot = 
            (ptr >> _OBJC_TAG_EXT_SLOT_SHIFT) & _OBJC_TAG_EXT_SLOT_MASK;
        return objc_tag_ext_classes[slot];
    } else {
        uintptr_t slot = 
            (ptr >> _OBJC_TAG_SLOT_SHIFT) & _OBJC_TAG_SLOT_MASK;
        return objc_tag_classes[slot];
    }
}
複製程式碼
inline Class 
objc_object::ISA() 
{
    assert(!isTaggedPointer()); 
#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
}

# if __arm64__
#   define ISA_MASK        0x0000000ffffffff8ULL
# elif __x86_64__
#   define ISA_MASK        0x00007ffffffffff8ULL
# endif
複製程式碼

列印出isa & mask的值,與class第一段地址比較。

iOS進階之路 (三)OC物件的原理 - isa 結構 & 走位

例項物件首地址一定 是isa。例項物件通過isa & isa_mask關聯類。

思考

如果我們把聯合體中的位域換成基本資料型別來表示,結合記憶體對齊原則,ISA_BITFIELD佔用24個位元組。 通過位域,每一個繼承自NSObject的物件都至少減少了16位元組的記憶體空間。

// 在arm64下將位域換成基本資料型別
struct isa_t_bitFields {
    unsigned char nonpointer;           // 1位元組  0
    unsigned char has_assoc;            // 1位元組  1
    unsigned char has_cxx_dtor;         // 1位元組  2 補3 4 5 6 7
    unsigned long shiftcls;             // 8位元組  8 9 10 11 12 13 14 15
    unsigned char magic;                // 1位元組  16
    unsigned char weakly_referenced;    // 1位元組  17
    unsigned char deallocating;         // 1位元組  18
    unsigned char has_sidetable_rc;     // 1位元組  19
    unsigned int extra_rc;              // 4位元組  20 21 22 23
};
複製程式碼

isa走位

類在記憶體中只存在一個

Class class1 = [Person class];
Class class2 = [Person alloc].class;
Class class3 = object_getClass([Person alloc]);
Class class4 = [Person alloc].class;

NSLog(@"\n%p\n%p\n%p\n%p", class1, class2, class3, class4);
複製程式碼
0x1000020f0
0x1000020f0
0x1000020f0
0x1000020f0
複製程式碼

類在記憶體中只會存在一個,而例項物件可以存在多個。

通過物件/類檢視isa走位

  • 例項物件例項化出來。例項物件 通過isa關聯
  • 本質上也是物件,通過元類例項化出來。類物件元類通過isa關聯:
  • 例項對 - isa - - isa - 元類

我們模仿object_getClass,通過isa & isa_mask,得到物件通過isa關聯的類。

iOS進階之路 (三)OC物件的原理 - isa 結構 & 走位

  1. 列印AKPerson類取得isa
  2. AKPerson類進行偏移得到AKPerson元類指標,列印AKPerson元類取得isa
  3. AKPerson元類進行偏移得到NSObject根元類指標,列印NSObject根元類取得isa
  4. NSObject根元類進行偏移得到NSObject根元類本身指標
  5. 列印NSObject根類取得isa
  6. NSObject根類進行偏移得到NSObject根元類指標

通過NSObject檢視isa走位

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // NSObject例項物件
        NSObject *object1 = [NSObject alloc];
        // NSObject類
        Class class = object_getClass(object1);
        // NSObject元類
        Class metaClass = object_getClass(class);
        // NSObject根元類
        Class rootMetaClass = object_getClass(metaClass);
        // NSObject根根元類
        Class rootRootMetaClass = object_getClass(rootMetaClass);
        NSLog(@"\n%p 例項物件\n%p 類\n%p 元類\n%p 根元類\n%p 根根元類",object1, class, metaClass, rootMetaClass, rootRootMetaClass);
    }
    return 0;
}
複製程式碼
0x103239120 例項物件
0x7fff9498b118 類
0x7fff9498b0f0 元類
0x7fff9498b0f0 根元類
0x7fff9498b0f0 根根元類
複製程式碼

1.例項物件-> 類物件 -> 元類 -> 根元類 -> 根元類(本身)
2.NSObject(根類) -> 根元類 -> 根元類(本身)
3.指向根元類的isa都是一樣的

類、元類是由系統建立的

iOS進階之路 (三)OC物件的原理 - isa 結構 & 走位
iOS進階之路 (三)OC物件的原理 - isa 結構 & 走位

1.物件是程式猿根據類例項化的。
2.類是程式碼編寫的,記憶體中只有一份,是系統建立的。
3.元類是系統編譯時,系統編譯器建立的,便於方法的編譯

isa走點陣圖

iOS進階之路 (三)OC物件的原理 - isa 結構 & 走位

isa 走位(虛線):例項物件 -> 類物件 -> 元類 -> 根元類 -> 根元類自身
繼承關係(實現):子類 -> 父類 -> NSObject -> nil。 根元類的父類為NSObject。

參考資料

帶你深入理解iOS-位域

結構體、聯合體、列舉

相關文章