iOS底層之isa結構分析及關聯類

iOS底層之alloc、init探究這篇文章,我們可以知道,alloc一個對象的過程,主要是計算所需內存大小cls->instanceSize、申請內存空間calloc、將isa與類進行關聯obj->initInstanceIsa

??那么指針和類是怎么關聯的呢?isa到底是什么結構?保存了什么信息?下面來一一解惑。


int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        
        
        BKPerson *objc = [BKPerson alloc];

        NSLog(@"Hello, World!  %@",objc);
    }
    return 0;
}

alloc的源碼跟進去到關聯指針和類的步驟:

if (!zone && fast) {
        obj->initInstanceIsa(cls, hasCxxDtor);
    } else {
        // Use raw pointer isa on the assumption that they might be
        // doing something weird with the zone or RR.
        obj->initIsa(cls);
    }

跟進obj->initInstanceIsa(cls, hasCxxDtor);

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

    initIsa(cls, true, 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;
        // 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;
    }
}

可以看到不管!nonpointer條件是否滿足,都會生成一個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是一個union聯合體,Class cls代表isa關聯的類的類型,uintptr_t bits是一段保存著isa指針優化、是否關聯對象標志位、對象是否有析構函數、類相關信息、引用計數等信息的8字節大小的無符號長整型數據。要知道聯合體和結構體的區別是:

  • 結構體(struct)中所有變量是“共存”的——優點是“有容乃大”, 全面;缺點是struct內存空間的分配是粗放的,不管用不用,全分配。結構體的類型大小大于等于內部所有變量的類型大小總和,最終的類型大小是最大成員的類型大小的倍數,不足補齊。

  • 聯合體(union)中是各變量是“互斥”的——缺點就是不夠“包容”; 但優點是內存使用更為精細靈活,也節省了內存空間。聯合體類型的大小等于最大成員類型大小。

就是說聯合體采用內存覆蓋機制,只有一塊變量存儲區,只能存一個變量的值,新的成員賦值會把原本存儲的成員信息替換掉。也就是Class clsuintptr_t bits只能set賦值其中一個。而內存使用的精細靈活,體現在以位域(即二進制中每一位均可表示不同的信息)存儲成員數據,也就是以計算機二進制存儲的方式,位bit為單位,用10標記數據,數據的尺寸大小是以占用多少bit,而不是以每個數據成員數據類型的尺寸大小(多少字節byte)存儲。isabits占用的內存大小是8字節,即64位,可以存儲足夠多的信息,很大節省了內存。

isabits成員的位域,定義在isa.h源文件中

struct {
       ISA_BITFIELD;  // defined in isa.h
   };

ISA_BITFIELD是一個宏定義,分別在x86_64架構arm64架構是這樣的:

isa的bits位域

bits的64位存儲分布圖:

bits位域分布

其中存儲的成員信息:

  • nonpointer是否開啟isa指針優化。上面介紹isa_t聯合體的兩個成員(Class cls;
    uintptr_t bits;),當nonpointer=1時,則代表對象isa不單指class了,而是使用了優化的bits保存類的信息和其他內存管理的信息。一般自定義的類都是1的,而系統類才會有純isa指針的情況,占1位。
    0:純isa指針,即表示class地址。
    1:不只是類對象地址,isa中包含了類信息、對象的引用計數等。
  • has_assoc關聯對象標志位,占1位。當關聯對象標志位被設置為 1 時,表示該對象具有關聯對象(Associated Objects)。關聯對象允許開發者將額外的數據與一個對象相關聯,而無需修改該對象的類結構。這對于給現有的類添加屬性或附加其他數據非常有用,特別是在無法修改類定義的情況下。沒有關聯對象的對象釋放的更快。
  • has_cxx_dtor 該對象是否有C++或Objc的析構器,如果有析構函數,則需要做析構邏輯,如果沒有,則可以更快釋放對象,占1位。
  • shiftclx存儲類指針的值, 也就是類信息,開啟指針優化的情況下,在arm64架構中有33位用來存儲類指針,x86_64架構中占44位。
  • magic 用于調試器判斷,在調試時區分對象是否已經初始化,占6位。固定為0x1a。
  • weakly_refrenced是用于表示該對象是否被別的對象弱引用。沒有被弱引用的對象釋放的更快。
  • deallocating 標志該對象是否正在被釋放。
  • has_sidetable_rc用于標識是否當前的引用計數過大,無法在isa中存儲,而需要借用sidetable來存儲。
  • extra_rc表示該對象的引用計數值-1,比如,一個object對象的引用計數為9,則此時extra_rc的值為8。 如果大于最大容量,就需要取一半計數存到散列表中,真機上最多有8張散列表存儲對象引用計數,x86_64則最多64張,這時上面的has_sidetable_rc值置為true

了解完isa內部結構之后,我們來驗證一下alloc的過程中isa跟類是如何關聯的。
在執行BKPerson *objc = [BKPerson alloc];時跟進到initIsa的方法中:


可以看到!nonpointer條件為false,說明BKPerson類并不是一個純isa指針,需要開啟指針優化,所以走到下面的初始化流程。

打印出這個newisa

這時的isa的成員clsnilbits默認為0bits的位域信息都是初始值0

往下執行


這一句是給bits賦值一個初始值,這是一個系統宏定義

#   define ISA_MAGIC_VALUE 0x001d800000000001ULL

這時再打印newisa,可以看到賦值后的nonpointer已經是1了,magic為59。
上面我們了解到magicx86_64架構下的bits位域分布在47-52位,占據6位,用計算器驗證下這個59是不是0x001d800000000001ULL里的:

0x001d800000000001ULL的二進制

可以看到第一位是1,跟我們的打印結果一致,nonpointer值變為1,第47位往后數6位是111011,那么59的二進制是:
59的二進制

此時此刻,可以得出結果,這magic59確實是由0x001d800000000001ULL填進去的。

再往下執行



has_cxx_dtor賦值為false,表示沒有自定義的析構函數。

newisa.shiftcls = (uintptr_t)cls >> 3;

表示將cls類地址右移3位,賦值給shiftcls,上面我們知道x86_64下,shiftclsbits64位內存中占用44位,從3-46位
通過打印的信息,cls = BKPerson能看出來已經將類信息關聯上指針了,也就是這個shiftcls = 536871965這個信息保存著類的信息。


打印cls這個類,并手動將其地址右移3位,可以得出536871965,確實等于shiftcls的數值。

??那么為什么要右移3位呢?而不直接賦值過去呢?

cls右移3位的原因

從圖可以清晰解釋,為什么需要右移3位?因為bits的成員shiftclsx86_64下占據44位,而類cls內存存儲的類信息是在第3位47位,所以需要右移3位后開始存儲,存到44位滿了就停止存儲。這樣才能準確的存儲到類的信息。

??那我們怎么證明得出的這個類就是已經關聯上了我們的對象指針?

我們將斷點的堆棧回退到obj的關聯類的地方。


控制臺打印這個obj指針,并將isa的內存地址右移3位,再左移20位,再右移17位,這時再打印地址移動之后的isa的地址,可以看到,就是我們上面關聯的類,也就是說這時對象指針和類關聯上了

這個過程可以用下圖清晰表現出來:


從指針獲取類信息的操作過程

獲取對象的類這個操作其實在我們日常開發中經常用到,我們通過導入#import <objc/runtime.h>

BKPerson *objc = [BKPerson alloc];
        
NSLog(@"%@", object_getClass(objc));  

結果為BKPerson

查看這個函數的源碼,

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

繼而查找getIsa()

inline Class 
objc_object::getIsa() 
{
    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()); 
#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里的條件的宏定義,

#   define SUPPORT_INDEXED_ISA 0

可以知道走的是這行代碼return (Class)(isa.bits & ISA_MASK);
也就是取出對象的isa里的bits與運算ISA_MASK
這個宏的定義是:

我們再通過lldb命令驗證這個過程。取出對象objisa地址 & ISA_MASK,可以得出就是對象的類。
那么這個算法,其實就簡化了我們上面對isa地址的一頓左移右移操作,直接一步到位得出類。
通過計算器查看這個ISA_MASK宏的二進制

ISA_MASK的二進制

可以看到,從第4位47位,一共44位,都為1,其他位都為0,而與運算,就是兩個數只有相同位上都為1,才會得出1,所以這個與運算,就是為了取出中間44位的類信息的算法,其他位補0,得出一個64位的數,表示這個類。
至此,我們了解了isa結構,及其位域的分布和成員作用,并探索了對象指針關聯類的過程并驗證結果。
感謝閱讀~

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。