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

本文的目的主要是理解類與 isa 是如何關(guān)聯(lián)的
在介紹正文之前,首先要理解一個(gè)概念:oc 對(duì)象的本質(zhì)是什么

OC 對(duì)象的本質(zhì)

在探索本質(zhì)之前,先了解一個(gè)編譯器 clang

Clang

  • \color{#DC143C}{clang} 是有 Apple 主導(dǎo)編寫,基于 LLVM 和 c/c++/oc 的編譯器
  • 主要是用于底層編譯,將一些文件輸出成 c++文件,例如 main.m 輸出成 main.cpp,其目的是為了更好的觀察底層實(shí)現(xiàn)邏輯.

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

  • 在 main 中自定義一個(gè) LGPerson 類,寫一個(gè) name 屬性
@interface LGPerson : NSObject
@property(nonatomic,copy)NSString * name;
@end

@implementation LGPerson
@end
  • 通過(guò)終端,將 main.m 編譯成 main.cpp 文件,可以通過(guò)以下幾種方式
//1、將 main.m 編譯成 main.cpp
clang -rewrite-objc main.m -o main.cpp

//2、將 ViewController.m 編譯成  ViewController.cpp
clang -rewrite-objc -fobjc-arc -fobjc-runtime=ios-13.0.0 -isysroot / /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator13.7.sdk ViewController.m

//以下兩種方式是通過(guò)指定架構(gòu)模式的命令行,使用xcode工具 xcrun
//3、模擬器文件編譯
- xcrun -sdk iphonesimulator clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp 

//4、真機(jī)文件編譯
- xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main- arm64.cpp 
  • 打開 cpp 文件,找到 LGPerson 的定義,發(fā)現(xiàn)被定義成了一個(gè)結(jié)構(gòu)體
  • LGPerson_IMPL 中的第一個(gè)屬性其實(shí)就是 isa,是繼承自 NSObject,屬于偽繼承,偽繼承的方式就是把NSObject_IMPL定義為 LGPerson_IMPL 的第一個(gè)屬性,意味著 LGPerson 就有了 NSObject 的所有屬性
struct LGPerson_IMPL {
    struct NSObject_IMPL NSObject_IVARS;
    NSString *_name;
};
struct NSObject_IMPL {
    Class isa;
};

通過(guò)以上分析,理解了 oc 對(duì)象的本質(zhì),但是有一個(gè)疑問,為什么 isa 的類型是 Class???

  • 底層 2中分析過(guò),alloc 核心方法之一initInstanceIsa,通過(guò)這個(gè)方法的實(shí)現(xiàn),isa 是一個(gè) isa_t 類型
  • 而在 NSObject 中定義的 isa 是 class 類型,根本原因在于,isa 對(duì)外反饋的是類信息,為了讓開發(fā)人員更加清晰明確,在 isa 返回時(shí)做了一個(gè)強(qiáng)制類型轉(zhuǎn)換,源碼中的強(qiáng)轉(zhuǎn)如下
inline Class
isa_t::getDecodedClass(bool authenticated) {
#if SUPPORT_INDEXED_ISA
    if (nonpointer) {
        return classForIndex(indexcls);
    }
    return (Class)cls;
#else
    return getClass(authenticated);
#endif
}

inline Class
objc_object::ISA(bool authenticated)
{
    ASSERT(!isTaggedPointer());
    return isa.getDecodedClass(authenticated);
}
image.png

總結(jié)

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

objc_setProperty 源碼探索

除了 LGPerson 的底層定義,我們還發(fā)現(xiàn)了 name 的 set 和 get 方法定義,其中 set 方法依賴objc_setProperty


image.png

下面就來(lái)探索 objc_setProperty 源碼

  • 在源碼中搜索 objc_setProperty,找到源碼實(shí)現(xiàn)
void objc_setProperty(id self, SEL _cmd, ptrdiff_t offset, id newValue, BOOL atomic, signed char shouldCopy) 
{
    bool copy = (shouldCopy && shouldCopy != MUTABLE_COPY);
    bool mutableCopy = (shouldCopy == MUTABLE_COPY);
    reallySetProperty(self, _cmd, newValue, offset, atomic, copy, mutableCopy);
}
  • 進(jìn)入reallySetProperty源碼實(shí)現(xiàn),其方法主要是新值 retain,舊值 release


    image.png

總結(jié)

通過(guò) objc_setProperty 源碼探索,有幾下幾點(diǎn)說(shuō)明

  • object_setProperty 主要作用就是關(guān)聯(lián)上層 set 方法以及底層reallySetProperty方法,作為一個(gè)中間層
  • 設(shè)計(jì)的原因.上層的 set 方法有很多,如果直接調(diào)用底層方法,會(huì)產(chǎn)生很多的臨時(shí)變量,當(dāng)你想查找一個(gè) sel 的時(shí)候,會(huì)非常麻煩
  • 所以蘋果采用了適配器設(shè)計(jì)模式(將底層接口適配為客戶端需要的接口),對(duì)外提供一個(gè)接口,供上層使用,對(duì)內(nèi)調(diào)用底層的 set 方法,使其相互不受影響,無(wú)論上層怎么變,下層都不變,主要達(dá)到一個(gè)上下層隔離的目的
    下圖代表,上層,隔離層,底層的關(guān)系


    未命名文件.png

cls 與類的關(guān)聯(lián)原理

探索出發(fā)點(diǎn)就是initInstanceIsa函數(shù), 探究 isa 與類是如何關(guān)聯(lián)到一起的
在這之前需要了解一個(gè)聯(lián)合體, 為什么 isa 的類型 isa_t 是聯(lián)合體類型

聯(lián)合體union

構(gòu)造數(shù)據(jù)類型的方式有兩種

  • 結(jié)構(gòu)體(struct)
  • 聯(lián)合體(union,也叫共用體)

結(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é)的空間,但在使用的時(shí)候,你只用了 4 個(gè), 就會(huì)有 12 字節(jié)浪費(fèi)掉了
  • 優(yōu)點(diǎn):存儲(chǔ)量大,包容性強(qiáng), 互相之間不影響

聯(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)存,節(jié)省了內(nèi)存空間

兩者的區(qū)別

  • 內(nèi)存占用情況
    - 結(jié)構(gòu)體的各個(gè)成員占用不同的內(nèi)存,互相不影響
    - 共用體各個(gè)成員占用同一段內(nèi)存,修改其中一個(gè)成員,會(huì)影響其他所有成員
  • 內(nèi)存分配大小
    - 結(jié)構(gòu)體的內(nèi)存 >= 內(nèi)部所有成員內(nèi)存相加(中間會(huì)有間隙)
    - 共用體占用的內(nèi)存 == 其內(nèi)部最大成員占用的內(nèi)存

isa 的類型 isa_t

以下是 isa 指針的類型 isa_t 的定義,從定義可以看出是通過(guò)聯(lián)合體(union)定義的

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

    uintptr_t bits;

private:
    // Accessing the class requires custom ptrauth operations, so
    // force clients to go through setClass/getClass by making this
    // private.
    Class cls;

public:
#if defined(ISA_BITFIELD)
    struct {
        ISA_BITFIELD;  // defined in isa.h
    };

    bool isDeallocating() {
        return extra_rc == 0 && has_sidetable_rc == 0;
    }
    void setDeallocating() {
        extra_rc = 0;
        has_sidetable_rc = 0;
    }
#endif

    void setClass(Class cls, objc_object *obj);
    Class getClass(bool authenticated);
    Class getDecodedClass(bool authenticated);
};

isa類型使用聯(lián)合體的原因也是基于內(nèi)存優(yōu)化考慮的,這里的內(nèi)存優(yōu)化是指在 isa 指針中通過(guò)char+位域(即二進(jìn)制中的每一位都可以代表不同的信息)的原理實(shí)現(xiàn),通常來(lái)說(shuō) isa 指針占用的內(nèi)存大小是 8 字節(jié),也就是 64 位,足夠存儲(chǔ)很多信息了,這樣可以極高的節(jié)省內(nèi)存
從 isa 的定義中可以看出

  • 提供了兩個(gè)成員 cls 和 bits,由聯(lián)合體的定義可知,這兩個(gè)成員是互斥的,也就意味著,初始化 isa 指針,有兩種方式
    - 通過(guò) cls 初始化,bits 無(wú)值
    - 通過(guò) bits 初始化,cls 無(wú)值
    還提供了一個(gè)結(jié)構(gòu)體定義的位域,用于存儲(chǔ)類信息和其他信息,結(jié)構(gòu)體的成員ISA_BITFIELD這是一個(gè)宏定義,有兩個(gè)版本,arm64(對(duì)應(yīng) ios 移動(dòng)端)和x86_64(macos)
#   else
#     define ISA_MASK        0x0000000ffffffff8ULL
#     define ISA_MAGIC_MASK  0x000003f000000001ULL
#     define ISA_MAGIC_VALUE 0x000001a000000001ULL
#     define ISA_HAS_CXX_DTOR_BIT 1
#     define ISA_BITFIELD                                                      \
        uintptr_t nonpointer        : 1;  //是否對(duì) isa 指針開啟了指針優(yōu)化
        uintptr_t has_assoc         : 1; //是否有關(guān)聯(lián)對(duì)象
        uintptr_t has_cxx_dtor      : 1;  //是否有 c++實(shí)現(xiàn)
        uintptr_t shiftcls          : 33; //存儲(chǔ)類信息
        uintptr_t magic             : 6; //調(diào)試器判斷對(duì)象是真對(duì)象還是未初始化空間
        uintptr_t weakly_referenced : 1;  //對(duì)象是否被指向或者曾經(jīng)指向一個(gè)ARC 弱變量
        uintptr_t unused            : 1;                                       \
        uintptr_t has_sidetable_rc  : 1;  //是否有外掛的散列表
        uintptr_t extra_rc          : 19//額外的引用計(jì)數(shù)
#     define RC_ONE   (1ULL<<45)
#     define RC_HALF  (1ULL<<18)
#   endif

# elif __x86_64__
#   define ISA_MASK        0x00007ffffffffff8ULL
#   define ISA_MAGIC_MASK  0x001f800000000001ULL
#   define ISA_MAGIC_VALUE 0x001d800000000001ULL
#   define ISA_HAS_CXX_DTOR_BIT 1
#   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 unused            : 1;                                         \
      uintptr_t has_sidetable_rc  : 1;                                         \
      uintptr_t extra_rc          : 8
  • nonpointer有兩個(gè)值,標(biāo)識(shí)自定義的類等,占 1 位
    • 0:純 isa 指針
    • 1:不只是類對(duì)象地址,還包含了類信息,對(duì)象的引用計(jì)數(shù)等
  • has_assoc:標(biāo)識(shí)關(guān)聯(lián)對(duì)象標(biāo)志,占一位
    • 0:沒有關(guān)聯(lián)對(duì)象
    • 1:有關(guān)聯(lián)對(duì)象
  • has_cxx_dtor:表示該對(duì)象是否有 c++/oc 的析構(gòu)器(dealloc),占 1 位
    • 如果有,做析構(gòu)邏輯
    • 如果沒有,可以更快的釋放對(duì)象
  • shiftcls:表示存儲(chǔ)類的指針值,即類信息
    • arm64 占 33 位,開啟指針優(yōu)化的情況下,在 arm64 架構(gòu)下有 33 位存儲(chǔ)類指針
    • x86_64 占 44 位,
  • magic:用于調(diào)試器判斷當(dāng)前對(duì)象是真對(duì)象還是未初始化空間,占 6 位
  • weakly_referenced:代表對(duì)象是否被指向或者曾經(jīng)指向一個(gè)ARC 的弱變量
    • 沒有弱引用的變量可以更快的釋放
  • has_sidetable_rc:表示當(dāng)對(duì)象的引用計(jì)數(shù)大于 10(舉例而已,不一定是 10),則需要借用該變量存儲(chǔ)進(jìn)位
  • extra_rc:額外的引用計(jì)數(shù),表示該對(duì)象的引用計(jì)數(shù)值,實(shí)際是引用計(jì)數(shù)值減 1,
    • 如果對(duì)象的引用計(jì)數(shù)為 10,那么 extra_rc 的值為 9(舉例而已),實(shí)際上 iphone 真機(jī)上的exra_rc 是使用 19 位來(lái)存儲(chǔ)引用計(jì)數(shù)的
      針對(duì) arm64 平臺(tái) isa存儲(chǔ)情況如下


      未命名文件-2.png

原理探索

  • 通過(guò)alloc->_objc_rootAlloc->callAlloc->_objc_rootAllocWithZone->_class_createInstanceFromZone->initInstanceIsa進(jìn)入查看實(shí)現(xiàn)
objc_object::initInstanceIsa(Class cls, bool hasCxxDtor)
{
    ASSERT(!cls->instancesRequireRawIsa());
    ASSERT(hasCxxDtor == cls->hasCxxDtor());

    initIsa(cls, true, hasCxxDtor);
}
  • 進(jìn)入 initIsa 實(shí)現(xiàn),主要是初始化 isa 指針


    image.png

    該方法的邏輯主要分為兩部分

  • 1.通過(guò) cls 初始化 isa
  • 2.通過(guò) bits 初始化 isa

驗(yàn)證 isa 指針 位域(0-64)

根據(jù)前面提到的位域信息,可以在這里驗(yàn)證一下位域是真的存在的,在newisa.bits 處打一個(gè)斷點(diǎn)


image.png

在執(zhí)行到這句代碼是,通過(guò) lldb 打印p newisa, 然后走到下一行在打印一次 newisa,得到的信息如下圖


image.png

通過(guò)與前一個(gè)newisa 相比,后一個(gè)的nonpointer變成了 1, magic變成了 59,
  • 其中的 59 是十進(jìn)制的體現(xiàn), 把 isa 指針從 47 位(x86 下,前面的位域占47 位)開始讀取 6 位,在轉(zhuǎn)成 10 進(jìn)制,就是 59


    未命名文件.png

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

cls 與 isa 關(guān)聯(lián)的原理,就是 isa 中的 shiftcls 位域存儲(chǔ)了類信息,其中initInstanceIsa的過(guò)程是將calloc 指針與當(dāng)前類關(guān)聯(lián)起來(lái),有以下幾種驗(yàn)證方式

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

1.通過(guò) initIsa

  • 運(yùn)行到shiftcls = (uintptr_t)newCls >> 3,其中 shiftcls 存儲(chǔ)的是當(dāng)前類的值信息
    • 查看 newCls 信息是 SATest類
    • shiftCls 賦值的邏輯是將編碼后的 SATest 數(shù)據(jù)右移3 位


      image.png
  • 執(zhí)行 lldb 指令, 打印p (uintptr_t)newCls >> 3得到值存儲(chǔ)到 shiftCls 中


    image.png
  • 繼續(xù)執(zhí)行到isa = newisa; 打印 p newisa


    image.png

與bits 賦值結(jié)果對(duì)比,bits 位域中有兩處變化

  • cls 由默認(rèn)值變成了 SATest, isa 與類完美關(guān)聯(lián)
  • shiftCls 從 0 變成了有值

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

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

為什么需要右移 3 位

因?yàn)?shiftcls 處于 isa 中間部分,前面還有 3 個(gè)位域,為了不影響前面 3 個(gè)位域,需要右移將其抹零

方式 2:通過(guò) isa & ISA_MASK

  • 在 main 中斷點(diǎn)到 SATest 創(chuàng)建時(shí),按照下圖方式進(jìn)行打印

arm64 中 ISA_MASK 為0x0000000ffffffff8ULL
x86 中 ISA_MASK 為0x00007ffffffffff8ULL


image.png

方式 3 通過(guò) runtime中的函數(shù) object_getClass

  • 查找 object_getClass源碼實(shí)現(xiàn)


    image.png
  • 進(jìn)入getIsa實(shí)現(xiàn)


    image.png
  • 進(jìn)入 ISA()實(shí)現(xiàn)


    image.png
  • 進(jìn)入getDecodedClass實(shí)現(xiàn)


    image.png
  • 進(jìn)入getClass實(shí)現(xiàn)


    image.png

方式 4:通過(guò)位運(yùn)算

  • 在 main 中 SATest 創(chuàng)建處加一個(gè)斷點(diǎn),通過(guò)x/4gx test打印test 存儲(chǔ)信息,當(dāng)前類的信息存儲(chǔ)在 isa 指針中,切此時(shí)的 shiftcls 占 44 位(因?yàn)樵?macos 環(huán)境下)


    image.png
  • 想要讀取中間的 44 位信息,就需要經(jīng)過(guò)位運(yùn)算,將 shiftcls 右邊的 3 位和左邊的 17 位都要抹零,相對(duì)位置不能變,分為如下幾步
    - 1.先將 isa >> 3: 將前三位抹零
    - 2. 然后用第一步的結(jié)果 << 20 (本身左邊是 17 位,但是經(jīng)過(guò)第一步以后,左邊變成了 20 位)
    - 3.第二步的結(jié)果 >> 17(回到最初 shiftcls 在 isa 中的初始位置,此時(shí)左右已經(jīng)全部抹零)


    image.png
最后編輯于
?著作權(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ù)。

相關(guān)閱讀更多精彩內(nèi)容

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