深入淺出Objective-C runtime
1. 前言
1.1. 什麼是Objective-C?
1.1.1. 概念
Objective-C是一種通用、高階、面向物件的程式語言。它擴充套件了標準的ANSI C程式語言,將Smalltalk式的訊息傳遞機制加入到ANSI C中。目前主要支援的編譯器有GCC和Clang(採用LLVM作為後端)。
Objective-C的商標權屬於蘋果公司,蘋果公司也是這個程式語言的主要開發者。蘋果在開發NeXTSTEP作業系統時使用了Objective-C,之後被OS X和iOS繼承下來。現在Objective-C與Swift是OS X和iOS作業系統、及與其相關的API、Cocoa和Cocoa Touch的主要程式語言。
(From 維基百科)
簡而言之,Objective-C是C的超集。而與C語言不同的是,雖然Objective-C關於C的部分是靜態的,但是關於面向物件的部分是動態的。所謂的靜態,指的是在編譯期間所有的函式、結構體等都確定好了記憶體地址,呼叫行為都被解析為記憶體地址+偏移量。而動態指的是,程式碼的合法性會延遲到執行的過程中校驗,呼叫行為會被解析為呼叫底層語言的介面。
1.1.2. 編譯過程
在Apple官方IDE Xcode中,其編譯的過程,可以簡單的理解為編譯器前端Clang先將Objective-C原始碼預處理成C/C++原始碼,再接下去進行編譯成IR的過程。可以在Terminal中使用Clang檢視Objective-C原始碼的編譯流程。如下所示:
Shell
$ # 假設Objective-C原始檔為main.m, 生成的C++原始檔則為同目錄下的main.cpp
$ clang -ccc-print-phases main.m
+- 0: input, "main.m", objective-c
+- 1: preprocessor, {0}, objective-c-cpp-output
+- 2: compiler, {1}, ir
+- 3: backend, {2}, assembler
+- 4: assembler, {3}, object
+- 5: linker, {4}, image
6: bind-arch, "x86_64", {5}, image
也可以使用Clang將Objective-C原始碼翻譯成C/C++原始碼。如下所示:
Shell
$ # 假設Objective-C原始檔為main.m, 生成的C++原始檔則為同目錄下的main.cpp
$ clang -rewrite-objc main.m
1.2. 什麼是runtime?
1.2.1. 概念
執行時期(Run time)在電腦科學中代表一個計算機程式從開始執行到終止執行的運作、執行的時期。與執行時期相對的其他時期包括:設計時期(design time)、編譯時期(compile time)、連結時期(link time)、與載入時期(load time)。
(From 維基百科)
簡而言之,runtime是計算機程式正在執行中的狀態。而我們集中關注的是Objective-C程式中在runtime裡的語言特性及實現原理。
1.2.2. Objective-C runtime
The Objective-C runtime is a runtime library that provides support for the dynamic properties of the Objective-C language, and as such is linked to by all Objective-C apps. Objective-C runtime library support functions are implemented in the shared library found at /usr/lib/libobjc.A.dylib.
You typically don't need to use the Objective-C runtime library directly when programming in Objective-C. This API is useful primarily for developing bridge layers between Objective-C and other languages, or for low-level debugging.
(From Apple)
簡單翻譯一下,Objective-C runtime是Objective-C這門語言為了支援語言的動態特性而催生出的底層動態連結庫。它提供的底層API能比較方便地與其他語言進行互動。
雖然Objective-C自身是開源的,但是支援其動態語言特性的runtime庫卻有不同的實現版本。除了Apple官方對macOS量身定製的runtime庫,GNU也開源了一份相同API的runtime庫。
如果想要使用Xcode除錯Objective-C runtime原始碼,可以參考Objective-C runtime 原始碼除錯。
接下來關於Objective-C runtime的剖析全部基於Apple開源的objc4-838.1。但是,由於不同的CPU架構對應的runtime原始碼實現有所不同(原始碼中通過巨集的方式來區分),為了簡化這部分的敘述,故以x86-64為例。
本文除錯環境
Mac機器配置:
- macOS Monterey (macOS 12)
- Intel® Core™ i7-9750H
Xcode配置:
- Version 13.2.1 (13C100)
- objc4-838.1
PAY ATTENTION
- 為了方便講解,對部分原始碼做了一定的改動,但不影響其主要邏輯
- 以下內容需要有一定的Objective-C和C/C++基礎
2. 剖析Objective-C的面向物件
在Objective-C中,有兩大基類NSObject
和NSProxy
,而NSObject也作為唯一的基協議。其他所有Objective-C的類都繼承自NSObject
或NSProxy
,並遵守NSObject
協議。NSObject
是日常研發中經常使用的基類,所以,我們接下來重點需要探究的是Objective-C面向物件的實現以及其與NSObject
的聯絡。
2.1. 面向物件的實現
要想搞清楚Objective-C是如何實現面向物件的,首要任務是剖析NSObject
。先給出NSObject
的實現原始碼:
```C++ typedef struct objc_class Class; typedef struct objc_object id;
union isa_t { uintptr_t bits; Class cls; struct { uintptr_t nonpointer : 1; uintptr_t has_assoc : 1; uintptr_t has_cxx_dtor : 1; uintptr_t shiftcls : 44; uintptr_t magic : 6; uintptr_t weakly_referenced : 1; uintptr_t unused : 1; uintptr_t has_sidetable_rc : 1; uintptr_t extra_rc : 8; }; };
struct objc_object { isa_t isa; };
struct objc_class : objc_object { Class superclass; cache_t cache; class_data_bits_t bits; };
@interface NSObject
```
將原始碼簡單轉化為UML類圖如下:
從原始碼不難看出,NSObject
本質上是objc_class
結構體。而在objc_class
結構體中,除了繼承得來的isa
變數,通過變數的命名,我們也可以輕易知道obj_class
還包含父類指標、快取和類資料。我們日常使用例項物件的id
型別和類物件的Class
型別,實質是一個指向objc_object
、objc_class
結構體的指標型別。舉個簡單的例子:
Objective-C
id instanceObj = [NSObject new];
這個簡單的Objective-C建立例項物件的程式碼中的instanceObj
實際上是一個指向objc_object
結構體的指標,通過被指向的記憶體空間中的objc_object
結構體中的成員變數isa
能獲得NSObject
類物件的objc_class
結構體的記憶體地址。注意到我這裡提到了NSObject
類物件,其實還有一個NSObject
元類物件。這裡先給出Objective-C對於面向物件的完整實現原理圖:
從圖中不難看出,無論是根類還是子類,都有分為類物件和元類物件。那問題來了,為什麼要區分出類物件和元類物件呢?我們先看這樣一個例子:
```Objective-C // define FooClass @interface FooClass : NSObject + (void)sayHi; - (void)sayHi; @end
@implementation FooClass + (void)sayHi { NSLog(@"+ FooClass: Hi"); }
-(void)sayHi { NSLog(@"- FooClass: Hi"); } @end
// some other function FooClass *foo = [FooClass new]; [foo sayHi]; [FooClass sayHi]; ```
在這個例子中,19行呼叫的是例項方法,20行呼叫的是類方法。之前有提到過,實際上Objective-C呼叫方法會的程式碼實際上會改寫為呼叫runtime的API,這兩個方法呼叫都會改寫為以下程式碼:
Objective-C
objc_msgSend(foo, @selector(sayHi));
objc_msgSend((id)objc_getClass("FooClass"), @selector(sayHi));
objc_getClass("FooClass")
這個方法會返回一個Class
型別,通過被指向記憶體空間中的objc_class
結構體中的成員變數isa
能獲得FooClass
元類物件的objc_class
結構體的記憶體地址。通過這樣的邏輯,當objc_msgSend
的第一個入參為例項物件指標時,就能找到類物件,並呼叫對應的方法;當objc_msgSend
的第一個入參為類物件指標時,就能找到元類物件,並呼叫對應的方法。這樣,在Objective-C的任何方法呼叫上,都能統一由objc_msgSend
收斂。並且,在Objective-C的實現中,也會將例項方法存放在類物件的objc_class
結構體內,而將類方法存放在元類物件的objc_class
結構體內。這樣,開發者就能輕鬆的呼叫例項方法或者類方法了。
2.2. isa
“指標”
講完了NSObject
以及Objective-C
在面向物件上的大致實現,接下來我們細緻分析一下isa
“指標”。注意到我這裡打上了雙引號,也就是意味著isa
並不僅僅是一個指標。isa
的型別為isa_t
聯合體。雖然現在的CPU對記憶體的定址空間達到了64位之多,“理論上”能支援2^64位元組的記憶體,但是實際上我們實體記憶體遠遠達不到這個量級,所以實際上64位編譯環境的C/C++指標型別是“非常”浪費空間的。為了達到極致的記憶體控制,isa_t
除了儲存了記憶體地址,還儲存了額外的資訊。
這裡先給出對應位元代表具體的含義:
nonpointer
(1)
是否為非純指標(即有無附帶額外資訊),當為0時,isa
為純指標;當為1時,isa
並非純指標。
has\_assoc
(1)
當前物件是否具有或者曾經具有關聯物件(與runtime API的objc_setAssociatedObject
等有關)。
has_cxx_dtor
(1)
當前物件是否具有Objective-C或者C++的析構器。
shiftcls
(43)
儲存指向的objc_class
結構體記憶體地址。
magic
(6)
用於偵錯程式判斷當前物件是真的物件還是沒有初始化的空間。在x86-64中,判斷其是否等於0x3B(0b111011)。
weakly_referenced
(1)
當前物件是否被弱引用指標指向或者曾經被弱引用指標指向。
unused
(1)
當前物件是否已經廢棄(正在釋放記憶體)。
has_sidetable_rc
(1)
當前物件的引用計數是否由散列表記錄(當引用計數過大時,由額外的散列表儲存)
extra_rc
(8)
存放當前物件的引用計數(當溢位時,由額外的散列表儲存,此時將has_sidetable_rc
置為1)
objc_object
結構體的成員函式具有isa
的初始化函式:
```C++
define ISA_MAGIC_VALUE 0x001d800000000001ULL
struct objc_object { isa_t isa;
void initInstanceIsa(Class cls, bool hasCxxDtor);
void initIsa(Class newCls, bool nonpointer, bool hasCxxDtor);
};
union isa_t { Class cls;
void setClass(Class newCls, objc_object *obj);
};
inline void objc_object::initInstanceIsa(Class cls, bool hasCxxDtor) { initIsa(cls, true, hasCxxDtor); }
inline void objc_object::initIsa(Class cls, bool nonpointer, bool hasCxxDtor) { isa_t newisa(0); if (!nonpointer) { newisa.setClass(cls, this); } else { newisa.bits = ISA_MAGIC_VALUE; newisa.has_cxx_dtor = hasCxxDtor; newisa.setClass(cls, this); newisa.extra_rc = 1; } isa = newisa; }
inline void isa_t::setClass(Class newCls, objc_object *obj) { shiftcls = (uintptr_t)newCls >> 3; } ```
根據這段原始碼,isa
初始化的操作是分別將nonpointer
、extra_rc
置為1,magic
置為0x3B(0b111011),設定has_cxx_dtor
和shiftcls
。注意到第31行的setClass
函式,對shiftcls
賦值為newCls
右移3位。那問題來了,為什麼要右移3位呢?其實在C/C++語言中,結構體會做記憶體對齊,所以在64位系統中的結構體的記憶體地址的末三位為0。雖然macOS在x86-64上記憶體定址空間為0x7fffffe00000(約為128TB),但僅需43位即可儲存需要的記憶體地址資訊。
而同樣的,要想從isa
中獲取Class
指標,僅需shiftcls
的內容。原始碼實現如下:
```C++
define ISA_MASK 0x00007ffffffffff8ULL
union isa_t { Class cls;
Class getClass(bool authenticated);
};
inline Class isa_t::getClass(bool authenticated) { uintptr_t clsbits = bits; clsbits &= ISA_MASK; return (Class)clsbits; } ```
實際上,並不是所有的例項物件都有isa
“指標”。Apple早在WWDC 2013就提出了Tagged Pointers技術,在64位機器上將資料巧妙地“儲存”到例項物件的“指標”內,所以這些例項物件也可以簡單理解為“偽物件”。由於這些“偽物件”本身不是指標型別,所以也沒有objc_object
結構,自然也沒有isa
“指標”。為了方便敘述,全篇都將不會討論Tagged Pointers,所有例項物件預設具備objc_object
結構。
對Tagged Pointers感興趣的同學可以參考以下連結:
- https://blog.devtang.com/2014/05/30/understand-tagged-pointer/
- https://developer.apple.com/videos/play/wwdc2020/10163/?time=893
2.3. 資料儲存bits
2.3.1. 資料儲存結構
接下來,重點講解的是class_data_bits_t
。class_data_bits_t
也是一個結構體,儲存了方法、屬性、協議、例項變數佈局等資料。先給出它的原始碼:
```C++
define FAST_DATA_MASK 0x00007ffffffffff8UL
define RW_REALIZED (1<<31)
struct explicit_atomic : public std::atomic
struct class_data_bits_t { friend objc_class;
uintptr_t bits;
class_rw_t *data() const {
return (class_rw_t *)(bits & FAST_DATA_MASK);
}
const class_ro_t *safe_ro() const {
class_rw_t *maybe_rw = data();
if (maybe_rw->flags & RW_REALIZED) {
return maybe_rw->ro();
} else {
return (class_ro_t *)maybe_rw;
}
}
};
struct class_rw_t {
uint32_t flags;
uint16_t witness;
explicit_atomic
Class firstSubclass;
Class nextSiblingClass;
using ro_or_rw_ext_t = PointerUnion<const class_ro_t, class_rw_ext_t, PTRAUTH_STR("class_ro_t"), PTRAUTH_STR("class_rw_ext_t")>;
const ro_or_rw_ext_t get_ro_or_rwe() const { return ro_or_rw_ext_t{ro_or_rw_ext}; }
class_rw_ext_t *ext() const { return get_ro_or_rwe().dyn_cast<class_rw_ext_t *>(&ro_or_rw_ext); }
const class_ro_t *ro() const {
auto v = get_ro_or_rwe();
if (v.is<class_rw_ext_t *>()) {
return v.get<class_rw_ext_t *>(&ro_or_rw_ext)->ro;
}
return v.get<const class_ro_t *>(&ro_or_rw_ext);
}
const method_array_t methods() const {
auto v = get_ro_or_rwe();
if (v.is<class_rw_ext_t *>()) {
return v.get<class_rw_ext_t *>(&ro_or_rw_ext)->methods;
} else {
return method_array_t{v.get<const class_ro_t *>(&ro_or_rw_ext)->baseMethods};
}
}
const property_array_t properties() const {
auto v = get_ro_or_rwe();
if (v.is<class_rw_ext_t *>()) {
return v.get<class_rw_ext_t *>(&ro_or_rw_ext)->properties;
} else {
return property_array_t{v.get<const class_ro_t *>(&ro_or_rw_ext)->baseProperties};
}
}
const protocol_array_t protocols() const {
auto v = get_ro_or_rwe();
if (v.is<class_rw_ext_t *>()) {
return v.get<class_rw_ext_t *>(&ro_or_rw_ext)->protocols;
} else {
return protocol_array_t{v.get<const class_ro_t *>(&ro_or_rw_ext)->baseProtocols};
}
}
};
struct class_rw_ext_t {
DECLARE_AUTHED_PTR_TEMPLATE(class_ro_t)
class_ro_t_authed_ptr
method_array_t methods;
property_array_t properties;
protocol_array_t protocols;
char *demangledName;
uint32_t version;
};
struct class_ro_t {
uint32_t flags;
uint32_t instanceStart;
uint32_t instanceSize;
uint32_t reserved;
union {
const uint8_t *ivarLayout;
Class nonMetaclass;
};
explicit_atomic<const char *> name;
WrappedPtr<method_list_t, method_list_t::Ptrauth> baseMethods;
protocol_list_t *baseProtocols;
const ivar_list_t *ivars;
const uint8_t *weakIvarLayout;
property_list_t *baseProperties;
_objc_swiftMetadataInitializer __ptrauth_objc_method_list_imp _swiftMetadataInitializer_NEVER_USE[0];
}; ```
簡單轉化為UML類圖如下:
不難看出,class_data_bits_t
結構體能獲取class_rw_t
和class_ro_t
結構體指標。而class_ro_t
結構體指標實際上是class_rw_ext_t
結構體的成員變數,而class_rw_ext_t
結構體指標實際上是class_rw_t
結構體的成員變數。這裡面引入了三個重要的結構體:class_rw_t
、class_rw_ext_t
和class_ro_t
。這裡簡單解釋一下,rw代表的是read-write,即可讀寫;ext代表的是extension,即拓展;ro代表的是read-only,即只讀。所以顧名思義,class_rw_t
儲存了讀寫資料,class_ro_t
儲存了只讀資料,而class_rw_ext_t
儲存了class_rw_t
的拓展資料。
那問題來了,為什麼搞了三個不同的資料結構呢(早些年Apple的實現其實不是如此)?其實,這是Apple為了節約記憶體做出的改變。在WWDC 2020中,專門有一期視訊講解了Apple在2020對Objective-C runtime上的改變——Advancements in the Objective-C runtime。
簡單的總結一下,Apple將記憶體分為兩類,一類是Dirty Memory,指的是在程序的執行中需要一直存在於記憶體中,也就是說程序在執行的過程中會對Dirty Memory進行讀寫操作;另一類是Clean Memory,指的是在程序的執行過程中不需要一直存在於記憶體中,也就是說程序在執行的過程中並不會對Clean Memory進行寫操作,也就是說Clean Memory是隻讀的。這樣一來,當記憶體緊張時可以丟棄Clean Memory,當有讀需求的時候再從硬碟中載入到記憶體中。這樣的設計尤其對iOS友好,眾所周知,iOS並沒有macOS中記憶體swap能力,所以優先使用Clean Memory是WWDC 2020對Objective-C runtime的一個重大改進。基於此,Apple對於class_rw_t
、class_rw_ext_t
和class_ro_t
這三個結構體的儲存方式是這樣設計的:
- 編譯成二進位制產物存在硬碟(Flash、SSD、HDD)
在編譯的過程中,自定義類的方法、協議、例項變數、屬性都是確定的,所以僅需要class_ro_t
結構體。
- 程序初次執行
程序初期執行時,會呼叫Objective-C runtime的初始化入口_objc_init
,將objec_class
和class_ro_t
載入到記憶體中。
- 首次呼叫
類被首次呼叫時,將在記憶體中建立class_rw_t
。
- 程序執行時動態新增資料
在程序執行時動態新增方法、屬性、協議等時,再建立class_rw_ext_t
來儲存執行時新增的資料。
2.3.2. 例項變數的儲存實現
為了瞭解例項變數在Objective-C中是如何實現的,我們先寫個簡單的例子:
```Objective-C @interface FooClass : NSObject
@property (nonatomic, strong) id someObject;
@end
@implementation FooClass
@end ```
這個例子中,我們定義了一個NSObject
的子類FooClass
,並且具有一個屬性someObject
。我們知道,在Objective-C中,如果我們要使用直接使用someObject
的例項變數,可以直接在FooClass
的方法中直接呼叫_someObject
。那為什麼可以這麼做呢?我們使用Clang將這段Objective-C原始碼翻譯成C/C++:
```C++ typedef struct objc_object NSObject; struct NSObject_IMPL { Class isa; };
typedef struct objc_object FooClass; struct FooClass_IMPL { struct NSObject_IMPL NSObject_IVARS; id _someObject; }; ```
其實翻譯後的C/C++接近十萬餘行,故只關注我們關心的FooClass
的例項變數實現。可以看到,實際上FooClass_IMPL
結構體才是FooClass
例項物件的實現,並且在FooClass_IMPL
結構體中,_someObject
是其成員變數。
至此,答案呼之欲出了,我們在FooClass
的方法中直接呼叫_someObject
實際上是編譯器將_someObject
硬編碼成記憶體偏移量(原理等同於在結構體方法中呼叫成員變數)。
這時候,問題又來了,我們在FooClass
的方法除了直接呼叫_someObject
外,還可以使用點方法self.someObject
或者getter方法[self someObject]
來獲取例項變數對應的值。getter方法沒什麼好說的,實際上它就是在getter方法中使用硬編碼記憶體偏移量的形式來獲取例項變數的(重寫getter方法的話就不一定如此了)。而點方法不同,如果對Objective-C稍微有點了解就知道,實際上點方法依賴於KVC(Key Value Coding),它首先會在方法列表中遍歷查詢getter或setter方法,假如沒有查詢到,就在例項變數列表中遍歷查詢對應的例項變數。再給出個簡單的例子:
```Objective-C @interface FooClass : NSObject { id _someObject; }
@end
@implementation FooClass
- (void)foo { id obj = self.someObject; id objx = [self valueForKey:@"someObject"]; }
@end ```
這裡例子中,obj
與objx
的賦值實際上都依賴於KVC。剛剛說到例項變數列表,那什麼是例項變數列表呢?而且我們剛剛也一直在強調硬編碼記憶體偏移量,意思是還存在“軟編碼”記憶體偏移量嗎?
直接給出答案,class_ro_t
中的ivars
儲存了所有例項變數的名稱、大小與記憶體偏移量等資訊。先看看ivars
的定義:
```C++ struct class_ro_t { // const ivar_list_t ivars; /*/ };
typedef struct ivar_t *Ivar;
struct ivar_t { int32_t offset; const char name; const char *type; uint32_t alignment_raw; uint32_t size;
uint32_t alignment() const {
if (alignment_raw == ~(uint32_t)0) return 1U << WORD_SHIFT;
return 1 << alignment_raw;
}
};
struct ivar_list_t : entsize_list_tt
簡單轉化為UML類圖如下:
(這裡忽略了entsize_list_tt
結構體的實現,簡單說來,它一共有兩個成員變數:entsizeAndFlags
記錄陣列單個item的大小,可能附帶flag資訊;count
記錄陣列的item
數量。從前64位起,後面才真正儲存了陣列資料。)
ivar實際上是instance variable的縮寫,顧名思義,ivar_list_t
是例項變數陣列,ivar_t
是例項變數。不難看出,ivar_t
依次儲存了例項變數的記憶體偏移量、名稱、型別、記憶體對齊方式和大小。於是,如果想要在執行時實現動態訪問例項變數,僅需要通過名稱等資訊查詢到對應的ivar_t
,從而找到其記憶體偏移量,再加上例項物件記憶體地址即可。
如果我們有一定的Objective-C的開發經驗,一定知道兩件事情:
- 無法給已經編譯好的類新增extension
- 無法在category中新增例項變數
其實,這兩件事情都表達了一個意思,無法改變已經編譯好的類的記憶體佈局。這裡簡單講解一下,以新增例項變數為例,我們知道例項變數列表僅存在於class_ro_t
中,要想實現新增例項變數的操作,就要讓class_ro_t
實現寫入操作。其實,在Objective-C的runtime API中,提供有新增例項變數的方法class_addIvar
。先給出一個簡單的例子:
```Objective-C @interface FooClass : NSObject { NSString *_name; }
@end
@implementation FooClass
@end
// some other function FooClass *foo = [FooClass new]; [foo setValue:@"foo" forKey:@"name"]; NSLog(@"foo.name = %@", [foo valueForKey:@"name"]); ```
那我們該如何在執行時中動態新建FooClass
並且新增例項變數_someObject
呢?如下所示:
```Objective-C // some other function Class FooClass = objc_allocateClassPair([NSObject class], "FooClass", 0); class_addIvar(FooClass, "_name", sizeof(NSString ), log2(sizeof(NSString )), @encode(NSString *)); objc_registerClassPair(FooClass);
id foo = [FooClass new]; [foo setValue:@"foo" forKey:@"name"]; NSLog(@"foo.name = %@", [foo valueForKey:@"name"]); ```
這裡,我們先用objc_allocateClassPair
建立了NSObject
的子類,並獲取了對應的類物件FooClass
(objc_allocateClassPair
的命名也是有講究的,classPair,意思就是建立了類對,表面return的是類物件,實則元類物件也同時建立好,並被指向於類物件的isa
)。接著使用class_addIvar
新增例項變數,最後用objc_registerClassPair
完成FooClass
的註冊。(其中@encode
的作用為將型別轉化為字串編碼,具體對應關係可以參考Apple的Type Encodings,這裡不做過多的贅述。)
至此,我們已然成功使用runtime的API實現執行時動態新增例項變數。再回到上面的問題,那又是為什麼編譯好的類無法使用category或者extension新增例項變數呢?或者說,我們可以給編譯好的類呼叫class_addIvar
完成新增例項變數的操作嗎?答案當然是否定的,我們可以試一下給NSObject
新增例項變數:
Objective-C
// some other function
BOOL isSuccess = class_addIvar([NSObject class], "_name", sizeof(NSString *), log2(sizeof(NSString *)), @encode(NSString *));
if (isSuccess) {
NSLog(@"NSObject can add ivar at runtime");
} else {
NSLog(@"NSObject can't add ivar at runtime");
}
執行這段程式碼,我們能從控制檯中獲得答案:NSObject can't add ivar at runtime。這是又是為什麼呢?其實Apple的官方文件已經說的很清楚了:
This function may only be called after objc_allocateClassPair and before objc_registerClassPair. Adding an instance variable to an existing class is not supported.
(From Apple)
在objc_registerClassPair
註冊類之後,class_ro_t
將徹底成為一個只讀結構體,禁止任何試圖修改class_ro_t
成員變數的行為。其實,在class_addIvar
的實現中,我們也能看出端倪:
```C++
define RW_CONSTRUCTING (1<<26)
define UINT32_MAX 4294967295U
BOOL class_addIvar(Class cls, const char name, size_t size, uint8_t alignment, const char type) { if (!cls) return NO;
if (!type) type = "";
if (name && 0 == strcmp(name, "")) name = nil;
checkIsKnownClass(cls);
ASSERT(cls->isRealized());
// No class variables
if (cls->isMetaClass()) {
return NO;
}
// Can only add ivars to in-construction classes.
if (!(cls->data()->flags & RW_CONSTRUCTING)) {
return NO;
}
// Check for existing ivar with this name, unless it's anonymous.
// Check for too-big ivar.
if ((name && getIvar(cls, name)) || size > UINT32_MAX) {
return NO;
}
class_ro_t *ro_w = make_ro_writeable(cls->data());
ivar_list_t *oldlist, *newlist;
if ((oldlist = (ivar_list_t *)cls->data()->ro()->ivars)) {
size_t oldsize = oldlist->byteSize();
newlist = (ivar_list_t *)calloc(oldsize + oldlist->entsize(), 1);
memcpy(newlist, oldlist, oldsize);
free(oldlist);
} else {
newlist = (ivar_list_t *)calloc(ivar_list_t::byteSize(sizeof(ivar_t), 1), 1);
newlist->entsizeAndFlags = (uint32_t)sizeof(ivar_t);
}
uint32_t offset = cls->unalignedInstanceSize();
uint32_t alignMask = (1<<alignment)-1;
offset = (offset + alignMask) & ~alignMask;
ivar_t& ivar = newlist->get(newlist->count++);
ivar.offset = (int32_t *)(int64_t *)calloc(sizeof(int64_t), 1);
*ivar.offset = offset;
ivar.name = name ? strdupIfMutable(name) : nil;
ivar.type = strdupIfMutable(type);
ivar.alignment_raw = alignment;
ivar.size = (uint32_t)size;
ro_w->ivars = newlist;
cls->setInstanceSize((uint32_t)(offset + size));
return YES;
} ```
第10-11和第18-21行的判斷是為了確保當前類處於構造中狀態,即已呼叫objc_allocateClassPair
,且未呼叫objc_registerClassPair
。第13-16行的判斷是為了確保當前類非元類物件,即無法為元類物件新增例項變數。而已經完成編譯的類,在進行判斷!(cls->data()->flags & RW\_CONSTRUCTING)
時為true
,導致無法執行到後續的新增ivar
的邏輯。換句話說,完成編譯的類已經處於已構造完成並完成註冊的狀態,即可以視為已呼叫了objc_registerClassPair
,故無法在執行時動態新增例項變數。實際上,也不難理解,假如能在執行時新增例項變數,那必定會改變例項物件的記憶體佈局,而先前的已經建立的例項變數的記憶體佈局無法隨之改變,則必將為後續的程式執行帶來無法預測的安全隱患。
2.3.3. 方法的儲存實現
與ivars
的儲存實現類似,方法也是由陣列的結構進行儲存。不同的是,編譯時確定的方法儲存在class_ro_t
結構體中,執行時動態新增的方法儲存在class_rw_ext_t
結構體中,分別對應baseMethods
和methods
。baseMethods
為method_list_t
型別,其實就是將方法型別method_t
結構體組織成陣列進行儲存,原理類似2.3.2.講解的ivar_list_t
陣列儲存實現。而methods
為method_array_t
型別,是將方法列表型別method_list_t
組織成陣列進行儲存,實現原理也比較簡單,在此不做過多贅述。接下來,我們重點分析method_t
結構體:
```C++ struct method_t { struct big { SEL name; const char *types; IMP imp; };
struct small {
RelativePointer<const void *> name;
RelativePointer<const char *> types;
RelativePointer<IMP, false> imp;
};
bool isSmall() const {
return ((uintptr_t)this & 1) == 1;
}
small &small() const {
ASSERT(isSmall());
return *(struct small *)((uintptr_t)this & ~(uintptr_t)1);
}
big &big() const {
ASSERT(!isSmall());
return *(struct big *)this;
}
ALWAYS_INLINE SEL name() const {
if (isSmall()) {
if (small().inSharedCache()) {
return (SEL)small().name.get(sharedCacheRelativeMethodBase());
} else {
return *(SEL *)small().name.get();
}
} else {
return big().name;
}
}
const char *types() const {
return isSmall() ? small().types.get() : big().types;
}
IMP imp(bool needsLock) const {
return isSmall() ? small().imp.get() : big().imp;
}
static uintptr_t sharedCacheRelativeMethodBase() {
return (uintptr_t)@selector(🤯);
}
}
template
void *getRaw(uintptr_t base) const {
if (isNullable && offset == 0)
return nullptr;
uintptr_t signExtendedOffset = (uintptr_t)(intptr_t)offset;
uintptr_t pointer = base + signExtendedOffset;
return (void *)pointer;
}
void *getRaw() const {
return getRaw((uintptr_t)&offset);
}
T get(uintptr_t base) const {
return (T)getRaw(base);
}
T get() const {
return (T)getRaw();
}
}; ```
簡單轉化為UML類圖如下:
不難看出,method_t
結構體實際上存在兩種不同的實現,一種實現為method_t::big
結構體,另一種實現為method_t::small
結構體,為了方便討論,我們稱其為method_big
結構體與method_small
結構體。那問題來了,二者在結構上看起來沒啥區別,一樣儲存了name
、types
和imp
,為什麼要區分成兩種不同的版本呢?
其實,在上面提到的Apple在WWDC 2020釋出的視訊Advancements in the Objective-C runtime就有對此的講解。這裡簡單總結一下,實際上顧名思義,method_big
結構體代表了記憶體佔用大的實現版本(以前的實現其實就是method_big
結構體版本),method_small
結構體代表了記憶體佔用小的實現版本。我們知道,一個method_t
結構體需要儲存的有三條重要資訊,根據method_big
結構體的實現,我們知道儲存的三個資訊都為指標型別,故在64位系統中一個method_t
結構體就需要佔用24個位元組。而在method_small
結構體中,我們發現它儲存的並不是指標型別,而是記憶體的偏移量,並且型別為int32_t
,所以在method_small
結構體僅佔用12個位元組,比起method_big
結構體真正縮小了一半的記憶體空間。那麼問題來了,為什麼可以這麼做?
如圖,很直觀的可以知道,假如使用method_big
結構體,因為dyld將二進位制檔案對映到記憶體的位置都是隨機的,所以每次對映都需要修正method_big
結構體的指標指向。
假如使用method_small
結構體,dyld可以直接把method_small
結構體進行對映,而不需要額外的修正操作。這是因為dylib中變數的記憶體位置總是“相鄰”的,即相對於一個變數,另一個變數的記憶體偏移量總在-2GB~+2GB之間,而這個偏移量在編譯時就已經確定了,並且在dyld對dylib進行載入及對映到記憶體的過程中並不會改變這個偏移量,或者說變數間的相對位置是不變的,所以,實際上dylib上的method_t
結構體並不需要完整的64位資料(整個指標)來索引到相關的資料,僅需記錄32位的偏移量資料即可索引到相關資料。
綜上可知,method_small
結構體相比起method_big
結構體,在記憶體佔用上更小,載入的時間代價也更小。但method_big
結構體的優勢在於更加靈活,記憶體索引空間也更大。所以,dylib會盡量優化為method_small
結構體,而在執行時可能需要動態修改method_t
的可執行檔案仍會採用method_big
結構體。
有些人可能注意到method_small
結構體在獲取SEL
(name
)的時候,進行了inSharedCache()
判斷。這個與dyld的共享快取有關,具體可以參考Apple在WWDC 2017釋出的視訊——App Startup Time: Past, Present, and Future,這裡不展開講解。
2.4. 方法快取cache
2.4.1. 快取結構
在實踐中,一個類的方法往往只有部分是會經常被呼叫的,如果所有方法都需要到方法列表裡面去查詢(方法列表的實現是陣列,通過遍歷列表來實現查詢),那麼就會造成效率低下。所以,Objective-C在實現的裡就考慮使用快取來儲存經常呼叫的方法。而cache_t
結構體儲存了方法快取:
```C++ typedef uint32_t mask_t;
struct cache_t {
explicit_atomic
mask_t mask() const;
struct bucket_t *buckets() const;
unsigned capacity() const;
mask_t occupied() const;
}
struct bucket_t {
explicit_atomic
inline SEL sel() const { return _sel.load(memory_order_relaxed); }
inline IMP imp(UNUSED_WITHOUT_PTRAUTH bucket_t *base, Class cls) const {
uintptr_t imp = _imp.load(memory_order_relaxed);
if (!imp) return nil;
return (IMP)(imp ^ (uintptr_t)cls);
}
}
mask_t cache_t::mask() const { return _maybeMask.load(memory_order_relaxed); }
struct bucket_t cache_t::buckets() const { uintptr_t addr = _bucketsAndMaybeMask.load(memory_order_relaxed); return (bucket_t )(addr & bucketsMask); }
unsigned cache_t::capacity() const { return mask() ? mask()+1 : 0; }
mask_t cache_t::occupied() const { return _occupied; } ```
簡單轉化為UML類圖如下:
cache_t
結構體其實很簡單,通過buckets()
得到快取散列表(雜湊表);通過capacity()
得到快取散列表的總容量;通過occupied()
得到快取散列表有多少個被佔用的bucket
。而bucket_t
結構體儲存了方法選擇器SEL
和對應的函式指標IMP
。
這裡我們注意到,bucket_t
獲取IMP
是通過方法imp(UNUSED_WITHOUT_PTRAUTH bucket_t *base, Class cls)
獲取的。第一個入參在非指標身份認證的系統裡沒有用處(在iPhone X開始,增加了指標身份認證的安全檢驗過程,可以在Apple的Preparing Your App to Work with Pointer Authentication文件中瞭解詳情,這裡不做贅述),本文的編譯平臺沒有指標身份認證的過程,故忽略。第二個入參用於函式指標的解碼,而解碼的過程也十分簡單,就是將快取所在的類物件或者元類物件的Class
指標與bucket_t
結構體實際儲存的_imp
做一次異或運算。既然存在解碼過程,必然存在編碼過程,接下來我們看看bucket_t
是如何編碼並存儲SEL
和IMP
的:
```C++ enum Atomicity { Atomic = true, NotAtomic = false }; enum IMPEncoding { Encoded = true, Raw = false };
struct bucket_t { uintptr_t encodeImp(UNUSED_WITHOUT_PTRAUTH bucket_t *base, IMP newImp, UNUSED_WITHOUT_PTRAUTH SEL newSel, Class cls) const { if (!newImp) return 0; return (uintptr_t)newImp ^ (uintptr_t)cls; }
template<Atomicity atomicity, IMPEncoding impEncoding>
void set(bucket_t *base, SEL newSel, IMP newImp, Class cls) {
ASSERT(_sel.load(memory_order_relaxed) == 0 ||
_sel.load(memory_order_relaxed) == newSel);
uintptr_t newIMP = (impEncoding == Encoded
? encodeImp(base, newImp, newSel, cls)
: (uintptr_t)newImp);
if (atomicity == Atomic) {
_imp.store(newIMP, memory_order_relaxed);
if (_sel.load(memory_order_relaxed) != newSel) {
_sel.store(newSel, memory_order_release);
}
} else {
_imp.store(newIMP, memory_order_relaxed);
_sel.store(newSel, memory_order_relaxed);
}
}
} ```
通過方法encodeImp()
可以發現,函式指標的編碼過程也是將快取所在的類物件或者元類物件的Class
指標與函式指標做一次異或運算。這裡面涉及的數學原理很簡單,簡而言之就是A==A^B^B
。而方法set()
中,我們注意到當為原子寫入時(atomicity == Atomic
),_sel
的寫入的記憶體順序是memory_order_release
。這是因為objc_msgSend
對方法快取的讀寫不進行加鎖操作,但是當_imp
有值而_sel
為空對objc_msgSend
來說是安全的,而_sel
不為空且_imp
為舊值對objc_msgSend
來說是不安全的。故當需要原子寫入時,需要確保當進行_sel
的寫入時,_imp
已經完成寫入操作,所以選擇_sel
的寫入的記憶體順序為memory_order_release
。
2.4.2. 讀寫快取
2.4.2.1. 新增方法快取
接下來講解cache_t
結構體是如何新增方法快取的,照例先上原始碼:
```C++
define CACHE_END_MARKER 1
enum {
INIT_CACHE_SIZE_LOG2 = 2,
INIT_CACHE_SIZE = (1 << INIT_CACHE_SIZE_LOG2),
MAX_CACHE_SIZE_LOG2 = 16,
MAX_CACHE_SIZE = (1 << MAX_CACHE_SIZE_LOG2),
};
struct cache_t { void incrementOccupied(); void setBucketsAndMask(struct bucket_t newBuckets, mask_t newMask); size_t bytesForCapacity(uint32_t cap); bucket_t endMarker(struct bucket_t b, uint32_t cap); bucket_t allocateBuckets(mask_t newCapacity);
void reallocate(mask_t oldCapacity, mask_t newCapacity, bool freeOld);
void collect_free(bucket_t *oldBuckets, mask_t oldCapacity);
void insert(SEL sel, IMP imp, id receiver);
};
void cache_t::insert(SEL sel, IMP imp, id receiver) { mask_t newOccupied = occupied() + 1; unsigned oldCapacity = capacity(), capacity = oldCapacity; if (isConstantEmptyCache()) { if (!capacity) capacity = INIT_CACHE_SIZE; reallocate(oldCapacity, capacity, false); } else if (newOccupied + CACHE_END_MARKER > cache_fill_ratio(capacity)) { capacity = capacity ? capacity * 2 : INIT_CACHE_SIZE; if (capacity > MAX_CACHE_SIZE) { capacity = MAX_CACHE_SIZE; } reallocate(oldCapacity, capacity, true); }
bucket_t *b = buckets();
mask_t m = capacity - 1;
mask_t begin = cache_hash(sel, m);
mask_t i = begin;
do {
if (b[i].sel() == 0) {
incrementOccupied();
b[i].set<Atomic, Encoded>(b, sel, imp, cls());
return;
}
if (b[i].sel() == sel) {
return;
}
} while ((i = cache_next(i, m)) != begin);
}
static inline mask_t cache_next(mask_t i, mask_t mask) { return (i+1) & mask; }
static inline mask_t cache_fill_ratio(mask_t capacity) { return capacity * 3 / 4; }
static inline mask_t cache_hash(SEL sel, mask_t mask) { uintptr_t value = (uintptr_t)sel; return (mask_t)(value & mask); }
void cache_t::incrementOccupied() { _occupied++; }
void cache_t::reallocate(mask_t oldCapacity, mask_t newCapacity, bool freeOld) { bucket_t oldBuckets = buckets(); bucket_t newBuckets = allocateBuckets(newCapacity);
setBucketsAndMask(newBuckets, newCapacity - 1);
if (freeOld) {
collect_free(oldBuckets, oldCapacity);
}
}
void cache_t::setBucketsAndMask(struct bucket_t *newBuckets, mask_t newMask) { _bucketsAndMaybeMask.store((uintptr_t)newBuckets, memory_order_release); _maybeMask.store(newMask, memory_order_release); _occupied = 0; }
size_t cache_t::bytesForCapacity(uint32_t cap) { return sizeof(bucket_t) * cap; }
bucket_t cache_t::endMarker(struct bucket_t b, uint32_t cap) { return (bucket_t *)((uintptr_t)b + bytesForCapacity(cap)) - 1; }
bucket_t cache_t::allocateBuckets(mask_t newCapacity) { bucket_t newBuckets = (bucket_t )calloc(bytesForCapacity(newCapacity), 1); bucket_t end = endMarker(newBuckets, newCapacity);
end->set<NotAtomic, Raw>(newBuckets, (SEL)(uintptr_t)1, (IMP)newBuckets, nil);
return newBuckets;
} ```
可以看出,新增方法快取的實現是非常簡單的,就是超過3/4容量就擴容翻倍。對照程式碼繪製等效流程圖如下:
注意到在buckets
擴容的過程中,是直接將擴容前的buckets
釋放掉而不是將其重新完整拷貝。這是其實是為了效能考慮,因為如果將舊的快取拷貝到新快取上會導致時間代價太大。
還有一點需要注意的是buckets
的最後一個bucket
的_sel
被設為1、_imp
被設為第一個bucket
的記憶體地址。
2.4.2.2. 查詢方法快取
為了極致的效能考慮,Apple使用匯編語言來實現查詢方法快取,並且提供了一個C/C++的API(cache_getImp()
)。而且,查詢方法快取是不對快取進行加鎖一類的讀寫互斥處理的(如何防止同時讀寫出現問題,參考2.4.2.1.講解的bucket_t
結構體set()
方法的實現,這裡不做贅述)。組合語言終歸是過於晦澀了,這裡我們使用C++在遵照原彙編實現效能考慮的基礎上對其進行了一定的改寫,如下:
```C++ extern "C" IMP cache_getImp(Class cls, SEL sel, IMP value_on_constant_cache_miss = nil);
// asm to C++ IMP cache_getImp(Class cls, SEL sel, IMP value_on_constant_cache_miss = nil) { struct cache_t cache = cls->cache;
if (cache.occupied() == 0) {
return value_on_constant_cache_miss;
}
struct bucket_t *buckets = cache.buckets();
mask_t mask = cache.mask();
mask_t begin = (mask_t)((uintptr_t)sel & mask);
struct bucket_t *bucket = (struct bucket_t *)((uintptr_t)buckets + 16 * begin);
do {
SEL _sel = bucket->_sel;
if (_sel == sel) {
return (IMP)(bucket->_imp ^ (uintptr_t)cls);
}
if (_sel == (SEL)0) {
return value_on_constant_cache_miss;
}
if (_sel != (SEL)1) {
bucket = (struct bucket_t *)((uintptr_t)bucket + 16);
} else {
bucket = (struct bucket_t *)(bucket->_imp);
}
} while (true);
return value_on_constant_cache_miss;
} ```
可以看到,我們這段程式碼是直接使用buckets
的記憶體地址加記憶體偏移量對其進行遍歷讀取的,這樣做的目的是讓CPU少做一次乘法運算(常規的陣列讀取buckets[i]
實際上是buckets+ i * sizeof(bucket_t)
)。這裡,我們也能清楚的知道為什麼在插入方法快取時需要將最後一個bucket
儲存上第一個bucket
的記憶體地址,原因就是為了方便組合語言在遍歷到最後一個bucket
時跳轉到第一個bucket
進行下一次遍歷。
2.5. 小結
以上,我們講解了Objective-C在面向物件上的實現及其實現結構,並且,我們可以知道了Objective-C的類可以在執行時動態地進行一定的修改。那我們來看看,實際在開發工作中,我們如何將這些知識合理的運用。
2.5.1. 實現給category新增屬性
首先看一個category的定義:
```Objective-C @interface FooClass (Foo)
@property (nonatomic, strong) id fooObj;
@end ```
在一些場景中,我們可能需要給已經編譯好的類新增屬性來實現一些特定程式碼邏輯。可是在2.3.2.中,我們已經講解了category是無法新增例項變數的,如此一來,FooClass (Foo)
就無法自動給屬性fooObj
生成setter函式和getter函式。那是不是對此我們就毫無辦法了呢?當然不是!在2.2.中,我們提到isa
的低2位元代表has_assoc
,即表示當前物件是否具有或者曾經具有關聯物件。何為關聯物件?即一個物件能關聯另一個物件,並且擁有它的生命週期控制權。(可能有點繞,想要詳細瞭解的同學可以參考這個連結: https://nshipster.cn/associated-objects/ )
對應的,要想使用關聯物件,得使用對應的runtime API:
```Objective-C void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy);
id objc_getAssociatedObject(id object, const void *key); ```
看起來非常簡單,objc_setAssociatedObject
表示設定關聯物件,objc_getAssociatedObject
表示獲取關聯物件。它們的第一個入參object
都是被關聯的物件,而第二個入參key
都是一個64位的鍵值(雖然他是指標型別,但實際上它並不會嘗試去讀取指標指向的內容,故將其理解為64位的鍵值比較合理)。objc_setAssociatedObject
的第三個入參value
是關聯物件,而第四個入參policy
是關聯策略。policy
是objc_AssociationPolicy
型別,而objc_AssociationPolicy
其實是個列舉型別,定義如下:
```Objective-C typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy) { OBJC_ASSOCIATION_ASSIGN = 0,
OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1,
OBJC_ASSOCIATION_COPY_NONATOMIC = 3,
OBJC_ASSOCIATION_RETAIN = 01401,
OBJC_ASSOCIATION_COPY = 01403
; ```
通過命名,也能知道每個列舉值對應的具體含義這裡就不再贅述了。言歸正傳,我們來看看它具體如何實現給category新增屬性:
```Objective-C @implementation FooClass (Foo)
-
(void)setFooObj:(id)obj { objc_setAssociatedObject(self, @selector(fooObj), obj, OBJC_ASSOCIATION_RETAIN_NONATOMIC); }
-
(id)fooObj { return objc_getAssociatedObject(self, _cmd); }
@end ```
注意到,我們這裡給入參key
賦的是SEL
值,後面的講解會提到同一個selector
名對應同一個SEL
值,故將其作為唯一識別符號賦給入參key
是合理的。
至此,我們就實現了給category新增屬性。
其實,關聯物件也可以關聯上類物件,這樣就能實現“類屬性”的操作。方法與上述方法大差不差,這裡不展開贅述。
2.5.2. 實現高效序列化與反序列化物件
眾所周知,想要將一個Objective-C物件序列化儲存到disk上或反序列化讀取到memory上,需要實現協議NSCoding
,即實現例項方法encodeWithCoder
和initWithCoder
。一般情況下,我們會如此實現:
```Objective-C
@interface FooClass : NSObject
@property (nonatomic, strong) id obj1; /***/ @property (nonatomic, strong) id objN;
@end
@implementation FooClass
-
(void)encodeWithCoder:(NSCoder )coder { [coder encodeObject:self.obj1 forKey:@"obj1"]; /**/ [coder encodeObject:self.objN forKey:@"objN"]; }
-
(nullable instancetype)initWithCoder:(NSCoder )coder { if (self = [super init]) { self.obj1 = [coder decodeObjectForKey:@"obj1"]; /**/ self.objN = [coder decodeObjectForKey:@"objN"]; } return self; }
@end ```
如此實現,帶來兩個弊端,一個是當屬性較多時,程式碼實現比較繁瑣;另一個是後期可拓展性不強,假如後期迭代的過程中增刪屬性,就需要對應著修改例項方法encodeWithCoder
和initWithCoder
。那有沒有一勞永逸的方法?當然有,考慮到一般我們序列化或反序列化的過程中,僅需要儲存例項變數,我們在2.4.2.2.中講解過例項變數實質上就是ivar
,可以通過獲取ivar
陣列進行遍歷編解碼操作。這裡給出所有例項變數都為Objective-C物件的實現方式:
```Objective-C @implementation FooClass
-
(void)encodeWithCoder:(NSCoder )coder { unsigned int outCount = 0; Ivar vars = class_copyIvarList([self class], &outCount); for (unsigned int index = 0; index < outCount; ++index) { Ivar var = vars[index]; NSString *key = [NSString stringWithCString:ivar_getName(var) encoding:typeUTF8Text];
id value = [self valueForKey:key]; [coder encodeObject:value forKey:key];
} }
-
(nullable instancetype)initWithCoder:(NSCoder )coder { if (self = [super init]) { unsigned int outCount = 0; Ivar vars = class_copyIvarList([self class], &outCount); for (unsigned int index = 0; index < outCount; ++index) { Ivar var = vars[i]; NSString *key = [NSString stringWithCString:ivar_getName(var) encoding:typeUTF8Text];
id value = [coder decodeObjectForKey:key]; [self setValue:value forKey:key]; }
} return self; }
@end ```
當例項變數存在非Objective-C物件時,使用runtime APIivar_getTypeEncoding
配合NSCoder
的encodeValueOfObjCType
使用,這裡不展開贅述。
對於需要儲存屬性,可以通過class_copyPropertyList
獲取屬性列表,通過屬性不同特性(attribute)實現需要的操作。
2.5.3. 實現優雅新增業務埋點
假如我們有一個業務需求,需要在所有的ViewController
的viewDidLoad
生命週期中新增業務埋點邏輯或者一些重複性的工作,一般情況下,我們有兩種實現方案。一種是將所有的ViewController
定義成繼承UIViewController
的子類,然後重寫方法viewDidLoad
:
```Objective-C @interface FooVC_1_1 : UIViewController
@end
@implementation FooVC_1_1
-
(void)viewDidLoad { [super viewDidLoad];
/* tracker operation /
/* other operation / }
@end
/***/
@interface FooVC_1_N : UIViewController
@end
@implementation FooVC_1_N
-
(void)viewDidLoad { [super viewDidLoad];
/* tracker operation /
/* other operation / }
@end ```
這種方法的缺點就是太繁瑣了,並且如果是UIViewController
例項物件將無法執行埋點邏輯。
另一種方法是定義一個BaseViewController
,在BaseViewController
中重寫方法viewDidLoad
並且讓其它的ViewController
繼承BaseViewController
:
```Objective-C @interface BaseViewController : UIViewController
-
(void)viewDidLoad { [super viewDidLoad];
/* tracker operation / }
@end
@interface FooVC_2_1 : BaseViewController
@end
@implementation FooVC_2_1
-
(void)viewDidLoad { [super viewDidLoad];
/* other operation / }
@end
/***/
@interface FooVC_2_N : BaseViewController
@end
@implementation FooVC_2_N
-
(void)viewDidLoad { [super viewDidLoad];
/* other operation / }
@end ```
這種方法的缺點同樣是UIViewController
例項物件無法執行埋點邏輯,並且每次新增一個埋點邏輯都需要在BaseViewController
的原始碼檔案中進行修改。
考慮到在2.3.3.中我們講解過,方法的實質是method_t
結構體,理論上在執行時是可以修改method_t
結構體的成員變數imp
,即達到了Hook方法的效果。於是,Objective-C的runtime中最有“魅力”的操作——方法混合(Method Swizzling)誕生了!
```Objective-C @interface UIViewController (Tracker)
@end
@interface UIViewController (Tracker)
-
(void)load { static dispatch_once_t onceFlag; dispatch_once(&onceFlag, ^{ Class class = [self class]; SEL originalSelector = @selector(viewDidLoad); SEL swizzledSelector = @selector(tracker_viewDidLoad); Method originalMethod = class_getInstanceMethod(class, originalSelector); Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector); BOOL didAddMethod = class_addMethod(class, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod)); if (didAddMethod) { class_replaceMethod(class, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod)); } else { method_exchangeImplementations(originalMethod, swizzledMethod); } }); }
-
(void)tracker_viewDidLoad { /* tracker operation /
[self tracker_viewDidLoad]; }
@end ```
類方法load
在類和類別載入的時候會自動呼叫(對此感興趣的可以參考連結: https://developer.apple.com/documentation/objectivec/nsobject/1418815-load?language=objc ),於是,我們可以在此方法中,先嚐試使用class_addMethod
新增SEL
為viewDidLoad
、IMP
為_I_UIViewController_Tracker_tracker_viewDidLoad
的方法。但是class_addMethod
只能給當前類(不會判斷父類)原本沒有的SEL
新增方法,因UIViewController
一定有SEL
為viewDidLoad
的方法,故其實在這個例子裡class_addMethod
會返回NO
(但對於一些繼承而來的子類仍有判斷的必要)。如果class_addMethod
裡成功添加了方法,那麼使用class_replaceMethod
將原本SEL
為tracker_viewDidLoad
的方法替換IMP
為_I_UIViewController_viewDidLoad
即可。而這裡的例子將會執行method_exchangeImplementations
的邏輯,即將SEL
為viewDidLoad
和SEL
為tracker_viewDidLoad
的方法交換IMP
。這樣就實現了例項物件呼叫viewDidLoad
時實際上呼叫了_I_UIViewController_Tracker_tracker_viewDidLoad
函式,而在tracker_viewDidLoad
方法實現的最後呼叫了tracker_viewDidLoad
將實際上呼叫_I_UIViewController_viewDidLoad
函式。這樣,在開發者的視角就相當於將兩個不同的方法混合起來了!
UIViewController (Tracker)
的原理圖如下:
再舉個例子:
```Objective-C @interface BaseViewControler : UIViewController
@end
@interface BaseViewControler
-
(void)load { static dispatch_once_t onceFlag; dispatch_once(&onceFlag, ^{ Class class = [self class]; SEL originalSelector = @selector(viewDidLoad); SEL swizzledSelector = @selector(tracker_viewDidLoad); Method originalMethod = class_getInstanceMethod(class, originalSelector); Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector); BOOL didAddMethod = class_addMethod(class, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod)); if (didAddMethod) { class_replaceMethod(class, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod)); } else { method_exchangeImplementations(originalMethod, swizzledMethod); } }); }
-
(void)tracker_viewDidLoad { /* tracker operation /
[self tracker_viewDidLoad]; }
@end ```
BaseViewController
的原理圖如下:
3. 訊息傳送與轉發
雖然都在說Objective-C模仿了Smalltalk式的訊息傳遞機制,但大部分人對Smalltalk不甚瞭解。這裡不會講解有關Smalltalk的內容,不過我們倒是可以看一下Smalltalk的經典訊息傳遞語法:
PlainText
receiver message
有沒有發現它跟Objective-C的方法呼叫語法很相似?
Objective-C
[receiver message];
Objective-C在方法呼叫上與Smalltalk的區別就是多了一對中括號。其實這是Objective-C為了方便編譯器實現巢狀的方法呼叫解析,故意偷的懶。
言歸正傳,上面的講解中其實說到了一點,實際上編譯器會將Objective-C的方法呼叫語法翻譯成呼叫Objective-C的runtime API。以這個方法呼叫為例:
C++
objc_msgSend(receiver, @selector(message));
我們也可以通過自然語言來理解這個過程——"Send message to receiver"。
接下來,我們圍繞Objective-C的方法呼叫(訊息傳遞)機制進行講解。
實際上,與objc_msgSend
類似作用的runtime API還有三個objc_msgSend_fpret
、objc_msgSend_fp2ret
和objc_msgSend_stret
。其中,objc_msgSend_fpret
和objc_msgSend_fp2ret
在arm上沒有作用,x86-64分別用於方法返回型別為long double
和_Complex long double
的情況。而objc_msgSend_stret
用於方法返回值為結構體型別的情況。
3.1. 重新認識訊息
我們日常開發中,經常使用@selector()
來獲取方法選擇器SEL
,那什麼是具體什麼是SEL
呢?這裡直接給出定義:
C++
typedef struct objc_selector *SEL;
typedef id _Nullable (*IMP)(id _Nonnull, SEL _Nonnull, ...);
通過定義,我們能清楚的知道,SEL
實際上是objc_selector
結構體指標。那問題又來了,什麼是objc_selector
結構體呢?可惜的是,runtime原始碼中並沒有給出objc_selector
結構體的實現,並且Apple官方文件和原始碼的註釋中都提到了一點:
Defines an opaque type that represents a method selector.
(From Apple)
也就是說,objc_selector
結構體可以理解為一個神祕的型別,並且實際上,我們可以直接把SEL
當作方法選擇器的64位UID來使用,即理解成:
C++
typedef uintptr_t SEL;
而@selector()
其實是sel_registerName()
的語法糖,sel_registerName()
的定義是:
C++
SEL sel_registerName(const char *name);
它的作用就是將方法選擇器的名稱註冊到全域性散列表(雜湊表)中,並返回一個SEL
。所以,實際上SEL
的值僅僅與方法選擇器的字串名有關,並且在當前程序生命週期中,無論何時呼叫@selector()
,都能返回一樣的SEL
值。故將其理解為方法選擇器的64位UID也無可厚非(實際上,還有另一個API,sel_getUid()
,它的實現與sel_registerName()
一摸一樣,只是API的名稱不同)。
至此,其實我們也能明白為什麼Objective-C不支援方法過載,就是因為SEL
僅僅與方法選擇器的名稱有關,不管入參的型別或者方法返回的型別如何改變,只要名稱不變,SEL
的值就恆定不變。
我們知道,Objective-C的方法呼叫實際上是模擬向接收者傳送訊息的過程,而訊息指的是就是SEL
的值,或者說SEL
就是訊息名。訊息名本身僅儲存了字串資訊,而接收者如何消費訊息,僅通過訊息名是不夠的。於是,我們需要通過訊息名能查詢到對應的方法,才能實現訊息響應的過程。這時候注意到在2.3.3.講解的方法型別method_t
結構體,除了儲存了SEL
型別的name
之外,還儲存了IMP
型別的imp
和const char *
型別的types
。這樣,我們就能通過訊息名找到相同訊息名的方法,實現方法呼叫。
於是,我們接下來研究一下method_t
結構體。這裡先給出IMP
的定義:
Objective-C
typedef id _Nullable (*IMP)(id _Nonnull, SEL _Nonnull, ...);
我們看到,IMP
實際上是函式指標型別。這樣我們就能在method_t
結構體中找到對應的實現函式,繼而實現方法呼叫等一系列操作。注意到IMP
的至少有兩個入參,第一個是id
型別,第二個是SEL
型別。那又是為什麼如此設計呢?這裡我們給個例子:
```Objective-C @interface FooClass : NSObject
- (void)foo;
@end
@implementation FooClass
- (void)foo { return; }
@end ```
在這個例子中,我們在FooClass
內定義一個簡單的例項方法foo
。接著,我們使用Clang將這段Objective-C原始碼翻譯成C/C++:
C++
static void _I_FooClass_foo(FooClass *self, SEL _cmd) {
return;
}
這裡我們很清楚的看到,例項方法foo
被翻譯為靜態函式_I_FooClass_foo
(這個命名是有講究的,後面會對此進行講解),而且也很清楚看到有兩個入參self
和_cmd
。這時候可能有些同學反應過來了,我們日常中使用的self
實際上不是什麼特殊的關鍵字,而是翻譯後的靜態函式的第一個入參。這裡直接給出結論,所有的Objective-C方法都存在兩個隱藏入參——self
和_cmd
。當通過Objective-C方法呼叫的方式進行方法呼叫時,第一個入參self
會被賦值為接收者(receiver),第二個入參_cmd
會被賦值為訊息(message、selector)。
所以,當我們呼叫FooClass
的例項方法foo
時,實際上呼叫的是_I_FooClass_foo
函式,而且,入參self
為FooClass
的例項物件,而入參_cmd
為@selector(foo)
的返回值。
這時候可能有人會疑惑了,self
和_cmd
都是函式的入參,那super
呢?實際上,super
不是函式入參,而是objc_msgSendSuper
的語法糖。舉個例子說明:
Objective-C
- (void)foo {
[super foo];
return;
}
我們這裡呼叫了[super foo],實際上這個語法糖等價於:
Objective-C
- (void)foo {
struct objc_super fooSuperClass;
fooSuperClass.receiver = self;
fooSuperClass.super_class = [FooClass superclass];
objc_msgSendSuper(&fooSuperClass, @selector(foo));
return;
}
從這個例子,我們能得出,objc_msgSendSuper
與objc_msgSend
類似,入參至少有兩個,並且第二個引數為SEL
值。而objc_super
有兩個成員函式,receiver
和super_class
。receiver
被賦值為方法的第一個入參self
,而super_class
則在編譯期間就固定為FooClass
的父類。(後面會對此展開詳細講解,先按下不表)
同樣的,與objc_msgSend
類似,objc_msgSendSuper_stret
用於方法返回值為結構體型別的情況。
特別注意的是,實際上編譯過程中super
會翻譯為呼叫objc_msgSendSuper2
,與objc_msgSendSuper
不同的是,objc_super
結構體的成員變數super_class
賦值為己類而非父類。在objc_msgSendSuper2
的實現中通過receiver
的isa
獲取父類,故效能上也優於objc_msgSendSuper
。
另外,除了特殊情況不建議開發者將super
翻譯成objc_msgSendSuper
進行呼叫,因為容易帶來無限遞迴的隱患。(可以思考為什麼上面的程式碼示例中,fooSuperClass.super_class
賦值為[FooClass superclass]
,而不是[self superclass]
)
接著探討方法,對於例項方法foo
,翻譯後是靜態函式_I_FooClass_foo
,那假如我們定義一個同樣命名的類方法foo
呢?再舉個例子:
```Objective-C @interface FooClass : NSObject
- (void)foo;
- (void)foo;
@end
@implementation FooClass
-
(void)foo { return; }
-
(void)foo { return; }
@end ```
使用Clang將這段Objective-C原始碼翻譯成C/C++:
```C++ static void _I_FooClass_foo(FooClass * self, SEL _cmd) { return; }
static void _C_FooClass_foo(Class self, SEL _cmd) { return; } ```
同樣的,我們嘗試構建FooClass
的category,並新增同樣命名為foo
的例項方法和類方法:
```Objective-C @interface FooClass (FooCategory)
- (void)foo;
- (void)foo;
@end
@implementation FooClass (FooCategory)
-
(void)foo { return; }
-
(void)foo { return; }
@end ```
使用Clang將這段Objective-C原始碼翻譯成C/C++:
```C++ static void _I_FooClass_FooCategory_foo(FooClass * self, SEL _cmd) { return; }
static void _C_FooClass_FooCategory_foo(Class self, SEL _cmd) { return; } ```
至此,我們已經很輕易的得出Objective-C方法實際對應的函式命名方式:
PlainText
_prefix_className(_categoryName)_methodName
其中,字首I和C分別表示例項方法(Instance Method)與類方法(Class Method)。通過命名方式,我們也能得知為什麼Objective-C雖然不支援方法過載,卻能通過類別重寫方法,因為通過類別重寫的方法本質上就不是同一個函式。
我們注意到,method_t
結構體還有const char *
型別的成員變數types
,它描述的是函式指標的返回型別和入參型別。因為我們在實際運用中,除了希望接收者(receiver)處理訊息,還能根據不同的附帶引數返回我們想要的返回型別。所以我們需要一個字串型別的變數來描述這些不同的型別。還是舉個簡單的例子:
```Objective-C @interface FooClass : NSObject
- (void)sayHelloTo:(id)foo;
@end
@implementation FooClass
- (void)sayHelloTo:(id)foo { NSLog(@"%@ %@ %@", self, NSStringFromSelector(_cmd), foo); }
@end ```
假如我們需要在執行時新增同樣功能的方法,可以如下操作:
```Objective-C void sayHello(id self, SEL _cmd, id foo) { NSLog(@"%@ %@ %@", self, NSStringFromSelector(_cmd), foo); }
// some other function class_addMethod([FooClass class], self(sayHelloTo:), sayHello, "v@:@"); ```
注意到,字串"v@:@"
就描述了sayHello
函式的返回值型別和入參型別,具體可以參考Apple的Type Encodings,這裡不做過多的贅述。
至此,我們能完整且清楚地知道訊息具體的實現方式,以及在方法呼叫這個場景下,訊息是如何與方法程式碼進行繫結的。
3.2. 訊息傳送
3.2.1. 沿繼承鏈查詢方法
在3.1.中,我們講解了訊息,接下來,我們的重點就是探究訊息是如何進行傳送的。我們知道,在Objective-C裡面傳送訊息的大部分場景中,實際上是呼叫objc_msgSend
函式,在runtime的原始碼中,objc_msgSend
函式的實現是由組合語言實現的(採用組合語言實現objc_msgSend
函式除了有效能和CPU架構上的考慮,還有就是組合語言能更優雅地應對可變引數,不過這裡不做深入探討)。這裡我們仍使用C++在遵照原彙編實現效能考慮的基礎上對其進行了一定的改寫(沒有想到一個比較好的可變參轉發,如下:
```C++ enum { LOOKUP_INITIALIZE = 1, LOOKUP_RESOLVER = 2, LOOKUP_NIL = 4, LOOKUP_NOCACHE = 8, };
id objc_msgSend(id self, SEL _cmd, ...) { if (!self) { return nil; }
Class cls = (self -> isa) & ISA_MASK;
IMP imp = cache_getImp(cls, _cmd);
if (imp) {
return imp(self, _cmd, ...);
}
imp = lookUpImpOrForward(self, _cmd, cls, LOOKUP_INITIALIZE | LOOKUP_RESOLVER);
return imp(self, _cmd, ...);
} ```
可以看到,實際上objc_msgSend
函式就幹了四件事:
- Nil test
判斷入參self
是否為空,若為空返回nil
。
- Get class
將self
的isa
與ISA_MASK
做或運算,即得到當前的類。
- Get imp in cache
在isa
指向的方法快取中嘗試獲取imp
,若成功獲取,直接進行方法呼叫。(可以參考2.4.2.2.中查詢方法快取的實現)
- Lookup imp in method list
在方法列表中查詢imp
,並直接呼叫。
objc_msgSendSuper
函式與objc_msgSend
函式在實現上基本無異,只是在【2. Get class】裡當前類取的是objc_super
結構體的成員變數super_class
。
注意到,我們在方法列表中查詢imp
時,呼叫了函式lookUpImpOrForward
,直接給出原始碼:
```C++ IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior) { const IMP forward_imp = (IMP)_objc_msgForward_impcache; IMP imp = nil; Class curClass = cls;
for (;;) {
method_t *meth = getMethodNoSuper_nolock(curClass, sel);
if (meth) {
imp = meth->imp(false);
goto done;
}
if ((curClass = curClass->getSuperclass()) == nil)) {
imp = forward_imp;
break;
}
imp = cache_getImp(curClass, sel);
if (imp == forward_imp) {
break;
}
if (imp) {
goto done;
}
}
if (behavior & LOOKUP_RESOLVER) {
behavior ^= LOOKUP_RESOLVER;
return resolveMethod_locked(inst, sel, cls, behavior);
}
done:
cls->cache.insert(sel, imp, receiver)
if ((behavior & LOOKUP_NIL) && imp == forward_imp)) {
return nil;
}
return imp;
}
static method_t * getMethodNoSuper_nolock(Class cls, SEL sel) { auto const methods = cls->data()->methods(); for (auto mlists = methods.beginLists(), end = methods.endLists(); mlists != end; ++mlists) { method_t m = search_method_list_inline(mlists, sel); if (m) return m; }
return nil;
}
static method_t * search_method_list_inline(const method_list_t *mlist, SEL sel) { int methodListIsFixedUp = mlist->isFixedUp(); int methodListHasExpectedSize = mlist->isExpectedSize();
if (methodListIsFixedUp && methodListHasExpectedSize) {
return findMethodInSortedMethodList(sel, mlist);
} else {
return findMethodInUnsortedMethodList(sel, mlist);
}
} ```
不難看出,lookUpImpOrForward
函式先是在通過getMethodNoSuper_nolock
函式在當前類的方法列表中查詢,若查詢不到則先是在父類的方法快取查詢,再是在父類的方法列表中查詢,直至找到或當前查詢類為nil
。注意到getMethodNoSuper_nolock
函式在遍歷方法列表時,呼叫了search_method_list_inline
函式,而它對是否已排序(升序)的方法列表分別呼叫findMethodInSortedMethodList
和findMethodInUnsortedMethodList
。而實際上,findMethodInSortedMethodList
就是個二分查詢函式,而findMethodInUnsortedMethodList
就是個簡單粗暴的便利函式,本身沒什麼難點,這裡不再贅述。
簡單總結成流程圖如下:
至此,我們也能輕鬆理解了為什麼子類能在不重寫方法的情況下能響應父類實現的方法。這就是沿繼承鏈查詢方法的全部過程,一旦在繼承鏈中找到方法的實現,就結束查詢並在objc_msgSend
實現方法呼叫;若是遍歷了整個繼承鏈都找不到方法的實現,就會嘗試動態方法決議。
3.2.2. 動態方法決議
在3.2.1.中講解過,當在繼承鏈上找不到方法的實現時,將嘗試動態方法決議。何為動態方法決議?英文原詞為“resolve method”,不過我個人認為這個英文詞對於國人理解起來有點費勁,還是就中文譯名作出解釋:“動態方法決議”指的就是在一個統一的方法裡判斷是否新增一個方法(這就是“決議”一詞的精髓所在)。那究竟是哪個方法呢?其實是兩個:
```Objective-C + (BOOL)resolveClassMethod:(SEL)sel;
- (BOOL)resolveInstanceMethod:(SEL)sel; ```
這兩個本身都是類方法,resolveClassMethod
作為類方法在繼承鏈搜尋不到時呼叫的決議方法;resolveInstanceMethod
作為例項方法在繼承鏈搜尋不到時呼叫的決議方法。
接下來我們直接給出動態方法決議的實現:
```C++ static IMP resolveMethod_locked(id inst, SEL sel, Class cls, int behavior) { if (!cls->isMetaClass()) { resolveInstanceMethod(inst, sel, cls); } else { resolveClassMethod(inst, sel, cls); if (!lookUpImpOrNilTryCache(inst, sel, cls)) { resolveInstanceMethod(inst, sel, cls); } }
return lookUpImpOrForwardTryCache(inst, sel, cls, behavior);
}
static void resolveInstanceMethod(id inst, SEL sel, Class cls) { SEL resolve_sel = @selector(resolveInstanceMethod:); if (!lookUpImpOrNilTryCache(cls, resolve_sel, cls->ISA(true))) { return; }
BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
bool resolved = msg(cls, resolve_sel, sel);
IMP imp = lookUpImpOrNilTryCache(inst, sel, cls);
}
static void resolveClassMethod(id inst, SEL sel, Class cls) { SEL resolve_sel = @selector(resolveClassMethod:); if (!lookUpImpOrNilTryCache(inst, resolve_sel, cls)) { return; }
Class nonmeta = getMaybeUnrealizedNonMetaClass(cls, inst);
BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
bool resolved = msg(nonmeta, resolve_sel, sel);
IMP imp = lookUpImpOrNilTryCache(inst, sel, cls);
}
IMP lookUpImpOrNilTryCache(id inst, SEL sel, Class cls, int behavior) { return _lookUpImpTryCache(inst, sel, cls, behavior | LOOKUP_NIL); }
IMP lookUpImpOrForwardTryCache(id inst, SEL sel, Class cls, int behavior) { return _lookUpImpTryCache(inst, sel, cls, behavior); }
static IMP _lookUpImpTryCache(id inst, SEL sel, Class cls, int behavior) { IMP imp = cache_getImp(cls, sel); if (imp != NULL) goto done; if (imp == NULL) { return lookUpImpOrForward(inst, sel, cls, behavior); }
done: if ((behavior & LOOKUP_NIL) && imp == (IMP)_objc_msgForward_impcache) { return nil; } return imp; } ```
通過程式碼不難看出,其實現邏輯非常簡單,總結起來就是兩點:
- 若為類物件(即繼承鏈中找不到例項方法),則呼叫類方法
resolveInstanceMethod
- 若為元類物件(即繼承鏈中找不到類方法),則呼叫類方法
resolveClassMethod
簡單總結成流程圖如下:
通過這個流程圖,我們更能清楚地知道在進行動態方法決議的時候,呼叫了resolveInstanceMethod
或resolveClassMethod
後,接著又進行了沿繼承鏈查詢方法的流程。所以,為什麼要呼叫resolveMethod
?它有什麼作用?為什麼又需要新一輪的沿繼承鏈查詢方法的流程?注意到,我們為什麼把resolve method翻譯成動態方法決議,這裡的“動態”才是精髓所在!通常,我們可以去實現這個方法來為在繼承鏈中找不到的方法而臨時進行動態新增該方法的操作。舉個例子:
```Objective-C void foo(id self, SEL _cmd) { if (object_isClass(self)) { NSLog(@"Class method, %@, was resolved!", NSStringFromSelector(_cmd)); } else { NSLog(@"Instance method, %@, was resolved!", NSStringFromSelector(_cmd)); } }
@interface FooClass : NSObject
- (void)foo;
- (void)foo;
@end
@implementation FooClass
-
(BOOL)resolveInstanceMethod:(SEL)sel { if (sel == @selector(foo)) { class_addMethod([self class], @selector(foo), foo, "v@:"); return YES; } return [super resolveInstanceMethod:sel]; }
-
(BOOL)resolveClassMethod:(SEL)sel { if (sel == @selector(foo)) { class_addMethod(objc_getMetaClass(object_getClassName(self)), @selector(foo), foo, "v@:"); return YES; } return [super resolveClassMethod:sel]; }
@end
// some other function [FooClass foo]; Foo *obj = [FooClass new]; [obj foo]; ```
執行37-39行程式碼,輸出如下:
PlainText
Class method, foo, was resolved!
Instance method, foo, was resolved!
至此,我們成功將方法的呼叫與方法的新增都放在了執行時的同一時刻。
3.3. 訊息轉發
如果在繼承鏈中查詢不到方法,並且在動態方法決議後仍無法在繼承鏈中查詢到方法,則訊息傳送的全部過程結束,接下來將開始訊息轉發。
在3.2.1.中,我們在lookUpImpOrForward
的原始碼中不難看到,當在繼承鏈中查詢不當方法,會返回一個特殊的函式指標_objc_msgForward_impcache
。我們知道,在彙編實現的objc_msgSend
中,會直接呼叫lookUpImpOrForward
返回的函式指標,也就是說,訊息轉發實際上是_objc_msgForward_impcache
這個特殊的函式指標的函式實現。不過_objc_msgForward_impcache
也是組合語言實現的,這裡也簡單將其使用C++進行改寫,如下:
```C++ id _objc_msgForward_impcache(id self, SEL _cmd, ...) { return _objc_msgForward(self, _cmd, ...); }
id _objc_msgForward(id self, SEL _cmd, ...) { return _objc_forward_handler(self, _cmd, ...); } ```
實際上,就是呼叫了_objc_forward_handler
函式,而事實上,Apple從這裡開始就不進行程式碼開源了。不過在原始碼中,Apple給了一個預設實現,如下:
C++
void objc_defaultForwardHandler(id self, SEL sel) {
_objc_fatal("%c[%s %s]: unrecognized selector sent to instance %p "
"(no message forward handler is installed)",
class_isMetaClass(object_getClass(self)) ? '+' : '-',
object_getClassName(self), sel_getName(sel), self);
}
這裡,我們能知道,每次我們呼叫沒有實現的方法時,編譯器報錯【xxxxx: unrecognized selector sent to instance xxxxx】是進行了類似objc_defaultForwardHandler
的邏輯了。
雖然,我們從原始碼中無法窺探Apple對訊息轉發的完整實現,但是查閱相關文件,我們仍能總結出訊息轉發的兩大流程:
- 轉發訊息
- 轉發呼叫
先說轉發訊息,轉發訊息實際上就是呼叫forwardingTargetForSelector
方法,返回一個可以處理此訊息的物件。舉個例子:
```Objective-C @interface FooClassA : NSObject
- (void)foo;
- (void)foo;
@end
@implementation FooClassA
-
(void)foo { NSLog(@"%@ invoke class method %@", self, NSStringFromSelector(_cmd)); }
-
(void)foo { NSLog(@"%@ invoke instance method %@", self, NSStringFromSelector(_cmd)); }
@end
@interface FooClassB : NSObject
- (void)foo;
- (void)foo;
@end
@implementation FooClassB
-
(id)forwardingTargetForSelector:(SEL)aSelector { if (aSelector == @selector(foo)) { id fooA = [FooClassA new]; return fooA; } return [super forwardingTargetForSelector:aSelector]; }
-
(id)forwardingTargetForSelector:(SEL)aSelector { if (aSelector == @selector(foo)) { return [FooClassA class]; } return [super forwardingTargetForSelector:aSelector]; }
@end
// some other function [FooClassB foo]; FooClassB *fooB = [FooClassB new]; [fooB foo]; ```
執行第47-49行的程式碼,輸出如下:
PlainText
FooClassA invoke class method foo
<FooClassA: xxxxx> invoke instance method foo
所以,分別實現forwardingTargetForSelector
的類方法和例項方法可以根據不同的方法轉發給不同的物件。
需要注意的是,在NSObject
的實現中,forwardingTargetForSelector
返回的是nil
。
如果forwardingTargetForSelector
中返回了nil
,則判定為轉發訊息失敗,將開始轉發呼叫的流程。轉發呼叫與轉發方法不同的是,轉發呼叫需要先後呼叫兩個方法:methodSignatureForSelector
和forwardInvocation
。methodSignatureForSelector
返回方法的簽名,實際上可以理解為返回方法的同樣的,我們舉個例子:
```Objective-C @interface FooClassA : NSObject
- (void)foo;
- (void)foo;
@end
@implementation FooClassA
-
(void)foo { NSLog(@"%@ invoke class method %@", self, NSStringFromSelector(_cmd)); }
-
(void)foo { NSLog(@"%@ invoke instance method %@", self, NSStringFromSelector(_cmd)); }
@end
@interface FooClassB : NSObject
- (void)foo;
- (void)foo;
@end
@implementation FooClassB
-
(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector { if (aSelector == @selector(foo)) { return [NSMethodSignature signatureWithObjCTypes:"v@:"]; // return [FooClassA instanceMethodSignatureForSelector:aSelector]; } return [super methodSignatureForSelector:aSelector]; }
-
(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector { if (aSelector == @selector(foo)) { return [NSMethodSignature signatureWithObjCTypes:"v@:"]; // return [FooClassA methodSignatureForSelector:aSelector]; } return [super methodSignatureForSelector:aSelector]; }
-
(void)forwardInvocation:(NSInvocation )anInvocation { if ([anInvocation selector] == @selector(foo)) { FooClassA fooA = [FooClassA new]; [anInvocation invokeWithTarget:fooA]; return; } return [super forwardInvocation:anInvocation]; }
-
(void)forwardInvocation:(NSInvocation *)anInvocation { if ([anInvocation selector] == @selector(foo)) { [anInvocation invokeWithTarget:[FooClassA class]]; return; } return [super forwardInvocation:anInvocation]; }
@end
// some other function [FooClassB foo]; FooClassB *fooB = [FooClassB new]; [fooB foo]; ```
執行第63-65行的程式碼,輸出如下:
PlainText
FooClassA invoke class method foo
<FooClassA: xxxxx> invoke instance method foo
實際上不難看出,methodSignatureForSelector
就是將需要轉發的訊息進行一次方法簽名,即將其返回型別和入參型別包裝成NSMethodSignature
型別。然後,runtime系統內部將其與訊息名(SEL
值)和入參一同包裝成NSInvocation
。接著就呼叫forwardInvocation
實現最後的轉發呼叫流程。
需要注意的是,若在methodSignatureForSelector
中返回的方法簽名不符合Objective-C的方法簽名的基本要求(即返回型別為基本型別或結構體,並且第一個入參為id
型別,第二個入參為SEL
型別),則在包裝NSInvocation
時就會報錯。
並且,在NSObject
的實現中,methodSignatureForSelector
會在繼承鏈中查詢到對應方法的方法型別,並將其包裝成NSMethodSignature
型別。若找不到將會報錯【xxxxx: unrecognized selector sent to instance xxxxx】。
同樣的,在NSObject
的實現中,forwardInvocation
會直接報錯【xxxxx: unrecognized selector sent to instance xxxxx】。
至此,訊息轉發流程結束!
3.4. 小結
在3.2.和3.3.中,我們講解了訊息傳送和訊息轉發的完整流程。那我們再總結一下,從我們使用訊息傳送的語法糖([receiver message]
)或直接使用runtime API(objc_msgSend(receiver, @selector(message))
),到最後呼叫對應的方法,對應的簡化版流程圖如下:
3.4.1. 小試牛刀
先通過一個簡單的題目來檢驗一下我們在3.中的學習成果:
```Objective-C @interface NSObject (Foo)
- (void)foo;
- (void)foo;
@end
@implementation NSObject (Foo)
- (void)foo { NSLog(@"%@ invoke %@", self, NSStringFromSelector(_cmd)); }
@end
// some other function [NSObject foo]; ```
在這段程式碼中,執行第17行會發生什麼?會crash嗎?
我們簡單分析一下,當執行[NSObject foo]
時,首先會在NSObject
類物件的isa
獲取NSObject
元類物件。然後先在NSObject
元類物件中查詢foo
方法,顯然查詢不到。接著會在NSObject
元類物件的superClass
中繼續查詢foo
方法。在2.1.中,我們知道NSObject
元類物件的superClass
是NSObject
類物件。於是乎,我們在NSObject
類物件中查詢到foo
方法的實現,然後呼叫foo
方法。所以,並不會crash,並且能正常得到輸出:
PlainText
NSObject invoke foo
再來個簡單的題目:
```Objective-C @interface FooClass : NSObject
- (void)foo;
@end
@implementation FooClass
- (void)foo { NSLog(@"[self class] : %@", [self class]); NSLog(@"[super class] : %@", [super class]); }
@end
// some other function [FooClass foo]; ```
在這段程式碼中,執行第17行程式碼會輸出什麼?
可能有些同學會把[super class]
理解為[self superclass]
,然後就認為第11行應該輸出【[super class] : NSObject
】。實際上這個是錯的。我們還是簡單分析一下,之前我們在3.1.中講解過,super
實際上是objc_msgSendSuper
的語法糖。我們可以將[super class]
簡單翻譯一下:
C++
struct objc_super fooSuperClass;
fooSuperClass.receiver = self;
fooSuperClass.super_class = [FooClass superclass];
objc_msgSendSuper(&fooSuperClass, @selector(class));
注意到,我們這裡的receiver仍為self
,只是當我們沿著繼承鏈查詢方法時,是從super_class
開始查詢,也就是NSObject
元類物件。於是,當我們找到class
方法時,呼叫方式如下:
C++
// objc_msgSendSuper(struct objc_super *super, SEL _cmd, ...)
IMP *imp = lookUpImpOrForward(super->receiver, @selector(class), super->super_clas, LOOKUP_INITIALIZE | LOOKUP_RESOLVER);
imp(super->receiver, @selector(class));
我們知道,NSObject
元類物件的class
方法的實現就是return self
,所以呼叫class
方法的imp
時候,得到的就是第一個入參super->receiver
,也就是FooClass
類物件。故這個題目的輸出是:
PlainText
[self class] : FooClass
[super class] : FooClass
至此,相信大家應該對訊息傳送有了更加深刻的認識。
3.4.2. 面向切面程式設計(AOP)
可能對於很多同學來說,面向物件程式設計(OOP,Object-oriented programming)在學習和日常工作中,運用的比較多。面向切面程式設計,可能就不甚瞭解了。這裡先給出維基百科的定義:
面向切面的程式設計(Aspect-oriented programming,AOP,又譯作面向方面的程式設計、剖面導向程式設計),是電腦科學中的一種程式設計思想,旨在將橫切關注點與業務主體進行進一步分離,以提高程式程式碼的模組化程度。通過在現有程式碼基礎上增加額外的通知(Advice)機制,能夠對被宣告為“切點(Pointcut)”的程式碼塊進行統一管理與裝飾。
(From 維基百科)
可能有點晦澀難懂,這用一句簡單的話概括一下:這種在執行時,動態地將程式碼切入到類的指定方法、指定位置上的程式設計思想就是面向切面程式設計。
這時可能就有同學想到了,我們在2.5.3.中使用方法混合(Method Swizzling)就是一種AOP思想。不過這裡,我們再介紹一種AOP的實現方式:
```Objective-C
@protocol Tracker
- (void)invoke:(NSInvocation *)invocation withTarget:(id)target;
@end
@interface SuperDelegate : NSProxy
-
(instancetype)createWithTarget:(id)delegate;
-
(void)addTrackSelector:(SEL)selector withTracker:(id
)tracker;
@end
@interface SuperDelegate ()
@property (nonatomic, weak) id delegate;
@property (nonatomic, strong) NSMutableDictionary
@end
@implementation SuperDelegate
-
(instancetype)createWithTarget:(id)delegate { SuperDelegate *proxy = [SuperDelegate alloc]; proxy.delegate = delegate; proxy.selectorDict = [NSMutableDictionary dictionary]; return proxy; }
-
(void)addTrackSelector:(SEL)selector withTracker:(id
)tracker { NSValue *selectorValue = [NSValue valueWithPointer:selector]; if (!self.selectorDict[selectorValue]) { self.selectorDict[selectorValue] = [NSMutableArray array]; } [self.selectorDict[selectorValue] addObject:tracker]; } -
(NSMethodSignature *)methodSignatureForSelector:(SEL)sel { return [self.delegate methodSignatureForSelector:sel]; }
-
(void)forwardInvocation:(NSInvocation )invocation { SEL selector = invocation.selector; NSValue selectorValue = [NSValue valueWithPointer:selector]; NSArray
> *trackers = self.selectorDict[selectorValue]; for (id tracker in trackers) { [tracker invoke:invocation withTarget:self.delegate]; } [invocation invokeWithTarget:self.delegate]; }
@end ```
這裡,我們建立了一個SuperDelegate
的類,故名思義,我們把它作為一個“超級代理”使用。注意到,SuperDelegate
繼承自NSProxy
,而不是我們常見的NSObject
。其實NSProxy
可以理解為一個抽象類,他本身不具備例項化的能力,即並沒有init
方法,並且它在訊息傳送上也與NSObject
有所不同,當它在繼承鏈上查詢不到方法,就直接進行轉發呼叫(forward invocation),即沒有動態方法決議(resolve method)和轉發訊息(forward selector)的過程(NSProxy
的詳細文件見Apple官方文件:https://developer.apple.com/documentation/foundation/nsproxy?language=objc ,這裡不再贅述)。除此之外,繼承自NSProxy
的子類必須實現methodSignatureForSelector
和forwardInvocation
。我們這裡的SuperDelegate
就是充分使用了這個特性,提供了addTrackSelector:withTracker:
方法,即將要追蹤的訊息與追蹤器進行繫結,當SuperDelegate
接收到被追蹤的訊息時,會自動呼叫追蹤器的invoke:withTarget:
。故只需要實現Tracker
協議的類,即可對其新增埋點等業務需求。這裡提供一個簡單的應用場景:
```Objective-C
@interface CollectionViewTracker : NSObject
@end
@implementation CollectionViewTracker
- (void)invoke:(NSInvocation )invocation withTarget:(id)target { if (invocation.selector == @selector(collectionView:didSelectItemAtIndexPath:)) { / tracker operation / } }
@end
// ViewController setup UICollectionView
id
在這個場景中,我們實現了一個CollectionViewTracker
類,專門負責UICollectionView
的點選埋點處理。當我們將其新增到我們的SuperDelegate
中,即實現了UICollectionView
的點選事件新增埋點功能。
4. 總結
通過本次學習,相信我們對Objective-C有了更加充分的認識,也能理解它作為一門動態語言的在iOS客戶端研發中的巨大優勢。不過,成也蕭何,敗也蕭河,正是因為它過於動態,將大部分的方法呼叫的延遲到執行時校驗,導致很多時候debug的難度也增大不少。而且,由於它在方法呼叫上,需要經過漫長的訊息傳送以及訊息轉發鏈路,所以往往效能上比不上C++、新興語言Swift等靜態語言。最後,最重要的一句話,也是把Apple開發者文件上的話照搬翻譯一下:如果不是對Objective-C runtime API充分了解,儘量不要使用它!!!
參考連結
-
Apple
- https://opensource.apple.com/source/objc4/objc4-818.2/
- https://developer.apple.com/documentation/objectivec/objective-c_runtime?language=objc
- https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/ObjCRuntimeGuide/Introduction/Introduction.html
-
halfrost blog
- https://halfrost.com/objc_life/
- https://halfrost.com/objc_runtime_isa_class/
- https://halfrost.com/objc_runtime_objc_msgsend/
- https://halfrost.com/how_to_use_runtime/
-
掘金社群
- https://juejin.cn/post/6844903878794706957
- https://juejin.cn/post/6844903888122822669
- https://juejin.cn/post/6844903896708562952
- https://juejin.cn/post/6844903903939526669
- https://juejin.cn/post/6976897400782716958
- https://juejin.cn/post/6975836165555372045