iOS底層之isa結(jié)構(gòu)分析及關(guān)聯(lián)類

iOS底層之a(chǎn)lloc、init探究這篇文章,我們可以知道,alloc一個(gè)對(duì)象的過程,主要是計(jì)算所需內(nèi)存大小cls->instanceSize、申請(qǐng)內(nèi)存空間calloc、將isa與類進(jìn)行關(guān)聯(lián)obj->initInstanceIsa。

??那么指針和類是怎么關(guān)聯(lián)的呢?isa到底是什么結(jié)構(gòu)?保存了什么信息?下面來一一解惑。


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

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

alloc的源碼跟進(jìn)去到關(guān)聯(lián)指針和類的步驟:

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);
    }

跟進(jìn)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條件是否滿足,都會(huì)生成一個(gè)isa_t的類型。跟進(jìn)去可以發(fā)現(xiàn):

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
};

這個(gè)isa_t是一個(gè)union聯(lián)合體,Class cls代表isa關(guān)聯(lián)的類的類型,uintptr_t bits是一段保存著isa指針優(yōu)化、是否關(guān)聯(lián)對(duì)象標(biāo)志位、對(duì)象是否有析構(gòu)函數(shù)、類相關(guān)信息、引用計(jì)數(shù)等信息的8字節(jié)大小的無符號(hào)長整型數(shù)據(jù)。要知道聯(lián)合體和結(jié)構(gòu)體的區(qū)別是:

  • 結(jié)構(gòu)體(struct)中所有變量是“共存”的——優(yōu)點(diǎn)是“有容乃大”, 全面;缺點(diǎn)是struct內(nèi)存空間的分配是粗放的,不管用不用,全分配。結(jié)構(gòu)體的類型大小大于等于內(nèi)部所有變量的類型大小總和,最終的類型大小是最大成員的類型大小的倍數(shù),不足補(bǔ)齊。

  • 聯(lián)合體(union)中是各變量是“互斥”的——缺點(diǎn)就是不夠“包容”; 但優(yōu)點(diǎn)是內(nèi)存使用更為精細(xì)靈活,也節(jié)省了內(nèi)存空間。聯(lián)合體類型的大小等于最大成員類型大小。

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

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

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

ISA_BITFIELD是一個(gè)宏定義,分別在x86_64架構(gòu)arm64架構(gòu)是這樣的:

isa的bits位域

bits的64位存儲(chǔ)分布圖:

bits位域分布

其中存儲(chǔ)的成員信息:

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

了解完isa內(nèi)部結(jié)構(gòu)之后,我們來驗(yàn)證一下alloc的過程中isa跟類是如何關(guān)聯(lián)的。
在執(zhí)行BKPerson *objc = [BKPerson alloc];時(shí)跟進(jìn)到initIsa的方法中:


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

打印出這個(gè)newisa

這時(shí)的isa的成員clsnil,bits默認(rèn)為0,bits的位域信息都是初始值0

往下執(zhí)行


這一句是給bits賦值一個(gè)初始值,這是一個(gè)系統(tǒng)宏定義

#   define ISA_MAGIC_VALUE 0x001d800000000001ULL

這時(shí)再打印newisa,可以看到賦值后的nonpointer已經(jīng)是1了,magic為59。
上面我們了解到magicx86_64架構(gòu)下的bits位域分布在47-52位,占據(jù)6位,用計(jì)算器驗(yàn)證下這個(gè)59是不是0x001d800000000001ULL里的:

0x001d800000000001ULL的二進(jìn)制

可以看到第一位是1,跟我們的打印結(jié)果一致,nonpointer值變?yōu)?code>1,第47位往后數(shù)6位是111011,那么59的二進(jìn)制是:
59的二進(jìn)制

此時(shí)此刻,可以得出結(jié)果,這magic59確實(shí)是由0x001d800000000001ULL填進(jìn)去的。

再往下執(zhí)行



has_cxx_dtor賦值為false,表示沒有自定義的析構(gòu)函數(shù)。

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

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


打印cls這個(gè)類,并手動(dòng)將其地址右移3位,可以得出536871965,確實(shí)等于shiftcls的數(shù)值。

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

cls右移3位的原因

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

??那我們?cè)趺醋C明得出的這個(gè)類就是已經(jīng)關(guān)聯(lián)上了我們的對(duì)象指針?

我們將斷點(diǎn)的堆?;赝说?code>obj的關(guān)聯(lián)類的地方。


控制臺(tái)打印這個(gè)obj指針,并將isa的內(nèi)存地址右移3位,再左移20位,再右移17位,這時(shí)再打印地址移動(dòng)之后的isa的地址,可以看到,就是我們上面關(guān)聯(lián)的類,也就是說這時(shí)對(duì)象指針和類關(guān)聯(lián)上了

這個(gè)過程可以用下圖清晰表現(xiàn)出來:


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

獲取對(duì)象的類這個(gè)操作其實(shí)在我們?nèi)粘i_發(fā)中經(jīng)常用到,我們通過導(dǎo)入#import <objc/runtime.h>,

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

結(jié)果為BKPerson

查看這個(gè)函數(shù)的源碼,

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);
也就是取出對(duì)象的isa里的bits與運(yùn)算ISA_MASK
這個(gè)宏的定義是:

我們?cè)偻ㄟ^lldb命令驗(yàn)證這個(gè)過程。取出對(duì)象objisa地址 & ISA_MASK,可以得出就是對(duì)象的類。
那么這個(gè)算法,其實(shí)就簡化了我們上面對(duì)isa地址的一頓左移右移操作,直接一步到位得出類。
通過計(jì)算器查看這個(gè)ISA_MASK宏的二進(jìn)制

ISA_MASK的二進(jìn)制

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

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

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