Objective-C
(以下簡稱 OC
)是一門動態性強的程式語言,OC
的動態性是基於 Runtime
來實現的,Runtime
系統是由 C\C++\組合語言
編寫的,提供的 API
基本都是 C
語言的。這裡我們從蘋果提供的 Runtime
程式碼來探究類的本質。
legacy 版本
OC
的 runtime
分為兩個版本.一個是 legacy
版本,一個是 modern
版本。相信很多讀者都見過下面這段代表 OC
類結構的程式碼:
struct objc_class {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
Class _Nullable super_class OBJC2_UNAVAILABLE;
const char * _Nonnull name OBJC2_UNAVAILABLE;
long version OBJC2_UNAVAILABLE;
long info OBJC2_UNAVAILABLE;
long instance_size OBJC2_UNAVAILABLE;
struct objc_ivar_list * _Nullable ivars OBJC2_UNAVAILABLE;
struct objc_method_list * _Nullable * _Nullable methodLists OBJC2_UNAVAILABLE;
struct objc_cache * _Nonnull cache OBJC2_UNAVAILABLE;
struct objc_protocol_list * _Nullable protocols OBJC2_UNAVAILABLE;
#endif
} OBJC2_UNAVAILABLE;
複製程式碼
其實這段程式碼就是 legacy
版本 已經在 2006
年的 WWDC
大會上釋出 Objective-C 2.0
後棄用了, OBJC2_UNAVAILABLE
標記的內容已經不再使用,那麼現在的結構是什麼呢?
物件
OC
中,每一個物件都是類的例項,先直接來看原始碼中的結構:
struct objc_object {
private:
isa_t isa;
// ...
}
複製程式碼
代表物件的結構中只有一個 isa
的成員變數,在 arm64
架構下,系統對 isa
進行了優化,它不光存著地址資訊,還存著其他資訊。因此物件的本質就是包含了一個私有成員變數 isa
的結構體,而 isa
存著的地址就指向著物件所屬的類。不同的物件有不同的成員變數,編譯後,每個物件的結構體也會存著自己的成員變數的值。
使用命令獲取編譯後的程式碼 xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc Coder.m
@interface Coder : Person
@property (nonatomic, copy) NSString *name;
@end
// 編譯後檢視 `Coder` 的實現
struct NSObject_IMPL {
Class isa;
};
struct Coder_IMPL {
struct NSObject_IMPL NSObject_IVARS;
NSString * _Nonnull _name;
};
複製程式碼
之所以成員變數的值存在物件中,這個也很好理解,每個物件肯定是獨立存在的,都需要擁有自己的變數值。而變數名稱和方法等等存在什麼地方呢,就是類了!
類
類存著成員變數的型別,方法等等,原始碼如下:
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
class_rw_t *data() {
return bits.data();
}
// 省略...
}
複製程式碼
首先可以看到的一點是 objc_class
繼承了 objc_object
,因此其實 OC
中的類也可以理解為一種物件,稱之為類物件,在 legacy
版本中,物件的結構體中只有一個 isa
指標,指向它的類物件,而類物件中也有一個 isa
指標,指向它的元類。modern
版本使用繼承後,類物件的結構體就繼承了這個優化後的 isa
變數。但對比兩個版本,會發現 modern
版本中除了superclass&cache
,其餘的很多變數不在了,並多了一個 bits
變數。
struct class_data_bits_t {
uintptr_t bits;
class_rw_t* data() {
return (class_rw_t *)(bits & FAST_DATA_MASK);
}
// ...
}
複製程式碼
這個結構體裡面是通過一個位運算獲取的指向 class_rw_t
的指標,可見 bits
存著 class_rw_t
結構體的指標和一些其他資訊。然後把目光轉到 class_rw_t
上:
'rw' 和 ro' 分別表示 'readwrite' 和 'readonly'
struct class_rw_t {
// Be warned that Symbolication knows the layout of this structure.
uint32_t flags;
uint32_t version;
const class_ro_t *ro;
method_array_t methods;
property_array_t properties;
protocol_array_t protocols;
// ...
}
複製程式碼
可以看到原先 legacy
版本中的方法、屬性和協議列表就存在這個裡面,這幾個列表可以理解為是二維陣列,是可讀可寫的,包含了類的初始內容、分類的內容,二維陣列方便增加。 而這裡又有一個 class_ro_t
:
struct class_ro_t {
uint32_t flags;
uint32_t instanceStart;
uint32_t instanceSize;
// ....
const char * name;
method_list_t * baseMethodList;
protocol_list_t * baseProtocols;
const ivar_list_t * ivars;
// ....
};
複製程式碼
class_ro_t
裡面的 baseMethodList、baseProtocols、ivars、baseProperties
可以理解為是一維陣列,是隻讀的,包含了類的初始內容。
從這裡我們也能看出分類不能動態新增成員變數到類物件的原因,分類是通過
runtime
載入的,這時候類結構已經確定下來了,並且這裡儲存成員變數的記憶體是隻讀的。
元類
上面已經提到,類物件的 isa
中儲存的地址指向的就算類物件的類,稱之為元類,元類儲存著物件方法。也就是說例項方法是儲存在類中的,類方法是儲存在元類中的。用一個經典的圖來表示物件、類和元類的關係。
圖中已經很好的闡述了三者之間的關係,不過這裡需要強調兩點。
- 元類的
isa
指向的是基類的元類。 - 基類的元類的
superclass
指向的是基類
這兩個點很容易被忽略,在一些面試題中經常出現。