iOS底層 - isa與類關(guān)聯(lián)的原理

iOS開發(fā)底層探究之路

isa 與類關(guān)聯(lián)原理探究

在探究isa之前,先要理清一個(gè)概念:對(duì)象是什么?對(duì)象的本質(zhì)是什么?
首先來(lái)了解一下Clang:

Clang是由Apple主導(dǎo)編寫,是一個(gè)C語(yǔ)言、C++、Objective-C語(yǔ)言的輕量級(jí)編譯器

因?yàn)?code>Objective-C是C、C++的超集,所以想要看到OC底層源碼結(jié)構(gòu),我們需要借助Clang 編譯器來(lái)查看底層實(shí)現(xiàn)。

1.對(duì)象的本質(zhì)探究

創(chuàng)建工程,在main.m中添加LGPerson 類,如下圖:


探究對(duì)象本質(zhì)

打開終端,定位到當(dāng)前main.m文件位置,在終端輸入clang -rewrite-objc main.m -o main.cpp,可以看到在main.m文件位置發(fā)現(xiàn)多了一個(gè)main.cpp文件:

終端生成main.cpp文件

打開main.cpp 文件,查找我們的LGPerson,如下所示:

main.cpp中的LGPerson是啥?

  • 從上圖可知,LGPerson類對(duì)象在底層中被編譯成了一個(gè)struct結(jié)構(gòu)體。
  • 因?yàn)樵?code>C++中結(jié)構(gòu)體是可以繼承的,LGPerson_IMPL中的第一個(gè)屬性其實(shí)就是 isa,是繼承自NSObject,屬于偽繼承,偽繼承的方式是直接將NSObject結(jié)構(gòu)體定義為LGPerson中的第一個(gè)屬性,意味著LGPerson 擁有 NSObject中的所有成員變量。
//NSObject的定義
@interface NSObject <NSObject> {
    Class isa  OBJC_ISA_AVAILABILITY;
}

//NSObject 的底層編譯
struct NSObject_IMPL {
    Class isa;
};

//LGPerson的底層編譯
struct LGPerson_IMPL {
    struct NSObject_IMPL NSObject_IVARS; // 等效于 Class isa;
    NSString *_name;
};

總結(jié)

  • OC對(duì)象的本質(zhì)就是結(jié)構(gòu)體 .
  • LGPerson中的isa就是繼承自NSObject中的isa.

2.objc_setProperty 探究

上面所展示的main.cppLGPerson相關(guān)信息中,我們還看到屬性變量namesetget方法,如下圖所示:

屬性的get與set

可以看到,set方法是通過(guò)Runtime 中的objc_setProperty方法實(shí)現(xiàn)的。
現(xiàn)在我們來(lái)看看這個(gè)objc_setProperty是如何一步一步實(shí)現(xiàn)的:

  • objc4_781源碼中全局搜索objc_setProperty,找到如下:
    objc_setProperty實(shí)現(xiàn)
  • 接著,我們繼續(xù)進(jìn)入reallySetProperty方法:
    reallySetProperty實(shí)現(xiàn)

通過(guò)查看objc_setProperty方法的實(shí)現(xiàn),發(fā)現(xiàn)上層屬性的set方法到底層的set方法經(jīng)過(guò)objc_setProperty方法處理之后,已經(jīng)失去了痕跡,只是帶進(jìn)來(lái)了每個(gè)set方法特有的_cmd,可想而知,objc_setProperty就是上層set和下層set的一個(gè)中間關(guān)聯(lián)層。

  • objc_setProperty 是關(guān)聯(lián)上層set和下層set的一個(gè)中間接口。
  • 這么設(shè)計(jì)的原因是,大量的上層set會(huì)產(chǎn)生大量的臨時(shí)變量。
  • 基于上述原因,蘋果采用了適配器設(shè)計(jì)模式(即將底層接口適配為客戶端需要的接口),對(duì)外提供一個(gè)接口,供上層的set方法使用,對(duì)內(nèi)調(diào)用底層的set方法,使其相互不受影響,即無(wú)論上層怎么變,下層都是不變的,或者下層的變化也無(wú)法影響上層,主要是達(dá)到上下層接口隔離的目的。
    大致可以用下圖表示上層、接口隔離層、底層關(guān)系:
    上層、接口隔離層、底層關(guān)系

3. isa類型isa_t分析

在分析isa_t之前我們先了解一下聯(lián)合體結(jié)構(gòu)體的區(qū)別:

結(jié)構(gòu)體

結(jié)構(gòu)體是指把不同的數(shù)據(jù)組合成一個(gè)整體,其變量是共存的,變量不管是否使用,都會(huì)分配內(nèi)存。

  • 缺點(diǎn):所有屬性都分配內(nèi)存,比較浪費(fèi)內(nèi)存,假設(shè)有4個(gè)int成員,一共分配了16字節(jié)的內(nèi)存,但是在使用時(shí),你只使用了4字節(jié),剩余的12字節(jié)就是屬于內(nèi)存的浪費(fèi)。

  • 優(yōu)點(diǎn)存儲(chǔ)容量較大,包容性強(qiáng),且成員之間不會(huì)相互影響。

聯(lián)合體

聯(lián)合體也是由不同的數(shù)據(jù)類型組成,但其變量是互斥的,所有的成員共占一段內(nèi)存。而且共用體采用了內(nèi)存覆蓋技術(shù),同一時(shí)刻只能保存一個(gè)成員的值,如果對(duì)新的成員賦值,就會(huì)將原來(lái)成員的值覆蓋掉。

  • 缺點(diǎn):包容性弱。

  • 優(yōu)點(diǎn):所有成員共用一段內(nèi)存,使內(nèi)存的使用更為精細(xì)靈活,同時(shí)也節(jié)省了內(nèi)存空間

兩者的區(qū)別
  • 內(nèi)存占用情況

    • 結(jié)構(gòu)體的各個(gè)成員會(huì)占用不同的內(nèi)存,互相之間沒有影響
    • 共用體的所有成員占用同一段內(nèi)存,修改一個(gè)成員會(huì)影響其余所有成員
  • 內(nèi)存分配大小

    • 結(jié)構(gòu)體內(nèi)存 >= 所有成員占用的內(nèi)存總和(成員之間可能會(huì)有縫隙)
    • 共用體占用的內(nèi)存等于最大的成員占用的內(nèi)存

下面我們就來(lái)分析分析isa到底是啥:

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的類型isa_t上圖所示,是一個(gè)union聯(lián)合體。使用聯(lián)合體的原因,是為了節(jié)省內(nèi)存,這里的內(nèi)存優(yōu)化是指在isa指針中通過(guò)char + 位域(即二進(jìn)制中每一位均可表示不同的信息)。

  • isa_t聯(lián)合體提供了兩個(gè)成員,clsbits ,他們的關(guān)系的互斥的,但是根據(jù)聯(lián)合體的定義(從前往后排列),初始化isa時(shí),會(huì)有兩種方式:

    • 通過(guò)cls初始化,bits無(wú)默認(rèn)值。
    • 通過(guò)bits初始化,cls有默認(rèn)值。
  • isa_t還提供了一個(gè)結(jié)構(gòu)體類型的位域,用于存儲(chǔ)類信息及其他信息,結(jié)構(gòu)體的成員ISA_BITFIELD,這是一個(gè)定義,有兩個(gè)版本 __arm64__(對(duì)應(yīng)ios移動(dòng)端) 和 __x86_64__(對(duì)應(yīng)macOS),以下是它們的一些宏定義,如下圖所示

    ISA_BITFIELD結(jié)構(gòu)圖

  • nonpointer:表示是否對(duì)isa指針開啟指針優(yōu)化

    • 0:純isa指針
    • 1:不止是類對(duì)象地址,isa中包含了類信息、對(duì)象的引用計(jì)數(shù)等
  • has_assoc:關(guān)聯(lián)對(duì)象標(biāo)志位

    • 0:沒有關(guān)聯(lián)對(duì)象
    • 1:存在關(guān)聯(lián)對(duì)象
  • has_cxx_dtor:該對(duì)象是否有C++或者Objc的析構(gòu)器,如果有析構(gòu)函數(shù),在對(duì)象釋放的時(shí)候就需要做析構(gòu)處理,沒有的話則會(huì)釋放的更快

  • shiftcls:存儲(chǔ)類指針的值。開啟指針優(yōu)化的情況下,在arm64架構(gòu)中有33位用來(lái)存儲(chǔ)類指針

  • magic:用于調(diào)試器判斷當(dāng)前對(duì)象是真的對(duì)象還是沒有初始化的空間

  • weakly_referenced:此對(duì)象是否指向或者曾經(jīng)指向一個(gè)ARC的弱變量,沒有弱引用的話,能更快地釋放對(duì)象

  • deallocation:標(biāo)志對(duì)象是否正在釋放內(nèi)存

  • has_sidetable_rc:當(dāng)對(duì)象引用計(jì)數(shù)大于10時(shí),則需要借用該變量存儲(chǔ)計(jì)數(shù)

  • extra_rc:當(dāng)表示該對(duì)象的引用計(jì)數(shù)值,實(shí)際上是引用計(jì)數(shù)值減1,例如,如果對(duì)象的引用計(jì)數(shù)為10,那么extra_rc為9。如果引用計(jì)數(shù)大于10,則需要使用到下面的has_sidetable_rc

4.isa初始化探究

下面我們就來(lái)看看isa_t類型的isa初始化情況,跟蹤源碼alloc底層實(shí)現(xiàn)我們來(lái)到initInstanceIsa方法,

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);//通過(guò) cls 初始化isa 
    } else {
        ASSERT(!DisableNonpointerIsa);
        ASSERT(!cls->instancesRequireRawIsa());

        isa_t newisa(0);//通過(guò) 

#if SUPPORT_INDEXED_ISA  //  0 
        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  //進(jìn)入下面代碼 
        newisa.bits = ISA_MAGIC_VALUE;  //初始化 bits 信息
        // 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;//初始化 shiftcls信息
#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;
    }
}

initIsa 方法就是通過(guò)兩種不同的初始化方式來(lái)初始化isa,根據(jù)nonpointer來(lái)判斷。

5.isa與類的關(guān)聯(lián)

調(diào)試跟蹤上圖的isa初始化過(guò)程,可以分析處isa是如何一步一步與類關(guān)聯(lián)起來(lái)的,首先打下斷點(diǎn),一步一步調(diào)試:


5.1 isa初始化調(diào)試

斷點(diǎn)p打印結(jié)果分別為:

(lldb) p newisa
(isa_t) $0 = {
  cls = nil
  bits = 0
   = {
    nonpointer = 0
    has_assoc = 0
    has_cxx_dtor = 0
    shiftcls = 0
    magic = 0
    weakly_referenced = 0
    deallocating = 0
    has_sidetable_rc = 0
    extra_rc = 0
  }
}
(lldb) p newisa
(isa_t) $1 = {
  cls = 0x001d800000000001
  bits = 8303511812964353
   = {
    nonpointer = 1
    has_assoc = 0
    has_cxx_dtor = 0
    shiftcls = 0
    magic = 59
    weakly_referenced = 0
    deallocating = 0
    has_sidetable_rc = 0
    extra_rc = 0
  }
}
(lldb) p newisa
(isa_t) $2 = {
  cls = LGPerson
  bits = 8303516107940081
   = {
    nonpointer = 1
    has_assoc = 0
    has_cxx_dtor = 0
    shiftcls = 536871966
    magic = 59
    weakly_referenced = 0
    deallocating = 0
    has_sidetable_rc = 0
    extra_rc = 0
  }
}

三步打印結(jié)果為下圖所示:


5.2 isa初始化三部曲

綜上所述,最后一個(gè)斷點(diǎn)即isa初始化結(jié)束時(shí),已經(jīng)可以看出isa的cls即為L(zhǎng)GPerson,且isa中的指針的shiftcls中存儲(chǔ)了類的信息,那接下來(lái)我們可以通過(guò)幾種方式驗(yàn)證一下:

1.通過(guò)initIsa方法中的newisa.shiftcls = (uintptr_t)cls >> 3;驗(yàn)證
2.通過(guò)isa指針地址與ISA_MSAK 的值 & 來(lái)驗(yàn)證
3.通過(guò)runtime的方法object_getClass驗(yàn)證
4.通過(guò)位運(yùn)算驗(yàn)證

方式一:斷點(diǎn)停在1位置,lldb命令p打印(uintptr_t)cls(uintptr_t)cls >> 3,然后繼續(xù)停在斷點(diǎn)3位置,p newisa發(fā)現(xiàn)此時(shí)isa中的shiftcls的值剛好與(uintptr_t)cls >> 3 相等,而且clsLGPerson綁定上了。

拓展:

為什么在shiftcls賦值時(shí)需要類型強(qiáng)轉(zhuǎn)?

因?yàn)閮?nèi)存的存儲(chǔ)不能存儲(chǔ)字符串,機(jī)器碼只能識(shí)別0 、1這兩種數(shù)字,所以需要將其轉(zhuǎn)換為uintptr_t數(shù)據(jù)類型,這樣shiftcls中存儲(chǔ)的類信息才能被機(jī)器碼理解, 其中uintptr_tlong

為什么需要右移3位?

主要是由于shiftcls處于isa指針地址的中間部分,前面還有3個(gè)位域,為了不影響前面的3個(gè)位域的數(shù)據(jù),需要右移將其抹零

方式二:回到_class_createInstanceFromZone方法中,我們?cè)?code>return obj前打下斷點(diǎn):


利用 x/4gx 獲取當(dāng)前obj的指針地址,然后進(jìn)行& ISA_MASK 操作。

方式三:通過(guò)runtimeapi,即object_getClass函數(shù)獲取類信息
查看object_getClass函數(shù) 源碼的實(shí)現(xiàn):

Class object_getClass(id obj)
{
    if (obj) return obj->getIsa();
    else return Nil;
}
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;
}
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
}

(Class)(isa.bits & ISA_MASK);, 強(qiáng)轉(zhuǎn) 而且 此步還進(jìn)行了 & 操作,這與方式二中的原理是一致的,獲得當(dāng)前的類信息

方式四:位運(yùn)算
回到_class_createInstanceFromZone方法中,x/4gx打印此時(shí)obj的存儲(chǔ)信息,此時(shí)isa中的shiftcls此時(shí)占44位(因?yàn)樘幱?code>macOS環(huán)境

位運(yùn)算驗(yàn)證方式

  • 首先,通過(guò)x/4gx obj獲取當(dāng)前obj 的存儲(chǔ)信息,拿到指針地址0x001d8001000020f1
  • p/x 0x001d8001000020f1 >> 3 右移三位,右邊三位抹0
  • p/x 0x0003b0002000041e << 20 左移20位,因?yàn)橹坝乙?code>3位后,最左邊補(bǔ)了3位0,加上之前的17位,所以想要抹掉左邊的,必須左移20
  • p/x 0x0002000041e00000 >> 17 最后右移17,使存儲(chǔ)信息的shiftcls回到原來(lái)的位置。
    最后利用p/x cls打印結(jié)果地址剛好與上方位運(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),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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