iOS 底層探索之isa結構分析

引:

什么是對象

OC的對象、類主要是基于C\C++的結構體數據結構實現的。OC對象的本質就是結構體。

在探索本質前,我們需要了解一個編輯器:clang

Clang

  • clang是一個由Apple主導編寫,基于LLVM的C/C++/OC的編輯器

  • 主要是用于底層編譯,將一些文件輸出成C++文件,例如main.m輸出成main.cpp,其目的是為了更好的觀察底層的一些結構實現的邏輯,方便理解底層原理

對象的本質

  • 在main中定義一個LGPerson類繼承于NSObject
@interface LGPerson : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, strong) NSString *nickName;
@end

@implementation LGPerson
@end
  • 通過終端,利用clang將main.m編譯成main.cpp。有以下4幾種編譯指令,可以根據自己的實際情況來編譯。
//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 
  • 編譯完成之后,打開main.cpp的文件,找到我們定義好的LGPerson類,發現在底層會被編譯成struct結構體
    • LGPerson_IMPL中的第一個屬性為NSObject_IMPL結構體,通過代碼發現此結構體就是isa,是繼承自NSObject,屬于偽繼承偽繼承的方式是直接將NSObject結構體定義為LGPerson中的第一個屬性,意味著LGPerson 擁有 NSObject中的所有成員變量。每個類的第一個屬性都是Class isa
//NSObject的定義
@interface NSObject <NSObject> {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wobjc-interface-ivars"
    Class isa  OBJC_ISA_AVAILABILITY;
#pragma clang diagnostic pop
}

//NSObject通過clang編譯的定義
struct NSObject_IMPL {
    Class isa;
};

extern "C" unsigned long OBJC_IVAR_$_LGPerson$_name;
extern "C" unsigned long OBJC_IVAR_$_LGPerson$_nickName;
// LGPerson的底層編譯結果
struct LGPerson_IMPL {
    struct NSObject_IMPL NSObject_IVARS;
    NSString *_name;
    NSString *_nickName;
};

編譯后得到的結果如下圖所示:

clang編譯后的代碼

問:isa的類型為什么會是Class?
通過之前查找源代碼找到initIsa方法,知道isa是通過isa_t類型初始化的。通過分析獲取isa是通過get方法,于是我們找到了getIsa這個方法,結果如下圖:

#if SUPPORT_NONPOINTER_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
}

源碼中,我們可以清楚的知道在isa返回的時候做了一個類型強制轉換

union聯合體位域

結構體 struct各成員各自擁有自己額內存,各自使用互不干涉,同時存在的,遵循內存對齊原則。一個struct的總長度等于內部最大成員的整數倍,不足的要補齊結構體(struct)中所有變量是“共存”的——優點是“有容乃大”, 全面;缺點是struct內存空間的分配是粗放的,不管用不用,全分配

聯合體union各成員共用一塊內存空間,并且同時只有一個成員可以得到這塊內存的使用權(對該內存的讀寫),各變量共用一個內存首地址。因此,聯合體比結構體更加節約內存。一個union變量的總長度至少能容納最大的成員變量,而且要滿足是所有成員變量類型大小的整數倍。不允許對聯合體變量名U2直接賦值或其他操作。聯合體(union)中是各變量是“互斥”的——缺點就是不夠“包容”; 但優點是內存使用更為精細靈活,也節省了內存空間

有時候為了節省內存占用可以使用的技術

@interface Car : NSObject

@property (nonatomic, assign) BOOL front;
@property (nonatomic, assign) BOOL left;
@property (nonatomic, assign) BOOL back;
@property (nonatomic, assign) BOOL right;

@end

@implementation Car

@end 

四個 BOOL屬性占用內存為 4 字節(sizeof(BOOL)= 1), 因為每次只能選擇一個方向,所以有點內存浪費,直接用 1 bit 表示一個方向也是可以的

union direction_t {
    char bits; // 1 字節
    struct {
        char front: 1; // 1 bit
        char left: 1; // 1 bit
        char back: 1;   // 1 bit
        char right: 1; // 1 bit
    };
};

printf("size of union direction_t = %lu",sizeof(_direction));
//size of union direction_t = 1
isa的類型isa_t

從源碼中,可以看到到isa指針的類型isa_t的定義,從定義中可以看出是通過聯合體(union)定義的。

union isa_t {//聯合體
    isa_t() { }
    isa_t(uintptr_t value) : bits(value) { }
    //提供了cls和bits,兩者是互斥的關系
    Class cls;
    uintptr_t bits;
#if defined(ISA_BITFIELD)
    struct {
        ISA_BITFIELD;  // defined in isa.h
    };
#endif
};

isa_t類型使用聯合體的原因也是基于內存優化的考慮,這里的內存優化是指在isa指針中通過char + 位域(即二進制中每一位均可表示不同的信息)的原理實現。通常來說,isa指針占用的內存大小8字節,即64位,已經足夠存儲很多信息了,這樣可以極大的節省內存,以提高性能

inline void 
objc_object::initIsa(Class cls, bool nonpointer, bool hasCxxDtor) 
{ 
    ASSERT(!isTaggedPointer()); 
    
    if (!nonpointer) {
        isa = isa_t((uintptr_t)cls);///isa初始化
    } else {
        ASSERT(!DisableNonpointerIsa);
        ASSERT(!cls->instancesRequireRawIsa());

        isa_t newisa(0);///isa初始化
#if SUPPORT_INDEXED_ISA /// !nonpointer的執行流程,即isa 通過cls定義
        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 ///bits的執行流程
        newisa.bits = ?ISA_MAGIC_VALUE;///bits進行賦值為0x001f800000000001ULL
        // 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;
    }
}
  • 提供了兩個成員,clsbits,由聯合體的定義所知,這兩個成員是互斥的,也可以通過上訴的代碼可以看出cls賦值和bit是賦值也是互斥的。也就意味著,當初始化isa指針時,有兩種初始化方式

    • 通過cls初始化,bits無默認值

    • 通過bits初始化,cls有默認值

  • 還提供了一個結構體定義的位域,用于存儲類信息及其他信息,結構體的成員ISA_BITFIEID,這是一個宏定義,有兩個版本__arm64__(對應iOS移動端)和__x86_64__(對應macOS),以下是它們的一些宏定義如下:

# if __arm64__
#   define ISA_MASK        0x0000000ffffffff8ULL
#   define ISA_MAGIC_MASK  0x000003f000000001ULL
#   define ISA_MAGIC_VALUE 0x000001a000000001ULL
#   define ISA_BITFIELD                                                     \
      uintptr_t nonpointer        : 1;                                       \/*是否對isa指針開啟指針優化 */ 
      uintptr_t has_assoc         : 1;                                       \/*是否有關聯對象*/
      uintptr_t has_cxx_dtor      : 1;                                       \/*是否有C++相關實現*/
      uintptr_t shiftcls          : 33; /*MACH_VM_MAX_ADDRESS 0x1000000000*/ \/*存儲類信息*/ 
      uintptr_t magic             : 6;                                       \/*調試器判斷對象是真對象還是為初始化空間*/ 
      uintptr_t weakly_referenced : 1;                                       \/*對象是否被指向或者曾經指向一個ARC的弱變量 */ 
      uintptr_t deallocating      : 1;                                       \/*標志對象是否正在釋放內存 */ 
      uintptr_t has_sidetable_rc  : 1;                                       \/*是否有外掛的散列表*/ 
      uintptr_t extra_rc          : 19/*額外的應用計數*/ 
#   define RC_ONE   (1ULL<<45)
#   define RC_HALF  (1ULL<<18)

# elif __x86_64__
#   define ISA_MASK        0x00007ffffffffff8ULL
#   define ISA_MAGIC_MASK  0x001f800000000001ULL
#   define ISA_MAGIC_VALUE 0x001d800000000001ULL
#   define ISA_BITFIELD                                                        \
      uintptr_t nonpointer        : 1;                                         \/*是否對isa指針開啟指針優化 */ 
      uintptr_t has_assoc         : 1;                                         \/*是否有關聯對象*/
      uintptr_t has_cxx_dtor      : 1;                                         \/*是否有C++相關實現*/
      uintptr_t shiftcls          : 44; /*MACH_VM_MAX_ADDRESS 0x7fffffe00000*/ \/*存儲類信息*/ 
      uintptr_t magic             : 6;                                         \/*調試器判斷對象是真對象還是為初始化空間*/ 
      uintptr_t weakly_referenced : 1;                                         \/*對象是否被指向或者曾經指向一個ARC的弱變量 */ 
      uintptr_t deallocating      : 1;                                         \/*標志對象是否正在釋放內存 */ 
      uintptr_t has_sidetable_rc  : 1;                                         \/*是否有外掛的散列表*/ 
      uintptr_t extra_rc          : 8/*額外的應用計數*/ 
#   define RC_ONE   (1ULL<<56)
#   define RC_HALF  (1ULL<<7)

# else
#   error unknown architecture for packed isa
# endif
  • nonpointer有兩個值,表示自定義的類等,占1位。0是純isa指針1不只是類對象地址,isa中包含了類信息、對象的引用計數等

  • has_assoc表示關聯對象標志等,占1位。0是沒有關聯對象1是存在關聯對象

  • has_cxx_dtor表示該對象是否有C++/OC的析構函數(dealloc),占1位。如果有析構函數,則需要做析構邏輯如果沒有,則可以更快的釋放對象

  • shiftcls表示存儲類的指針的值(類地址),即類信息。arm64中占 33位,開啟指針優化的情況下,在arm64架構中有33位用來存儲類指針,x86_64中占 44位

  • magic用于調試器判斷當前對象是真對象還是沒有初始化空間,占6

  • weakly_referenced是指對象是否被指向或者曾經指向一個ARC的弱變量。沒有弱引用對象可以更快釋放。

  • deallocating標志對象是否正在釋放內存

  • has_sidetable_rc表示 當對象引用計數大于10時,則需要借用該變量存儲進位

  • extra_rc(額外的引用計數) --- 表示該對象的引用計數值,實際上是引用計數值減1。如果對象的引用計數為10,那么extra_rc為9

isa 與 類 的關聯

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

1、通過initIsa方法中的newisa.shiftcls = (uintptr_t)cls >> 3;驗證

  • 通過 lldb打印賦值前后newisa的過程我們發現shiftcls在賦值的過程中有兩個值發生了變化, cls通過0x001d800000000001變成了LGPersonbits中的shiftcls從0變成了536871965,將isa和cls關聯了起來。

如結果下圖:


賦值前后對比.png

2、通過isa指針地址與ISA_MSAK 的值 & 來驗證

  • arm64中,ISA_MASK 宏定義的值為0x0000000ffffffff8ULL

  • x86_64中,ISA_MASK宏定義的值為0x00007ffffffffff8ULL
    首先知道ISA_MASK宏定義如上,然后回到obj->initInstanceIsa,通過LLDB打印結果如下:

    isa指針地址& ISA_MASK的結果

3、通過位運算驗證
通過上述的一些源碼分析,我們知道isa中占有的64位信息,而存儲類信息的shiftcls占33位或者44位,是從第4位開始存儲。而我們的源碼是macOS環境所以此時shiftcls占44位
所以我們獲取isa的值時,需要將右邊3位和左邊17位抹零,并且保證其相對位置不變。

然后通過LLDB指令驗證步驟如下圖:

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