iOS OC物件的本質及其isa探究
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方法的底層實現,可以看到,這兩個函式中都有兩個隱藏引數(
self,
cmd),這就是為什麼我們可以在OC文程式碼中類的例項方法中使用
self這個物件的原因,在
MyName的
get方法中,是使用
self(
LGPerson例項在記憶體中的地址)加上
OBJC_IVAR_$LGPerson$_MyName(
MyName成員變數在結構體中地址的偏移量)獲取得到的
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
初始化函式的呼叫流程,首先,打上斷點,執行程式,發現呼叫瞭如下函式:
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());
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
```
記憶體結構圖示如下:
在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
記憶體結構圖示如下:
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
位。