1. 程式人生 > >iOS RunTime

iOS RunTime

本文詳細整理了 Cocoa 的 Runtime 系統的知識,它使得 Objective-C 如虎添翼,具備了靈活的動態特性,使這門古老的語言煥發生機。主要內容如下: 

  • 引言
  • 簡介
  • 與 Runtime 互動
  • Runtime 基礎資料結構
  • 訊息
  • 動態方法解析
  • 訊息轉發
  • 健壯的例項變數 (Non Fragile ivars)
  • Objective-C Associated Objects
  • Method Swizzling
  • 總結

引言

曾經覺得Objc特別方便上手,面對著 Cocoa 中大量 API,只知道簡單的查文件和呼叫。還記得初學 Objective-C 時把 [receiver message] 當成簡單的方法呼叫,而無視了“傳送訊息”

這句話的深刻含義。其實 [receiver message] 會被編譯器轉化為: 

1
objc_msgSend(receiver, selector)

如果訊息含有引數,則為: 

1
objc_msgSend(receiver, selector, arg1, arg2, ...)

如果訊息的接收者能夠找到對應的 selector,那麼就相當於直接執行了接收者這個物件的特定方法;否則,訊息要麼被轉發,或是臨時向接收者動態新增這個 selector 對應的實現內容,要麼就乾脆玩完崩潰掉。 

現在可以看出 [receiver message]

 真的不是一個簡簡單單的方法呼叫。因為這只是在編譯階段確定了要向接收者傳送 message 這條訊息,而 receive 將要如何響應這條訊息,那就要看執行時發生的情況來決定了。 

Objective-C 的 Runtime 鑄就了它動態語言的特性,這些深層次的知識雖然平時寫程式碼用的少一些,但是卻是每個 Objc 程式設計師需要了解的。 

簡介

因為Objc是一門動態語言,所以它總是想辦法把一些決定工作從編譯連線推遲到執行時。也就是說只有編譯器是不夠的,還需要一個執行時系統 (runtime system) 來執行編譯後的程式碼。這就是 Objective-C Runtime 系統存在的意義,它是整個 Objc 執行框架的一塊基石。 

Runtime其實有兩個版本: “modern” 和 “legacy”。我們現在用的 Objective-C 2.0 採用的是現行 (Modern) 版的 Runtime 系統,只能執行在 iOS 和 macOS 10.5 之後的 64 位程式中。而 maxOS 較老的32位程式仍採用 Objective-C 1 中的(早期)Legacy 版本的 Runtime 系統。這兩個版本最大的區別在於當你更改一個類的例項變數的佈局時,在早期版本中你需要重新編譯它的子類,而現行版就不需要。 

Runtime 基本是用 C 和彙編寫的,可見蘋果為了動態系統的高效而作出的努力。你可以在這裡下到蘋果維護的開原始碼。蘋果和GNU各自維護一個開源的 runtime 版本,這兩個版本之間都在努力的保持一致。 

與 Runtime 互動

Objc 從三種不同的層級上與 Runtime 系統進行互動,分別是通過 Objective-C 原始碼,通過 Foundation 框架的NSObject類定義的方法,通過對 runtime 函式的直接呼叫。 

Objective-C 原始碼

大部分情況下你就只管寫你的Objc程式碼就行,runtime 系統自動在幕後辛勤勞作著。
還記得引言中舉的例子吧,訊息的執行會使用到一些編譯器為實現動態語言特性而建立的資料結構和函式,Objc中的類、方法和協議等在 runtime 中都由一些資料結構來定義,這些內容在後面會講到。(比如 objc_msgSend 函式及其引數列表中的 id 和 SEL 都是啥)

NSObject 的方法

Cocoa 中大多數類都繼承於 NSObject 類,也就自然繼承了它的方法。最特殊的例外是 NSProxy,它是個抽象超類,它實現了一些訊息轉發有關的方法,可以通過繼承它來實現一個其他類的替身類或是虛擬出一個不存在的類,說白了就是領導把自己展現給大家風光無限,但是把活兒都交給幕後小弟去幹。 

有的NSObject中的方法起到了抽象介面的作用,比如description方法需要你過載它併為你定義的類提供描述內容。NSObject還有些方法能在執行時獲得類的資訊,並檢查一些特性,比如class返回物件的類;isKindOfClass:isMemberOfClass:則檢查物件是否在指定的類繼承體系中;respondsToSelector:檢查物件能否響應指定的訊息;conformsToProtocol:檢查物件是否實現了指定協議類的方法;methodForSelector:則返回指定方法實現的地址。 

Runtime 的函式

Runtime 系統是一個由一系列函式和資料結構組成,具有公共介面的動態共享庫。標頭檔案存放於/usr/include/objc目錄下。許多函式允許你用純C程式碼來重複實現 Objc 中同樣的功能。雖然有一些方法構成了NSObject類的基礎,但是你在寫 Objc 程式碼時一般不會直接用到這些函式的,除非是寫一些 Objc 與其他語言的橋接或是底層的debug工作。在 Objective-C Runtime Reference 中有對 Runtime 函式的詳細文件。 

Runtime 基礎資料結構

還記得引言中的objc_msgSend:方法吧,它的真身是這樣的: 

1
id objc_msgSend ( id self, SEL op, ... );

下面將會逐漸展開介紹一些術語,其實它們都對應著資料結構。熟悉 Objective-C 類的記憶體模型或看過相關原始碼的可以直接跳過。 

SEL

objc_msgSend函式第二個引數型別為SEL,它是selector在Objc中的表示型別(Swift中是Selector類)。selector是方法選擇器,可以理解為區分方法的 ID,而這個 ID 的資料結構是SEL

1
typedef struct objc_selector *SEL;

其實它就是個對映到方法的C字串,你可以用 Objc 編譯器命令 @selector() 或者 Runtime 系統的 sel_registerName 函式來獲得一個 SEL 型別的方法選擇器。 

不同類中相同名字的方法所對應的方法選擇器是相同的,即使方法名字相同而變數型別不同也會導致它們具有相同的方法選擇器,於是 Objc 中方法命名有時會帶上引數型別(NSNumber 一堆抽象工廠方法拿走不謝),Cocoa 中有好多長長的方法哦。 

id

objc_msgSend 第一個引數型別為id,大家對它都不陌生,它是一個指向類例項的指標: 

1
typedef struct objc_object *id;

objc_object又是啥呢,參考 objc-private.h 檔案部分原始碼: 

1
2
3
4
5
6
7
8
9
10
11
12
13
struct objc_object {
private:
isa_t isa;

public:

// ISA() assumes this is NOT a tagged pointer object
Class ISA();

// getIsa() allows this to be a tagged pointer object
Class getIsa();
... 此處省略其他方法宣告
}

objc_object 結構體包含一個 isa 指標,型別為 isa_t 聯合體。根據 isa 就可以順藤摸瓜找到物件所屬的類。isa 這裡還涉及到 tagged pointer 等概念。因為 isa_t 使用 union 實現,所以可能表示多種形態,既可以當成是指標,也可以儲存標誌位。有關 isa_t 聯合體的更多內容可以檢視 Objective-C 引用計數原理

PS: isa 指標不總是指向例項物件所屬的類,不能依靠它來確定型別,而是應該用 class 方法來確定例項物件的類。因為KVO的實現機理就是將被觀察物件的 isa 指標指向一箇中間類而不是真實的類,這是一種叫做 isa-swizzling 的技術,詳見官方文件

Class

Class 其實是一個指向 objc_class 結構體的指標: 

1
typedef struct objc_class *Class;

而 objc_class 包含很多方法,主要都為圍繞它的幾個成員做文章: 

1
2
3
4
5
6
7
8
9
10
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,也就是說一個 ObjC 類本身同時也是一個物件,為了處理類和物件的關係,runtime 庫建立了一種叫做元類 (Meta Class) 的東西,類物件所屬型別就叫做元類,它用來表述類物件本身所具備的元資料。類方法就定義於此處,因為這些方法可以理解成類物件的例項方法。每個類僅有一個類物件,而每個類物件僅有一個與之相關的元類。當你發出一個類似 [NSObject alloc] 的訊息時,你事實上是把這個訊息發給了一個類物件 (Class Object) ,這個類物件必須是一個元類的例項,而這個元類同時也是一個根元類 (root meta class) 的例項。所有的元類最終都指向根元類為其超類。所有的元類的方法列表都有能夠響應訊息的類方法。所以當 [NSObject alloc] 這條訊息發給類物件的時候,objc_msgSend() 會去它的元類裡面去查詢能夠響應訊息的方法,如果找到了,然後對這個類物件執行方法呼叫。 

上圖實線是 superclass 指標,虛線是isa指標。 有趣的是根元類的超類是 NSObject,而 isa 指向了自己,而 NSObject 的超類為 nil,也就是它沒有超類。

可以看到執行時一個類還關聯了它的超類指標,類名,成員變數,方法,快取,還有附屬的協議。 

cache_t

1
2
3
4
5
6
struct cache_t {
struct bucket_t *_buckets;
mask_t _mask;
mask_t _occupied;
... 省略其他方法
}

_buckets 儲存 IMP_mask 和 _occupied 對應 vtable

cache 為方法呼叫的效能進行優化,通俗地講,每當例項物件接收到一個訊息時,它不會直接在isa指向的類的方法列表中遍歷查詢能夠響應訊息的方法,因為這樣效率太低了,而是優先在 cache 中查詢。Runtime 系統會把被呼叫的方法存到 cache 中(理論上講一個方法如果被呼叫,那麼它有可能今後還會被呼叫),下次查詢的時候效率更高。

bucket_t 中儲存了指標與 IMP 的鍵值對:

1
2
3
4
5
6
7
8
9
10
11
12
13
struct bucket_t {
private:
cache_key_t _key;
IMP _imp;

public:
inline cache_key_t key() const { return _key; }
inline IMP imp() const { return (IMP)_imp; }
inline void setKey(cache_key_t newKey) { _key = newKey; }
inline void setImp(IMP newImp) { _imp = newImp; }

void set(cache_key_t newKey, IMP newImp);
};

有關快取的實現細節,可以檢視 objc-cache.mm 檔案。

class_data_bits_t

objc_class 中最複雜的是 bitsclass_data_bits_t 結構體所包含的資訊太多了,主要包含 class_rw_tretain/release/autorelease/retainCount 和 alloc 等資訊,很多存取方法也是圍繞它展開。檢視 objc-runtime-new.h 原始碼如下:

1
2
3
4
5
6
7
8
9
struct class_data_bits_t {

// Values are the FAST_ flags above.
uintptr_t bits;
class_rw_t* data() {
return (class_rw_t *)(bits & FAST_DATA_MASK);
}
... 省略其他方法
}

注意 objc_class 的 data 方法直接將 class_data_bits_t 的data 方法返回,最終是返回 class_rw_t,保了好幾層。

可以看到 class_data_bits_t 裡又包了一個 bits,這個指標跟不同的 FAST_ 字首的 flag 掩碼做按位與操作,就可以獲取不同的資料。bits 在記憶體中每個位的含義有三種排列順序:

32 位:

0 1 2 - 31
FAST_IS_SWIFT FAST_HAS_DEFAULT_RR FAST_DATA_MASK

64 位相容版:

0 1 2 3 - 46 47 - 63
FAST_IS_SWIFT FAST_HAS_DEFAULT_RR FAST_REQUIRES_RAW_ISA FAST_DATA_MASK 空閒

64 位不相容版:

0 1 2 3 - 46 47
FAST_IS_SWIFT FAST_REQUIRES_RAW_ISA FAST_HAS_CXX_DTOR FAST_DATA_MASK FAST_HAS_CXX_CTOR
48 49 50 51 52 - 63
FAST_HAS_DEFAULT_AWZ FAST_HAS_DEFAULT_RR FAST_ALLOC FAST_SHIFTED_SIZE_SHIFT 空閒

其中 64 位不相容版每個巨集對應的含義如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// class is a Swift class
#define FAST_IS_SWIFT (1UL<<0)
// class's instances requires raw isa
#define FAST_REQUIRES_RAW_ISA (1UL<<1)
// class or superclass has .cxx_destruct implementation
// This bit is aligned with isa_t->hasCxxDtor to save an instruction.
#define FAST_HAS_CXX_DTOR (1UL<<2)
// data pointer
#define FAST_DATA_MASK 0x00007ffffffffff8UL
// class or superclass has .cxx_construct implementation
#define FAST_HAS_CXX_CTOR (1UL<<47)
// class or superclass has default alloc/allocWithZone: implementation
// Note this is is stored in the metaclass.
#define FAST_HAS_DEFAULT_AWZ (1UL<<48)
// class or superclass has default retain/release/autorelease/retainCount/
// _tryRetain/_isDeallocating/retainWeakReference/allowsWeakReference
#define FAST_HAS_DEFAULT_RR (1UL<<49)
// summary bit for fast alloc path: !hasCxxCtor and
// !instancesRequireRawIsa and instanceSize fits into shiftedSize
#define FAST_ALLOC (1UL<<50)
// instance size in units of 16 bytes
// or 0 if the instance size is too big in this field
// This field must be LAST
#define FAST_SHIFTED_SIZE_SHIFT 51

這裡面除了 FAST_DATA_MASK 是用一段空間儲存資料外,其他巨集都是隻用 1 bit 儲存 bool 值。class_data_bits_t 提供了三個方法用於位操作:getBit,setBits 和 clearBits,對應到儲存 bool 值的掩碼也有封裝函式,比如:

1
2
3
4
5
6
7
bool isSwift() {
return getBit(FAST_IS_SWIFT);
}

void setIsSwift() {
setBits(FAST_IS_SWIFT);
}

重頭戲在於最大的那塊儲存區域–FAST_DATA_MASK,它其實就儲存了指向 class_rw_t 的指標:

1
2
3
class_rw_t* data() {
return (class_rw_t *)(bits & FAST_DATA_MASK);
}

對這片記憶體讀寫處於併發環境,但並不需要加鎖,因為會通過對一些狀態(realization or construction)判斷來決定是否可讀寫。

class_data_bits_t 甚至還包含了一些對 class_rw_t 中 flags 成員存取的封裝函式。

class_ro_t

objc_class 包含了 class_data_bits_tclass_data_bits_t 儲存了 class_rw_t 的指標,而 class_rw_t 結構體又包含 class_ro_t 的指標。

class_ro_t 中的 method_list_tivar_list_tproperty_list_t 結構體都繼承自 entsize_list_tt<Element, List, FlagMask>。結構為 xxx_list_t 的列表元素結構為 xxx_t,命名很工整。protocol_list_t 與前三個不同,它儲存的是 protocol_t * 指標列表,實現比較簡單。

entsize_list_tt 實現了 non-fragile 特性的陣列結構。假如蘋果在新版本的 SDK 中向 NSObject 類增加了一些內容,NSObject的佔據的記憶體區域會擴大,開發者以前編譯出的二進位制中的子類就會與新的 NSObject 記憶體有重疊部分。於是在編譯期會給 instanceStart 和 instanceSize 賦值,確定好編譯時每個類的所佔記憶體區域起始偏移量和大小,這樣只需將子類與基類的這兩個變數作對比即可知道子類是否與基類有重疊,如果有,也可知道子類需要挪多少偏移量。更多細節可以參考後面的章節 Non Fragile ivars。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
struct class_ro_t {
uint32_t flags;
uint32_t instanceStart;
uint32_t instanceSize;
#ifdef __LP64__
uint32_t reserved;
#endif

const uint8_t * ivarLayout;

const char * name;
method_list_t * baseMethodList;
protocol_list_t * baseProtocols;
const ivar_list_t * ivars;

const uint8_t * weakIvarLayout;
property_list_t *baseProperties;

method_list_t *baseMethods() const {
return baseMethodList;
}
};

class_ro_t->flags 儲存了很多在編譯時期就確定的類的資訊,也是 ABI 的一部分。下面這些 RO_ 字首的巨集標記了 flags 一些位置的含義。其中後三個並不需要被編譯器賦值,是預留給執行時載入和初始化類的標誌位,涉及到與 class_rw_t 的型別強轉。執行時會用到它做判斷,後面會講解。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#define RO_META               (1<<0) // class is a metaclass
#define RO_ROOT (1<<1) // class is a root class
#define RO_HAS_CXX_STRUCTORS (1<<2) // class has .cxx_construct/destruct implementations
// #define RO_HAS_LOAD_METHOD (1<<3) // class has +load implementation
#define RO_HIDDEN (1<<4) // class has visibility=hidden set
#define RO_EXCEPTION (1<<5) // class has attribute(objc_exception): OBJC_EHTYPE_$_ThisClass is non-weak
// #define RO_REUSE_ME (1<<6) // this bit is available for reassignment
#define RO_IS_ARC (1<<7) // class compiled with ARC
#define RO_HAS_CXX_DTOR_ONLY (1<<8) // class has .cxx_destruct but no .cxx_construct (with RO_HAS_CXX_STRUCTORS)
#define RO_HAS_WEAK_WITHOUT_ARC (1<<9) // class is not ARC but has ARC-style weak ivar layout

#define RO_FROM_BUNDLE (1<<29) // class is in an unloadable bundle - must never be set by compiler
#define RO_FUTURE (1<<30) // class is unrealized future class - must never be set by compiler
#define RO_REALIZED (1<<31) // class is realized - must never be set by compiler

class_rw_t

class_rw_t 提供了執行時對類拓展的能力,而 class_ro_t 儲存的大多是類在編譯時就已經確定的資訊。二者都存有類的方法、屬性(成員變數)、協議等資訊,不過儲存它們的列表實現方式不同。

class_rw_t 中使用的 method_array_tproperty_array_tprotocol_array_t 都繼承自 list_array_tt<Element, List>, 它可以不斷擴張,因為它可以儲存 list 指標,內容有三種:

  1. 一個 entsize_list_tt 指標
  2. entsize_list_tt 指標陣列

class_rw_t 的內容是可以在執行時被動態修改的,可以說執行時對類的拓展大都是儲存在這裡的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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;

Class firstSubclass;
Class nextSiblingClass;

char *demangledName;

#if SUPPORT_INDEXED_ISA
uint32_t index;
#endif
... 省略操作 flags 的相關方法
}

class_rw_t->flags 儲存的值並不是編輯器設定的,其中有些值可能將來會作為 ABI 的一部分。下面這些 RW_ 字首的巨集標記了 flags 一些位置的含義。這些 bool 值標記了類的一些狀態,涉及到宣告週期和記憶體管理。有些位目前甚至還空著。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#define RW_REALIZED           (1<<31) // class_t->data is class_rw_t, not class_ro_t
#define RW_FUTURE (1<<30) // class is unresolved future class
#define RW_INITIALIZED (1<<29) // class is initialized
#define RW_INITIALIZING (1<<28) // class is initializing
#define RW_COPIED_RO (1<<27) // class_rw_t->ro is heap copy of class_ro_t
#define RW_CONSTRUCTING (1<<26) // class allocated but not yet registered
#define RW_CONSTRUCTED (1<<25) // class allocated and registered
// #define RW_24 (1<<24) // available for use; was RW_FINALIZE_ON_MAIN_THREAD
#define RW_LOADED (1<<23) // class +load has been called
#if !SUPPORT_NONPOINTER_ISA
#define RW_INSTANCES_HAVE_ASSOCIATED_OBJECTS (1<<22) // class instances may have associative references
#endif
#define RW_HAS_INSTANCE_SPECIFIC_LAYOUT (1 << 21) // class has instance-specific GC layout
// #define RW_20 (1<<20) // available for use
#define RW_REALIZING (1<<19) // class has started realizing but not yet completed it
#define RW_HAS_CXX_CTOR (1<<18) // class or superclass has .cxx_construct implementation
#define RW_HAS_CXX_DTOR (1<<17) // class or superclass has .cxx_destruct implementation
// class or superclass has default alloc/allocWithZone: implementation
// Note this is is stored in the metaclass.
#define RW_HAS_DEFAULT_AWZ (1<<16)
#if SUPPORT_NONPOINTER_ISA
#define RW_REQUIRES_RAW_ISA (1<<15) // class's instances requires raw isa
#endif

demangledName 是計算機語言用於解決實體名稱唯一性的一種方法,做法是向名稱中新增一些型別資訊,用於從編譯器中向連結器傳遞更多語義資訊。

realizeClass

在某個類初始化之前,objc_class->data() 返回的指標指向的其實是個 class_ro_t 結構體。等到 static Class realizeClass(Class cls) 靜態方法在類第一次初始化時被呼叫,它會開闢 class_rw_t 的空間,並將 class_ro_t 指標賦值給 class_rw_t->ro。這種偷天換日的行為是靠 RO_FUTURE 標誌位來記錄的:

1
2
3
4
5
6
7
8
9
10
11
12
13
ro = (const class_ro_t *)cls->data();
if (ro->flags & RO_FUTURE) {
// This was a future class. rw data is already allocated.
rw = cls->data();
ro = cls->data()->ro;
cls->changeInfo(RW_REALIZED|RW_REALIZING, RW_FUTURE);
} else {
// Normal class. Allocate writeable class data.
rw = (class_rw_t *)calloc(sizeof(class_rw_t), 1);
rw->ro = ro;
rw->flags = RW_REALIZED|RW_REALIZING;
cls->setData(rw);
}

注意之前 RO 和 RW flags 巨集標記的一個細節:

1
2
3
4
5
#define RO_FUTURE             (1<<30)
#define RO_REALIZED (1<<31)

#define RW_REALIZED (1<<31)
#define RW_FUTURE (1<<30)

也就是說 ro = (const class_ro_t *)cls->data(); 這種強轉對於接下來的 ro->flags & RO_FUTURE 操作完全是 OK 的,兩種結構體第一個成員都是 flagsRO_FUTURE 與 RW_FUTURE 值一樣的。

經過 realizeClass 函式處理的類才是『真正的』類,呼叫它時不能對類做寫操作。

Category

Category 為現有的類提供了拓展性,它是 category_t 結構體的指標。

1
typedef struct category_t *Category;

category_t 儲存了類別中可以拓展的例項方法、類方法、協議、例項屬性和類屬性。類屬性是 Objective-C 2016 年新增的特性,沾 Swift 的光。所以 category_t 中有些成員變數是為了相容 Swift 的特性,Objective-C 暫沒提供介面,僅做了底層資料結構上的相容。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
struct category_t {
const char *name;
classref_t cls;
struct method_list_t *instanceMethods;
struct method_list_t *classMethods;
struct protocol_list_t *protocols;
struct property_list_t *instanceProperties;
// Fields below this point are not always present on disk.
struct property_list_t *_classProperties;

method_list_t *methodsForMeta(bool isMeta) {
if (isMeta) return classMethods;
else return instanceMethods;
}

property_list_t *propertiesForMeta(bool isMeta, struct header_info *hi);
};

在 App 啟動載入映象檔案時,會在 _read_images