Objective-C isa指針探秘

稍微精深一點的IOS開發(fā)都聽說過isa指針。它在OC的類中起到了指示自身類型的作用,是runtime實現(xiàn)的基礎(chǔ)。那么isa指針到底是如何實現(xiàn)的呢,讓我們從源碼的層面進行分析。

NSObject -> Class -> objc_class -> objc_object

新建一個最簡單的空類:

@interface Person : NSObject
@end

@implementation Person
@end

command點擊NSObejct,我們可以看到cls的實現(xiàn):

@interface NSObject <NSObject> {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wobjc-interface-ivars"
    Class isa  OBJC_ISA_AVAILABILITY;
#pragma clang diagnostic pop
}

那么這個Class又是什么數(shù)據(jù)呢?

typedef struct objc_class *Class;

我們可以看到Class實際上是objc_class結(jié)構(gòu)體的指針。而objc_class,就是OC類的元類。

objc_class在objc源碼中有三處定義:

// in runtime.h
struct objc_class {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;
    .....
} OBJC2_UNAVAILABLE;


#if __OBJC2__
#include "objc-runtime-new.h"
#else
#include "objc-runtime-old.h"
#endif

// in objc-runtime-old.h
struct objc_class : objc_object {}

// in objc-runtime-new.h
struct objc_class : objc_object {}

我們當(dāng)前使用的版本多是 objc2,所以起作用的是 最后一個 objc-runtime-new.h中定義的objc_class。objc_class則繼承了objc_object。

struct objc_class : objc_object 

objc_object同樣在兩個位置有定義:

in objc.h

#if !OBJC_TYPES_DEFINED
....
struct objc_object {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;
};
#endif


in objc-private.h

#ifdef _OBJC_OBJC_H_
#error include objc-private.h before other headers
#endif

#define OBJC_TYPES_DEFINED 1

struct objc_object {
private:
    isa_t isa;
.....
}

根據(jù)objc-private.h中的宏定義我們可以看到,起作用的實際上是objc-private.h中的定義。

對一個繼承了NSObject的類而言,isa最后能定位到objc_object中。而用來存儲類信息的,是objc_objectisa_t isa;

isa_t

** isa_t是一個union**聚合體。聚合體和結(jié)構(gòu)體最大的不同是它的內(nèi)存存儲方式。對聚合體而言,所有的成員變量的起始地址相同,對每一個成員的修改都會影響所有的變量。這樣做最大的好處就是可以節(jié)約內(nèi)存空間。但是也會導(dǎo)致每次可用的變量只有一個。

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

    Class cls; // 初始化是不會使用的
    uintptr_t bits;
#if defined(ISA_BITFIELD)
    struct {
        ISA_BITFIELD;  // 這才是關(guān)鍵
    };
#endif
};

isa_t 是一個聯(lián)合體,這里所占空間為 8字節(jié),共64位 ,內(nèi)存布局從低位到高位情況如下圖:


    struct {
        ISA_BITFIELD;  // 這才是關(guān)鍵
    };

#   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

注意 在 struct 的變量A后面加 :(數(shù)字n), 是指定該變量A在內(nèi)存中占用 n 位。

  • nonpointer(存儲在第0字節(jié)) : 是否為優(yōu)化isa標(biāo)志。0代表是優(yōu)化前的isa,一個純指向類或元類的指針;1表示優(yōu)化后的isa,不止是一個指針,isa中包含類信息、對象的引用計數(shù)等?,F(xiàn)在基本上都是優(yōu)化后的isa。

  • has_assoc (存儲在第1個字節(jié)): 關(guān)聯(lián)對象標(biāo)志位。對象含有或者曾經(jīng)含有關(guān)聯(lián)引用,0表示沒有,1表示有,沒有關(guān)聯(lián)引用的可以更快地釋放內(nèi)存。

  • has_cxx_dtor(存儲在第2個字節(jié)): 析構(gòu)函數(shù)標(biāo)志位,如果有析構(gòu)函數(shù),則需進行析構(gòu)邏輯,如果沒有,則可以更快速地釋放對象。

  • shiftcls :(存儲在第3-35字節(jié))存儲類的指針,其實就是優(yōu)化之前 isa 指向的內(nèi)容。在arm64架構(gòu)中有33位用來存儲類指針。x86_64架構(gòu)有44位。

  • magic(存儲在第36-41字節(jié)):判斷對象是否初始化完成, 是調(diào)試器判斷當(dāng)前對象是真的對象還是沒有初始化的空間。

  • weakly_referenced(存儲在第42字節(jié)):對象被指向或者曾經(jīng)指向一個 ARC 的弱變量,沒有弱引用的對象可以更快釋放(dealloc的底層代碼有體現(xiàn))。

  • deallocating(存儲在第43字節(jié)):標(biāo)志對象是否正在釋放內(nèi)存。

  • has_sidetable_rc(存儲在第44字節(jié)):判斷該對象的引用計數(shù)是否過大,如果過大則需要其他散列表來進行存儲。

  • extra_rc(存儲在第45-63字節(jié)。):存放該對象的引用計數(shù)值減1后的結(jié)果。對象的引用計數(shù)超過 1,會存在這個里面,如果引用計數(shù)為 10,extra_rc 的值就為 9。

isa的初始化

對OC類而言,isa的初始化是在 alloc中完成的。具體來說是在

static ALWAYS_INLINE id
_class_createInstanceFromZone(Class cls,...) {

...
id obj; // id 是objc_object的指針

    // 3: ?
    if (!zone && fast) {
        obj->initInstanceIsa(cls, hasCxxDtor);
    } else {
        obj->initIsa(cls);
    }
...
}

initInstanceIsainitIsa最后都會統(tǒng)一到一個函數(shù)里:

(去除一些宏定義,斷言以及條件判斷等,我們直接將代碼減少到它執(zhí)行的代碼)

inline void 
objc_object::initIsa(Class cls, bool nonpointer, bool hasCxxDtor)  {
          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;
}
  1. 首先對整個bits進行賦值,傳入 ISA_MAGIC_VALUE ,在arm64架構(gòu)下,該值為
#   define ISA_MAGIC_VALUE 0x000001a000000001ULL

將該值轉(zhuǎn)換為2進制

image.png

對應(yīng) isa_t 中內(nèi)存布局的位置,可以看出對 bits 賦值 就是對 nonpinter 和 magic 賦值的過程。

2.其次對has_cxx_dtor賦值。

  1. 最后對shifcls賦值

shiftcls詳解

shiftcls 里面存放著類的指針。眾所周知指針是64位的,那么為什么可以放置在33位或或者44位的shiftcls中呢?在需要指針時又如何還原呢?

首先我們看下shiftcls的賦值:

inline void 
objc_object::initIsa(Class cls, bool nonpointer, bool hasCxxDtor) 
{ 
....
        isa_t newisa(0);
....
        newisa.shiftcls = (uintptr_t)cls >> 3;
....
        isa = newisa;
    }
}

首先我們將cls的尾部三位去掉,這是因為對類而言,它至少包含一個isa指針,那么它的長度一定是8的整數(shù)倍。這里我們可以使用runtime計算下Person的長度來認(rèn)證下:

#import <objc/runtime.h>
size_t insSize = class_getInstanceSize([Person class]);
NSLog(@"PP Size:%zd",insSize);

然后,我們會看到源碼中shiftcls的宏定義后面有一個數(shù)據(jù) MACH_VM_MAX_ADDRESS:

 uintptr_t shiftcls          : 33; /*MACH_VM_MAX_ADDRESS 0x1000000000*/ \

這是最大虛擬尋址空間,也就是內(nèi)存空間能夠分配的最大地址。以ARM32為例,它最大的虛擬尋址空間為 0x10,0000,0000。而33 + 3 (右移的三位) = 36 = 4 * 9。正好是虛擬尋址空間的位數(shù) - 1。正好可以覆蓋虛擬尋址空間。也就是說64位的數(shù)據(jù)中,前面64 - 36 = 18位是無用的。

因此,只要存儲64位指針中間的33位就可以表示了。只要在使用的時候與上mask:0x0000000ffffffff8,就可以得到對應(yīng)的類指針。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容