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

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

OC 對象的本質(zhì)

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

Clang

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

探索對象本質(zhì)

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

@implementation LGPerson
@end
  • 通過終端,將 main.m 編譯成 main.cpp 文件,可以通過以下幾種方式
//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

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

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

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

  • 底層 2中分析過,alloc 核心方法之一initInstanceIsa,通過這個方法的實現(xiàn),isa 是一個 isa_t 類型
  • 而在 NSObject 中定義的 isa 是 class 類型,根本原因在于,isa 對外反饋的是類信息,為了讓開發(fā)人員更加清晰明確,在 isa 返回時做了一個強制類型轉(zhuǎn)換,源碼中的強轉(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 對象的本質(zhì)是結(jié)構體
  • LGPerson 中 isa 繼承自 NSObject 中的 isa

objc_setProperty 源碼探索

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


image.png

下面就來探索 objc_setProperty 源碼

  • 在源碼中搜索 objc_setProperty,找到源碼實現(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);
}
  • 進入reallySetProperty源碼實現(xiàn),其方法主要是新值 retain,舊值 release


    image.png

總結(jié)

通過 objc_setProperty 源碼探索,有幾下幾點說明

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


    未命名文件.png

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

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

聯(lián)合體union

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

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

結(jié)構體

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

  • 缺點:所有變量都分配內(nèi)存,比較浪費內(nèi)存,假設有 4 個 int 成員,一共分配了 16 字節(jié)的空間,但在使用的時候,你只用了 4 個, 就會有 12 字節(jié)浪費掉了
  • 優(yōu)點:存儲量大,包容性強, 互相之間不影響

聯(lián)合體

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

  • 缺點:包容性弱
  • 優(yōu)點:所有成員共用一段內(nèi)存,節(jié)省了內(nèi)存空間

兩者的區(qū)別

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

isa 的類型 isa_t

以下是 isa 指針的類型 isa_t 的定義,從定義可以看出是通過聯(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 指針中通過char+位域(即二進制中的每一位都可以代表不同的信息)的原理實現(xiàn),通常來說 isa 指針占用的內(nèi)存大小是 8 字節(jié),也就是 64 位,足夠存儲很多信息了,這樣可以極高的節(jié)省內(nèi)存
從 isa 的定義中可以看出

  • 提供了兩個成員 cls 和 bits,由聯(lián)合體的定義可知,這兩個成員是互斥的,也就意味著,初始化 isa 指針,有兩種方式
    - 通過 cls 初始化,bits 無值
    - 通過 bits 初始化,cls 無值
    還提供了一個結(jié)構體定義的位域,用于存儲類信息和其他信息,結(jié)構體的成員ISA_BITFIELD這是一個宏定義,有兩個版本,arm64(對應 ios 移動端)和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;  //是否對 isa 指針開啟了指針優(yōu)化
        uintptr_t has_assoc         : 1; //是否有關聯(lián)對象
        uintptr_t has_cxx_dtor      : 1;  //是否有 c++實現(xiàn)
        uintptr_t shiftcls          : 33; //存儲類信息
        uintptr_t magic             : 6; //調(diào)試器判斷對象是真對象還是未初始化空間
        uintptr_t weakly_referenced : 1;  //對象是否被指向或者曾經(jīng)指向一個ARC 弱變量
        uintptr_t unused            : 1;                                       \
        uintptr_t has_sidetable_rc  : 1;  //是否有外掛的散列表
        uintptr_t extra_rc          : 19//額外的引用計數(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有兩個值,標識自定義的類等,占 1 位
    • 0:純 isa 指針
    • 1:不只是類對象地址,還包含了類信息,對象的引用計數(shù)等
  • has_assoc:標識關聯(lián)對象標志,占一位
    • 0:沒有關聯(lián)對象
    • 1:有關聯(lián)對象
  • has_cxx_dtor:表示該對象是否有 c++/oc 的析構器(dealloc),占 1 位
    • 如果有,做析構邏輯
    • 如果沒有,可以更快的釋放對象
  • shiftcls:表示存儲類的指針值,即類信息
    • arm64 占 33 位,開啟指針優(yōu)化的情況下,在 arm64 架構下有 33 位存儲類指針
    • x86_64 占 44 位,
  • magic:用于調(diào)試器判斷當前對象是真對象還是未初始化空間,占 6 位
  • weakly_referenced:代表對象是否被指向或者曾經(jīng)指向一個ARC 的弱變量
    • 沒有弱引用的變量可以更快的釋放
  • has_sidetable_rc:表示當對象的引用計數(shù)大于 10(舉例而已,不一定是 10),則需要借用該變量存儲進位
  • extra_rc:額外的引用計數(shù),表示該對象的引用計數(shù)值,實際是引用計數(shù)值減 1,
    • 如果對象的引用計數(shù)為 10,那么 extra_rc 的值為 9(舉例而已),實際上 iphone 真機上的exra_rc 是使用 19 位來存儲引用計數(shù)的
      針對 arm64 平臺 isa存儲情況如下


      未命名文件-2.png

原理探索

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

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


    image.png

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

  • 1.通過 cls 初始化 isa
  • 2.通過 bits 初始化 isa

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

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


image.png

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


image.png

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


    未命名文件.png

isa 與類的關聯(lián)

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

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

1.通過 initIsa

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


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


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


    image.png

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

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

為什么在shiftcls 賦值時需要強轉(zhuǎn)

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

為什么需要右移 3 位

因為 shiftcls 處于 isa 中間部分,前面還有 3 個位域,為了不影響前面 3 個位域,需要右移將其抹零

方式 2:通過 isa & ISA_MASK

  • 在 main 中斷點到 SATest 創(chuàng)建時,按照下圖方式進行打印

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


image.png

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

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


    image.png
  • 進入getIsa實現(xiàn)


    image.png
  • 進入 ISA()實現(xiàn)


    image.png
  • 進入getDecodedClass實現(xiàn)


    image.png
  • 進入getClass實現(xiàn)


    image.png

方式 4:通過位運算

  • 在 main 中 SATest 創(chuàng)建處加一個斷點,通過x/4gx test打印test 存儲信息,當前類的信息存儲在 isa 指針中,切此時的 shiftcls 占 44 位(因為在 macos 環(huán)境下)


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


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

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

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