OC底層原理05 - isa與對象的關聯

對象在alloc的時候有重要三步。

  • 計算需要開辟的內存空間大小
  • 開辟指定大小的空間
  • 將對象與isa指針關聯起來

本文重點分析,如何將對象與isa指針關聯起來

OC對象的本質

在探索對象與isa關聯之前,需要了解到底什么是OC對象。那如何探索OC對象呢?

探索前準備

在探索OC對象的本質之前,先了解一個編譯器:Clang

  • Clang是一個由Apple主導編寫的,基于LLVMC/C++/OC的編譯器

  • Clang主要用于底層編譯,可以將OC文件轉化為C++文件,這使得我們可以更好的觀察底層的結構實現邏輯

  • 常用的Clang編譯指令

//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/iPhoneSimulator14.0.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 

開始探索

  • 自定義一個類LGPerson,為了方便觀察,在自定義類中增加一個屬性name。
@interface LGPerson : NSObject
@property (nonatomic, copy) NSString *name;
@end

@implementation LGPerson
@end
  • 在main函數中定義一個LGPerson對象。
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        LGPerson *person = [LGPerson alloc];
        NSLog(@"%@",person);
    }
    return 0;
}
  • 使用clang將main函數編譯成c++文件
clang -rewrite-objc main.m -o main.cpp
  • 打開編譯好的main.cpp,找到LGPerson的定義
//NSObject的定義
typedef struct objc_class *Class;
@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;
};

由上面的代碼可以看到,LGPerson在底層會被編譯成結構體

  1. LGPerson_IMPL中的第一個成員是一個嵌套的結構體NSObject_IMPL,這個結構體的成員只有一個,那就是isa
  2. LGPerson_IMPL中的第二個成員就是定義的屬性name。

總結

  1. 對象的本質一個含有isa的結構體

  2. isaClass類型

  3. Class是一個objc_class結構體類型的指針

探索isa

object_class結構在Object.mm中。定義如下:

struct objc_class : objc_object {
    // Class ISA;
    Class superclass;
    cache_t cache;             // formerly cache pointer and vtable
    class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags
}

struct objc_object {
private:
    isa_t isa;
}

由源碼可知,objc_class結構體繼承于objc_object結構體,,而objc_object中只有一個私有成員isa_t isa

接下來我們一項一項分析。

isa_t isa

arm64之前isa就是一個普通的指針,只存儲類對象元類對象指針,但arm64之后isa做了優化,采用了聯合體的形式,這使得8字節的內存可以存儲更多的內容。

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_BITFIELD對8字節內存的位域進行了宏定義,方便不同架構下代碼統一。

isa位域.png

接下來以arm64架構對isa的位域進行說明

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

    Class cls;
    uintptr_t bits;
    
    struct {
        uintptr_t nonpointer        : 1;                                       
        uintptr_t has_assoc         : 1;                                       
        uintptr_t has_cxx_dtor      : 1;                                       
        uintptr_t shiftcls          : 33; /*MACH_VM_MAX_ADDRESS 0x1000000000*/ 
        uintptr_t magic             : 6;                                       
        uintptr_t weakly_referenced : 1;                                       
        uintptr_t deallocating      : 1;                                       
        uintptr_t has_sidetable_rc  : 1;                                       
        uintptr_t extra_rc          : 19
     }
};
  • nonpointer(bit0):是否對isa指針開啟指針優化

    • 0 - 純isa指針;
    • 1 - 不止是類對象地址,還包含了類信息、對象引用計數等。
  • has_assoc(bit1):關聯對象標志位

    • 0 - 沒有關聯對象;
    • 1 - 存在關聯對象;
  • has_cxx_dtor(bit2):該對象是否有C++或者Objc的析構器,如果有則做析構邏輯,如果沒有則可以更快的釋放對象。

    • 0 - 沒有C++或者Objc的析構器;
    • 1 - 有C++或者Objc的的析構器;
  • shiftcls(bit3 - bit35):存儲類的指針,開啟指針優化的情況下在arm64架構下有33位來存儲類的指針

  • magic(bit36 - bit41):判斷當前對象是否初始化完成。調試器用來判斷當前對象是真的對象還是沒有初始化的空間。

  • weakly_referenced(bit42):對象被指向或者曾經指向一個 ARC 的弱變量,沒有弱引用的對象可以更快釋放(dealloc的底層代碼有體現)

  • deallocating(bit43):標志對象是否正在釋放內存

  • has_sidetable_rc(bit44):判斷該對象的引用計數是否過大,如果過大則需要其他散列表來進行存儲。

  • extra_rc(bit45 - bit63):存放該對象的引用計數值減1后的結果。對象的引用計數超過 1,會存在這個里面,如果引用計數為 10,extra_rc 的值就為 9。

initIsa

了解了isa的結構之后,接下來看一下是如何將isa與類關聯起來的。

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);
        newisa.bits = ISA_MAGIC_VALUE;
        newisa.has_cxx_dtor = hasCxxDtor;
        newisa.shiftcls = (uintptr_t)cls >> 3;
        isa = newisa;
    }
}

這個函數主要做三件事:

  • 定義一個newisa結構體,并初始化為全0
  • 向newisa結構體中的各位賦值
  • 把newisa賦值給objc_object的成員變量

這個過程最重要的就是第二步,向newisa結構體中的各位賦值

  1. newisa.bits = ISA_MAGIC_VALUE;
    newisa.bits初始化為ISA_MAGIC_VALUE
    ISA_MAGIC_VALUE被宏定義為0x000001a000000001ULL。轉換為二進制就是
    ISA_MAGIC_VALUE

從這個對應著isa_t的64個位域,可以看到這是對nonpointermagic賦值。

  1. newisa.has_cxx_dtor = hasCxxDtor;
    對has_cxx_dtor賦值

  2. newisa.shiftcls = (uintptr_t)cls >> 3
    cls右移3位,然后賦值給newisa.shiftcls
    重點來了,這里為什么要右移3位。

shiftcls為什么是(uintptr_t)cls >> 3

cls是Class類型,定義如下:

typedef struct objc_class *Class;

struct objc_class : objc_object {
    // Class ISA;
    Class superclass;
    cache_t cache;             // formerly cache pointer and vtable
    class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags
    // 省略 ...
}

再繼續看cache_tclass_data_bits_t發現都是結構體,然后看里面的成員變量,大部分都是uintptr_t類型的,查看定義

typedef unsigned long           uintptr_t;

根據內存對齊原則,可知Class肯定是8字節對齊的,同樣的,cls的指向地址(也既開始地址)肯定是8的倍數,轉換成二進制后低三位肯定是000

再聯想聯合體的說明,共用內存,可見蘋果設計優化節省內存的良苦用心。

賦值shiftcls的時候既沒有改變cls的值,也最大的優化了內存使用

至此,isa與類就關聯起來,接下來就來驗證了。

驗證之前分析

定義一個LGPerson對象,然后再進入到initIsa函數中,斷點停下。

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);
        newisa.bits = ISA_MAGIC_VALUE;
        newisa.has_cxx_dtor = hasCxxDtor;
        newisa.shiftcls = (uintptr_t)cls >> 3;
        isa = newisa;
    }
}
  • 當執行isa_t newisa(0);時,通過lldb調試輸出newisa的值。
(lldb) p newisa
(isa_t) $1 = {
  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
  }
}

此時newisa內容為全為0;

  • 當執行完位域賦值后再查看newisa的值
(lldb) p newisa
(isa_t) $2 = {
  cls = LGPerson
  bits = 8303516107965037
   = {
    nonpointer = 1
    has_assoc = 0
    has_cxx_dtor = 1
    shiftcls = 536875085
    magic = 59
    weakly_referenced = 0
    deallocating = 0
    has_sidetable_rc = 0
    extra_rc = 0
  }
}

此時可以看到,isashiftcls已經與類關聯起來了。
isashiftcls已經與類關聯起來之后,回到obj->initInstanceIsa(cls, hasCxxDtor);這里。
通過lldb輸出obj的內容

(lldb) x/4gx obj
0x101233f30: 0x001d80010000826d 0x0000000000000000
0x101233f40: 0x0000000000000000 0x0000000000000000

根據我們的分析,對象是結構體,且第一個成員為isa。此時0x001d80010000826d就是isa

如何驗證該isa就是LGPerson類呢?

我們前面分析isa的shiftcls就是類,那如何取出shiftcls的值呢?

這里仍然是前面的分析,shiftclsisabit3 - bit35

接下來通過lldb取出isabit3 - bit35

//兩種方式取出bit3 - bit35
//1. 通過移位的方式 
(lldb) p 0x001d80010000826d >> 3
(long) $14 = 1037939513495629
(lldb) p 1037939513495629 << 30
(long) $15 = 576465233028055040
(lldb) p 576465233028055040 >> 27
(long) $16 = 4295000680
(lldb) po 4295000680
LGPerson

//2. 通過掩碼的方式
(lldb) po 0x001d80010000826d & 0x0000000ffffffff8ULL
LGPerson

果然,此時isabit3 - bit35存的就是LGPerson類信息。這也驗證了我們之前的分析。

疑問

前面提到newisa.shiftcls = (uintptr_t)cls >> 3;,即將cls右移了3位之后再賦值給shiftcls。那為何取出來的時候直接是Class而不需要左移3位呢?

解答

  1. 前面有講到,為什么右移3位
    答:因為Class是8字節對齊cls指向地址(也既開始地址)肯定是8的倍數,轉換為二進制后3位就是0,出于優化的考慮,在賦值的時候無需將無用的后3位也賦值過去。

  2. 賦值之后如何取?
    我們來看蘋果是如何取出Class的。

objc_object::ISA() 
{
    ASSERT(!isTaggedPointer()); 
    return (Class)(isa.bits & ISA_MASK);
}

這里直接將isa.bits與掩碼ISA_MASK進行與操作后,強轉成Class類型。
為什么這樣就可以取出Class呢?
答:因為進行與操作可以直接將isa.bitsbit3 - bit35保留外,其余的位都置0,此時就相當于bit3 - bit35Class右移3位的值,而bit0 - bit2Class的后三位。這樣就與Class結構對應了。因此可以通過強轉獲得Class信息。

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