iOS OC物件的本質及其isa探究

語言: CN / TW / HK

1.OC物件的本質探究

  我們要想明白OC物件的本質是什麼,它的底層是如何實現類這種結構的,首先,我們就需要建立一個類並使用Clang命令將這個檔案編譯稱為.cpp檔案,檢視並分析底層C++程式碼。

1.1 編譯OC檔案

建立一個LGPerson類,類中新增一個NSString型別的MyName屬性,在main.m檔案中如下所示:

```

import

import

@interface LGPerson : NSObject

@property (nonatomic, copy) NSString *MyName;

@end

@implementation LGPerson

@end

int main(int argc, const char * argv[]) { @autoreleasepool {

    NSLog(@"Hello, World!");
}
return 0;

} `` 使用clang命令將main.m檔案編譯為.cpp`檔案,如下圖所示:

1.2 OC類結構底層原始碼分析

  我們檢視編譯好的C++原始碼檔案,發現其中的程式碼一共有十幾萬行的程式碼,那麼我們該從那裡分析呢?我們只需要在這個檔案中搜索建立的類名就可以了,我們就可以發現一個如下的結構體: struct LGPerson_IMPL { struct NSObject_IMPL NSObject_IVARS; NSString *_MyName; };   我們發現這個結構體中正好有一個與我們建立的同名的NSString指標型別MyName成員變數,那麼通過分析我們就可以知道,其實OC物件的本質其實就是結構體,那麼另一個成員變數NSObject_IVARS是什麼呢?通過搜尋NSObject_IMPL關鍵字我們發現它也是一個結構體,如下所示: struct NSObject_IMPL { Class isa; };   因此我們發現NSObject_IMPL其實是包含了一個成員變數isa的結構體,而LGPerson_IMPL中這樣的寫法,表示的意思是LGPerson_IMPL中繼承了NSObject_IMPL中的成員變數,那麼Class又是個什麼型別呢?我們在原始碼中可以找到如下程式碼: typedef struct objc_class *Class;   Class實際上是一個objc_class *型別的結構體指標,而objc_class實際上又是一個包含了一個objc_class *型別的成員變數的結構體,原始碼中程式碼如下所示: struct objc_class { Class _Nonnull isa __attribute__((deprecated)); } __attribute__((unavailable));   另外,在原始碼檔案中又發現瞭如下的程式碼: ``` typedef struct objc_object LGPerson;

struct objc_object { Class _Nonnull isa attribute((deprecated)); };

typedef struct objc_object id; ``   分析以上的程式碼,我們知道LGPerson繼承自NSObject,而下沉到C++的底層程式碼中,其實objc_object就是NSObject的底層實現,而id實際上是一個objc_object結構體型別的指標,這就是為什麼我們在開發中使用id指標不需要加`號的原因。

1.3 OC類的屬性方法原始碼分析

  探究完LGPerson這個類的結構之後,我們再來看看關於MyName屬性的set以及get方法底層是如何實現的,其底層原始碼實現如下: ``` static NSString * I_LGPerson_MyName(LGPerson * self, SEL _cmd) { return ((NSString )((char )self + OBJC_IVAR$_LGPerson$_MyName)); }

extern "C" __declspec(dllimport) void objc_setProperty (id, SEL, long, id, bool, bool);

static void I_LGPerson_setMyName(LGPerson * self, SEL _cmd, NSString *MyName) { objc_setProperty (self, _cmd, OFFSETOFIVAR(struct LGPerson, _MyName), (id)MyName, 0, 1); }

``   上面的I_LGPerson_MyName靜態函式就是MyName屬性的get方法的底層實現,_I_LGPerson_setMyName靜態函式就是MyName屬性的set方法的底層實現,可以看到,這兩個函式中都有兩個隱藏引數(selfcmd),這就是為什麼我們可以在OC文程式碼中類的例項方法中使用self這個物件的原因,在MyNameget方法中,是使用selfLGPerson例項在記憶體中的地址)加上OBJC_IVAR_$LGPerson$_MyNameMyName成員變數在結構體中地址的偏移量)獲取得到的MyName屬性在記憶體中的地址,進而訪問這個屬性的值。實際上,在_I_LGPerson_setMyName方法中也是通過這樣的方式為MyName`屬性重新賦值的。

2. isa關聯類

  在第一小節詳細探討了OC物件的本質,我們發現OC物件的本質其實是一個結構體,並且其中有一個(objc_class結構體指標型別的)isa指標,那麼我們現在就來探究isa指標的資料儲存結構,看看isa是如何關聯到類的,但是在探究之前,我們首先需要補充一下位域以及共用體的基本知識。

2.1 位域與共用體

2.1.1 位域

想要了解在什麼情況下使用位域這種資料結構,我們首先來看如下程式碼 typedef struct { bool front; bool back; bool left; bool right; } Direction;   在上面的結構體Direction中,一共定義了4個bool型別的成員變數,通過sizeof()運算子的計算,我們可以得到Direction這個結構體的大小為4個位元組,但是bool型別的資料使用1 bit就可以表示了,如果用4個位元組(也就是32 bit)來儲存就過於浪費了。

  因此以上4個成員變數的資訊在儲存時,並不需要佔用一個完整的位元組,而只需佔用一個二進位制位。為了節省儲存空間,並使處理簡便,C語言又提供了一種資料結構,稱為"位域"或"位段"。而所謂"位域"是把一個位元組中的二進位劃分為幾個不同的區域,並說明每個區域的位數。每個域有一個域名,允許在程式中按域名進行操作。因此我們可以使用位域將每個成員變數佔用的空間為設定為1 bit來節省空間,如下所示: typedef struct { bool front : 1; bool back : 1; bool left : 1; bool right : 1; } Direction; 這樣通過sizeof()運算子的計算,Direction這個結構體的大小就變為了1個位元組大小。

2.1.2 共用體(聯合體)

  共用體是一種特殊的資料型別,允許您在相同的記憶體位置儲存不同的資料型別。您可以定義一個帶有多成員的共用體,但是任何時候只能有一個成員帶有值。共用體提供了一種使用相同的記憶體位置的有效方式。   定義共用體方式與定義結構類似,假設定義一個有三個型別不同屬性的Person共用體,程式碼如下: typedef union { int age; double height; char name[20]; } Person;   Person型別的變數可以儲存一個整數、或者一個浮點數、或者一個字串。這意味著一個變數(相同的記憶體位置)可以儲存多個多種型別的資料。您可以根據需要在一個共用體內使用任何內建的或者使用者自定義的資料型別。   共用體佔用的記憶體應足夠儲存共用體中佔用記憶體最大的成員。例如,在上面的例項中,Person 將佔用20個位元組的記憶體空間,因為在各個成員中,成員變數name所佔用的空間是最大的。

2.1.2 共用體(聯合體)與結構體的比較

優點:

  • 結構體:結構體(struct)中所有變數是“共存”的——優點是“有容乃⼤”。
  • 共用體:聯合體(union)中是各變數是“互斥”的——優點是記憶體使⽤更為精細靈活,也節省了記憶體空間。

缺點:

  • 結構體:結構體(struct)記憶體空間的分配是粗放的,缺點是不管⽤不⽤,全分配
  • 共用體:聯合體(union)中是各變數是互斥的,缺點是“不夠包容

2.2 isa指標結構探究

  前幾篇文章我們已經對OC物件在初始化的過程中兩個函式(計算記憶體大小)instanceSize以及(分配記憶體空間)calloc函式進行了詳細的探究,接下來,就該對isa指標的初始化函式進行探究了,首先來看看isa初始化函式的呼叫流程,首先,打上斷點,執行程式,發現呼叫瞭如下函式:

image.png

initInstanceIsa函式的程式碼如下: ``` inline void objc_object::initInstanceIsa(Class cls, bool hasCxxDtor) { ASSERT(!cls->instancesRequireRawIsa()); ASSERT(hasCxxDtor == cls->hasCxxDtor());

initIsa(cls, true, hasCxxDtor);

} 這個函式中實際上呼叫了`initIsa`函式,這個函式的程式碼如下所示: inline void objc_object::initIsa(Class cls, bool nonpointer, UNUSED_WITHOUT_INDEXED_ISA_AND_DTOR_BIT bool hasCxxDtor) { ASSERT(!isTaggedPointer());

image.png isa_t newisa(0);

if (!nonpointer) {
    newisa.setClass(cls, this);
} else {
    ASSERT(!DisableNonpointerIsa);
    ASSERT(!cls->instancesRequireRawIsa());

if SUPPORT_INDEXED_ISA

    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

    newisa.bits = ISA_MAGIC_VALUE;
    // isa.magic is part of ISA_MAGIC_VALUE
    // isa.nonpointer is part of ISA_MAGIC_VALUE

if ISA_HAS_CXX_DTOR_BIT

    newisa.has_cxx_dtor = hasCxxDtor;

endif

    newisa.setClass(cls, this);

endif

    newisa.extra_rc = 1;
}

// 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;

} 首先,我們發現這個函式程式碼最後是將`newisa`賦值給了`isa`,而這個`isa`實際上是一個聯合體`isa_t`型別,因此我們再來看看這個聯合體型別的isa_t是如何定義的,其程式碼如下: 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_t`這個聯合體中一共有兩個成員變數分別是`uintptr_t`(其實就是`unsigned long`)型別的`bits`以及一個`Class`(其實就是 `objc_class *`)型別的指標`cls`,這兩個成員變數在64位作業系統中佔用空間位元組長度都是`8`位元組,而如果系統支援包裝ISA指標,在arm64架構中程式碼中struct中成員變數的定義如下: # define ISA_BITFIELD \ 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 unused : 1; \ uintptr_t has_sidetable_rc : 1; \ uintptr_t extra_rc : 19 ``` 記憶體結構圖示如下:

image.png

在x86_64架構中程式碼中struct中成員變數的定義如下:

# 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 記憶體結構圖示如下:

image.png

nonpointer:表示是否開啟isa指標優化。值為0,不開啟指標優化,僅儲存類物件地址;值為1,不僅包含了類物件地址,也包含了類資訊、物件引用計數、是否析構等。儲存在第0個進位制位。\ has_assoc:表示關聯物件標誌位。值為0,沒關聯;值為1,關聯。儲存在第1個進位制位。\ has_cxx_dtor:表示該物件是否有解構函式,值為0,表示沒有解構函式,可以更快是否物件;值為1,表示有解構函式。儲存在第2個進位制位。\ shiftcls:表示類指標的值。在x86_64架構中儲存在第3到第46位,在arm64架構中佔用儲存在第3到第35位。\ magic:用於調式器判斷當前物件是真的物件還是沒有初始化的空間。在x86_64架構中儲存在第47到第52位;在arm64架構中儲存在第36位到第41位。\ weakly_referenced:標誌物件是否被指向或者曾經指向一個ARC的弱變數,沒有弱引用的物件可以更快釋放。在x86_64架構中儲存在第53位,在arm64架構中儲存在第42位。 \ unused:標誌物件是否正在釋放記憶體。在x86_64架構中儲存在第54位,在arm64架構中儲存在第43位。 \ has_sidetable_rc:表示當物件引用計數大於10時,則需要借用該變數儲存進位。在x86_64架構中儲存在第55位,在arm64架構中儲存在第44位。\ extra_rc:當表示該物件的引用計數值減1之後的值。例如:如果物件的引用計數為8,那麼extra_rc值為7,如果引用計數大於10,則需要使用到has_sidetable_rc。在x86_64架構中儲存在第56到第63位,在arm64架構中儲存在第45到第63位。