本文首發於我的個人部落格:『不羈閣』
本文用來介紹 iOS開發中 『Runtime』的基礎知識。通過本文您將瞭解到:
- 什麼是 Runtime?
- 訊息機制的基本原理
- Runtime 中的概念解析
- Runtime 訊息轉發
- 訊息傳送以及轉發機制總結
1. 什麼是 Runtime?
我們都知道,將原始碼轉換為可執行的程式,通常要經過三個步驟:編譯、連結、執行。不同的編譯語言,在這三個步驟中所進行的操作又有些不同。
C 語言
作為一門靜態類語言,在編譯階段就已經確定了所有變數的資料型別,同時也確定好了要呼叫的函式,以及函式的實現。
而 Objective-C 語言
是一門動態語言。在編譯階段並不知道變數的具體資料型別,也不知道所真正呼叫的哪個函式。只有在執行時間才檢查變數的資料型別,同時在執行時才會根據函式名查詢要呼叫的具體函式。這樣在程式沒執行的時候,我們並不知道呼叫一個方法具體會發生什麼。
Objective-C 語言
把一些決定性的工作從編譯階段、連結階段推遲到 執行時階段 的機制,使得 Objective-C
變得更加靈活。我們甚至可以在程式執行的時候,動態的去修改一個方法的實現,這也為大為流行的『熱更新』提供了可能性。
而實現 Objective-C 語言
執行時機制 的一切基礎就是 Runtime
。
Runtime
實際上是一個庫,這個庫使我們可以在程式執行時動態的建立物件、檢查物件,修改類和物件的方法。
2. 訊息機制的基本原理
Objective-C 語言
中,物件方法呼叫都是類似 [receiver selector];
的形式,其本質就是讓物件在執行時傳送訊息的過程。
我們來看看方法呼叫 [receiver selector];
在『編譯階段』和『執行階段』分別做了什麼?
- 編譯階段:
[receiver selector];
方法被編譯器轉換為:objc_msgSend(receiver,selector)
(不帶引數)objc_msgSend(recevier,selector,org1,org2,…)
(帶引數)
- 執行時階段:訊息接受者
recever
尋找對應的selector
。- 通過
recevier
的isa 指標
找到recevier
的Class(類)
; - 在
Class(類)
的cache(方法快取)
的雜湊表中尋找對應的IMP(方法實現)
; - 如果在
cache(方法快取)
中沒有找到對應的IMP(方法實現)
的話,就繼續在Class(類)
的method list(方法列表)
中找對應的selector
,如果找到,填充到cache(方法快取)
中,並返回selector
; - 如果在
Class(類)
中沒有找到這個selector
,就繼續在它的superClass(父類)
中尋找; - 一旦找到對應的
selector
,直接執行recever
對應selector
方法實現的IMP(方法實現)
。 - 若找不到對應的
selector
,訊息被轉發或者臨時向recever
新增這個selector
對應的實現方法,否則就會發生崩潰。
- 通過
在上述過程中涉及了好幾個新的概念:objc_msgSend
、isa 指標
、Class(類)
、IMP(方法實現)
等,下面我們來具體講解一下各個概念的含義。
3. Runtime 中的概念解析
3.1 objc_msgSend
所有 Objective-C 方法呼叫在編譯時都會轉化為對 C 函式 objc_msgSend
的呼叫。objc_msgSend(receiver,selector);
是 [receiver selector];
對應的 C 函式。
3.2 Class(類)
在 objc/runtime.h
中,Class(類)
被定義為指向 objc_class 結構體
的指標,objc_class 結構體
的資料結構如下:
/// An opaque type that represents an Objective-C class.
typedef struct objc_class *Class;
struct objc_class {
Class _Nonnull isa; // objc_class 結構體的例項指標
#if !__OBJC2__
Class _Nullable super_class; // 指向父類的指標
const char * _Nonnull name; // 類的名字
long version; // 類的版本資訊,預設為 0
long info; // 類的資訊,供執行期使用的一些位標識
long instance_size; // 該類的例項變數大小;
struct objc_ivar_list * _Nullable ivars; // 該類的例項變數列表
struct objc_method_list * _Nullable * _Nullable methodLists; // 方法定義的列表
struct objc_cache * _Nonnull cache; // 方法快取
struct objc_protocol_list * _Nullable protocols; // 遵守的協議列表
#endif
};
複製程式碼
從中可以看出,
objc_class 結構體
定義了很多變數:自身的所有例項變數(ivars)、所有方法定義(methodLists)、遵守的協議列表(protocols)等。objc_class 結構體
存放的資料稱為 後設資料(metadata)。
objc_class 結構體
的第一個成員變數是isa 指標
,isa 指標
儲存的是所屬類的結構體的例項的指標,這裡儲存的就是objc_class 結構體
的例項指標,而例項換個名字就是 物件。換句話說,Class(類)
的本質其實就是一個物件,我們稱之為 類物件。
3.3 Object(物件)
接下來,我們再來看看 objc/objc.h
中關於 Object(物件)
的定義。
Object(物件)
被定義為 objc_object 結構體
,其資料結構如下:
/// Represents an instance of a class.
struct objc_object {
Class _Nonnull isa; // objc_object 結構體的例項指標
};
/// A pointer to an instance of a class.
typedef struct objc_object *id;
複製程式碼
這裡的 id
被定義為一個指向 objc_object 結構體
的指標。從中可以看出 objc_object 結構體
只包含一個 Class
型別的 isa 指標
。
換句話說,一個 Object(物件)
唯一儲存的就是它所屬 Class(類)
的地址。當我們對一個物件,進行方法呼叫時,比如 [receiver selector];
,它會通過 objc_object 結構體
的 isa 指標
去找對應的 objc_class 結構體
,然後在 objc_class 結構體
的 methodLists(方法列表)
中找到我們呼叫的方法,然後執行。
3.4 Meta Class(元類)
從上邊我們看出,物件(objc_object 結構體)
的 isa 指標
指向的是對應的 類物件(objc_class 結構體)
。那麼 類物件(objc_class 結構體
)的 isa 指標
又指向什麼呢?
objc_class 結構體
的 isa 指標
實際上指向的的是 類物件
自身的 Meta Class(元類)
。
那麼什麼是 Meta Class(元類)
?
Meta Class(元類)
就是一個類物件所屬的 類。一個物件所屬的類叫做 類物件
,而一個類物件所屬的類就叫做 元類。
Runtime 中把類物件所屬型別就叫做
Meta Class(元類)
,用於描述類物件本身所具有的特徵,而在元類的 methodLists 中,儲存了類的方法連結串列,即所謂的「類方法」。並且類物件中的isa 指標
指向的就是元類。每個類物件有且僅有一個與之相關的元類。
在 2. 訊息機制的基本原理 中我們講解了 物件方法的呼叫過程,我們是通過物件的 isa 指標
找到 對應的 Class(類)
;然後在 Class(類)
的 method list(方法列表)
中找對應的 selector
。
而 類方法的呼叫過程 和物件方法呼叫差不多,流程如下:
- 通過類物件
isa 指標
找到所屬的Meta Class(元類)
; - 在
Meta Class(元類)
的method list(方法列表)
中找到對應的selector
; - 執行對應的
selector
。
下面看一個示例:
NSString *testString = [NSString stringWithFormat:@"%d,%s",3, "test"];
複製程式碼
上邊的示例中,stringWithFormat:
被髮送給了 NSString 類
,NSString 類
通過 isa 指標
找到 NSString 元類
,然後在該元類的方法列表中找到對應的 stringWithFormat:
方法,然後執行該方法。
3.5 例項物件、類、元類之間的關係
上面,我們講解了 例項物件(Object)、類(Class)、Meta Class(元類) 的基本概念,以及簡單的指向關係。下面我們通過一張圖來清晰地表示出這種關係。
我們先來看 isa 指標
:
- 水平方向上,每一級中的
例項物件
的isa 指標
指向了對應的類物件
,而類物件
的isa 指標
指向了對應的元類
。而所有元類的isa 指標
最終指向了NSObject 元類
,因此NSObject 元類
也被稱為根源類
。 - 垂直方向上,
元類
的isa 指標
和父類元類
的isa 指標
都指向了根元類
。而根源類
的isa 指標
又指向了自己。
我們再來看 父類指標
:
類物件
的父類指標
指向了父類的類物件
,父類的類物件
又指向了根類的類物件
,根類的類物件
最終指向了 nil。元類
的父類指標
指向了父類物件的元類
。父類物件的元類
的父類指標
指向了根類物件的元類
,也就是根元類
。而根元類
的父親指標
指向了根類物件
,最終指向了 nil。
3.6 方法(Method)
objc_class 結構體
的 methodLists(方法列表)
中存放的元素就是 方法(Method)
。
先來看下 objc/runtime.h
中,表示 方法(Method)
的 objc_method 結構體
的資料結構:
/// An opaque type that represents a method in a class definition.
/// 代表類定義中一個方法的不透明型別
typedef struct objc_method *Method;
struct objc_method {
SEL _Nonnull method_name; // 方法名
char * _Nullable method_types; // 方法型別
IMP _Nonnull method_imp; // 方法實現
};
複製程式碼
可以看到,objc_method 結構體
中包含了 方法名(method_name)
,方法型別(method_types)
和 方法實現(method_imp)
。下面,我們來了解下這三個變數。
SEL method_name; // 方法名
/// An opaque type that represents a method selector.
typedef struct objc_selector *SEL;
複製程式碼
SEL
是一個指向 objc_selector 結構體
的指標,但是在 runtime 相關標頭檔案中並沒有找到明確的定義。不過,通過測試我們可以得出: SEL
只是一個儲存方法名的字串。
SEL sel = @selector(viewDidLoad);
NSLog(@"%s", sel); // 輸出:viewDidLoad
SEL sel1 = @selector(test);
NSLog(@"%s", sel1); // 輸出:test
複製程式碼
IMP method_imp; // 方法實現
/// A pointer to the function of a method implementation.
#if !OBJC_OLD_DISPATCH_PROTOTYPES
typedef void (*IMP)(void /* id, SEL, ... */ );
#else
typedef id _Nullable (*IMP)(id _Nonnull, SEL _Nonnull, ...);
#endif
複製程式碼
IMP
的實質是一個函式指標,所指向的就是方法的實現。IMP
用來找到函式地址,然後執行函式。
char * method_types; // 方法型別
方法型別 method_types
是個字串,用來儲存方法的引數型別和返回值型別。
到這裡,
Method
的結構就已經很清楚了,Method
將SEL(方法名)
和IMP(函式指標)
關聯起來,當對一個物件傳送訊息時,會通過給出的SEL(方法名)
去找到IMP(函式指標)
,然後執行。
4. Runtime 訊息轉發
在 2. 訊息機制的基本原理 最後一步中我們提到:若找不到對應的 selector
,訊息被轉發或者臨時向 recever
新增這個 selector
對應的實現方法,否則就會發生崩潰。
當一個方法找不到的時候,Runtime 提供了 訊息動態解析、訊息接受者重定向、訊息重定向 等三步處理訊息,具體流程如下圖所示:
4.1 訊息動態解析
Objective-C 執行時會呼叫 +resolveInstanceMethod:
或者 +resolveClassMethod:
,讓你有機會提供一個函式實現。我們可以通過重寫這兩個方法,新增其他函式實現,並返回 YES
, 那執行時系統就會重新啟動一次訊息傳送的過程。
主要用的的方法如下:
// 類方法未找到時調起,可以在此新增類方法實現
+ (BOOL)resolveClassMethod:(SEL)sel;
// 物件方法未找到時調起,可以在此物件方法實現
+ (BOOL)resolveInstanceMethod:(SEL)sel;
/**
* class_addMethod 向具有給定名稱和實現的類中新增新方法
* @param cls 被新增方法的類
* @param name selector 方法名
* @param imp 實現方法的函式指標
* @param types imp 指向函式的返回值與引數型別
* @return 如果新增方法成功返回 YES,否則返回 NO
*/
BOOL class_addMethod(Class cls, SEL name, IMP imp,
const char * _Nullable types);
複製程式碼
舉個例子:
#import "ViewController.h"
#include "objc/runtime.h"
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// 執行 fun 函式
[self performSelector:@selector(fun)];
}
// 重寫 resolveInstanceMethod: 新增物件方法實現
+ (BOOL)resolveInstanceMethod:(SEL)sel {
if (sel == @selector(fun)) { // 如果是執行 fun 函式,就動態解析,指定新的 IMP
class_addMethod([self class], sel, (IMP)funMethod, "v@:");
return YES;
}
return [super resolveInstanceMethod:sel];
}
void funMethod(id obj, SEL _cmd) {
NSLog(@"funMethod"); //新的 fun 函式
}
@end
複製程式碼
列印結果: 2019-06-12 10:25:39.848260+0800 runtime[14884:7977579] funMethod
從上邊的例子中,我們可以看出,雖然我們沒有實現 fun
方法,但是通過重寫 resolveInstanceMethod:
,利用 class_addMethod
方法新增物件方法實現 funMethod
方法,並執行。從列印結果來看,成功調起了funMethod
方法。
我們注意到 class_addMethod 方法中的特殊引數
v@:
,具體可參考官方文件中關於Type Encodings
的說明:傳送門
4.2 訊息接受者重定向
如果上一步中 +resolveInstanceMethod:
或者 +resolveClassMethod:
沒有新增其他函式實現,執行時就會進行下一步:訊息接受者重定向。
如果當前物件實現了 -forwardingTargetForSelector:
,Runtime 就會呼叫這個方法,允許我們將訊息的接受者轉發給其他物件。
用到的方法:
// 重定向方法的訊息接收者,返回一個類或例項物件
- (id)forwardingTargetForSelector:(SEL)aSelector;
複製程式碼
注意:這裡
+resolveInstanceMethod:
或者+resolveClassMethod:
無論是返回YES
,還是返回NO
,只要其中沒有新增其他函式實現,執行時都會進行下一步。
舉個例子:
#import "ViewController.h"
#include "objc/runtime.h"
@interface Person : NSObject
- (void)fun;
@end
@implementation Person
- (void)fun {
NSLog(@"fun");
}
@end
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// 執行 fun 方法
[self performSelector:@selector(fun)];
}
+ (BOOL)resolveInstanceMethod:(SEL)sel {
return YES; // 為了進行下一步 訊息接受者重定向
}
// 訊息接受者重定向
- (id)forwardingTargetForSelector:(SEL)aSelector {
if (aSelector == @selector(fun)) {
return [[Person alloc] init];
// 返回 Person 物件,讓 Person 物件接收這個訊息
}
return [super forwardingTargetForSelector:aSelector];
}
複製程式碼
列印結果: 2019-06-12 17:34:05.027800+0800 runtime[19495:8232512] fun
可以看到,雖然當前 ViewController
沒有實現 fun
方法,+resolveInstanceMethod:
也沒有新增其他函式實現。但是我們通過 forwardingTargetForSelector
把當前 ViewController
的方法轉發給了 Person
物件去執行了。列印結果也證明我們成功實現了轉發。
我們通過 forwardingTargetForSelector
可以修改訊息的接收者,該方法返回引數是一個物件,如果這個物件是不是 nil
,也不是 self
,系統會將執行的訊息轉發給這個物件執行。否則,繼續進行下一步:訊息重定向流程。
4.3 訊息重定向
如果經過訊息動態解析、訊息接受者重定向,Runtime 系統還是找不到相應的方法實現而無法響應訊息,Runtime 系統會利用 -methodSignatureForSelector:
方法獲取函式的引數和返回值型別。
- 如果
-methodSignatureForSelector:
返回了一個NSMethodSignature
物件(函式簽名),Runtime 系統就會建立一個NSInvocation
物件,並通過-forwardInvocation:
訊息通知當前物件,給予此次訊息傳送最後一次尋找 IMP 的機會。 - 如果
-methodSignatureForSelector:
返回nil
。則 Runtime 系統會發出-doesNotRecognizeSelector:
訊息,程式也就崩潰了。
所以我們可以在 -forwardInvocation:
方法中對訊息進行轉發。
用到的方法:
// 獲取函式的引數和返回值型別,返回簽名
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector;
// 訊息重定向
- (void)forwardInvocation:(NSInvocation *)anInvocation;
複製程式碼
舉個例子:
#import "ViewController.h"
#include "objc/runtime.h"
@interface Person : NSObject
- (void)fun;
@end
@implementation Person
- (void)fun {
NSLog(@"fun");
}
@end
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// 執行 fun 函式
[self performSelector:@selector(fun)];
}
+ (BOOL)resolveInstanceMethod:(SEL)sel {
return YES; // 為了進行下一步 訊息接受者重定向
}
- (id)forwardingTargetForSelector:(SEL)aSelector {
return nil; // 為了進行下一步 訊息重定向
}
// 獲取函式的引數和返回值型別,返回簽名
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
if ([NSStringFromSelector(aSelector) isEqualToString:@"fun"]) {
return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}
return [super methodSignatureForSelector:aSelector];
}
// 訊息重定向
- (void)forwardInvocation:(NSInvocation *)anInvocation {
SEL sel = anInvocation.selector; // 從 anInvocation 中獲取訊息
Person *p = [[Person alloc] init];
if([p respondsToSelector:sel]) { // 判斷 Person 物件方法是否可以響應 sel
[anInvocation invokeWithTarget:p]; // 若可以響應,則將訊息轉發給其他物件處理
} else {
[self doesNotRecognizeSelector:sel]; // 若仍然無法響應,則報錯:找不到響應方法
}
}
@end
複製程式碼
列印結果: 2019-06-13 13:23:06.935624+0800 runtime[30032:8724248] fun
可以看到,我們在 -forwardInvocation:
方法裡面讓 Person
物件去執行了 fun
函式。
既然 -forwardingTargetForSelector:
和 -forwardInvocation:
都可以將訊息轉發給其他物件處理,那麼兩者的區別在哪?
區別就在於 -forwardingTargetForSelector:
只能將訊息轉發給一個物件。而 -forwardInvocation:
可以將訊息轉發給多個物件。
以上就是 Runtime 訊息轉發的整個流程。
結合之前講的 2. 訊息機制的基本原理,就構成了整個訊息傳送以及轉發的流程。下面我們來總結一下整個流程。
5. 訊息傳送以及轉發機制總結
呼叫 [receiver selector];
後,進行的流程:
- 編譯階段:
[receiver selector];
方法被編譯器轉換為:objc_msgSend(receiver,selector)
(不帶引數)objc_msgSend(recevier,selector,org1,org2,…)
(帶引數)
- 執行時階段:訊息接受者
recever
尋找對應的selector
。- 通過
recevier
的isa 指標
找到recevier
的class(類)
; - 在
Class(類)
的cache(方法快取)
的雜湊表中尋找對應的IMP(方法實現)
; - 如果在
cache(方法快取)
中沒有找到對應的IMP(方法實現)
的話,就繼續在Class(類)
的method list(方法列表)
中找對應的selector
,如果找到,填充到cache(方法快取)
中,並返回selector
; - 如果在
class(類)
中沒有找到這個selector
,就繼續在它的superclass(父類)
中尋找; - 一旦找到對應的
selector
,直接執行recever
對應selector
方法實現的IMP(方法實現)
。 - 若找不到對應的
selector
,Runtime 系統進入訊息轉發機制。
- 通過
- 執行時訊息轉發階段:
- 動態解析:通過重寫
+resolveInstanceMethod:
或者+resolveClassMethod:
方法,利用class_addMethod
方法新增其他函式實現; - 訊息接受者重定向:如果上一步新增其他函式實現,可在當前物件中利用
-forwardingTargetForSelector:
方法將訊息的接受者轉發給其他物件; - 訊息重定向:如果上一步沒有返回值為
nil
,則利用-methodSignatureForSelector:
方法獲取函式的引數和返回值型別。- 如果
-methodSignatureForSelector:
返回了一個NSMethodSignature
物件(函式簽名),Runtime 系統就會建立一個NSInvocation
物件,並通過-forwardInvocation:
訊息通知當前物件,給予此次訊息傳送最後一次尋找 IMP 的機會。 - 如果
-methodSignatureForSelector:
返回nil
。則 Runtime 系統會發出-doesNotRecognizeSelector:
訊息,程式也就崩潰了。
- 如果
- 動態解析:通過重寫
參考資料
- 文件:Objective-C 執行時(蘋果官方文件)
- 文件:Objective-C 執行時程式設計指南(蘋果官方文件)
- 博文:Runtime-iOS 執行時基礎篇
- 博文:iOS Runtime 詳解
- 博文:新手也看得懂的 iOS Runtime 教程
以上就是 iOS 開發:『Runtime』詳解(一):基礎知識 的所有內容了。 整篇文章主要就講了一件事:訊息傳送以及轉發機制的原理和流程。這也是 Runtime 系統的工作原理。
下一篇筆者準備講一下『Runtime』的黑魔法 Method Swizzling。
iOS 開發:『Runtime』詳解 系列文章:
- iOS 開發:『Runtime』詳解(一)基礎知識
- iOS 開發:『Runtime』詳解(二)Method Swizzling
- iOS 開發:『Runtime』詳解(三)Category 底層原理
- iOS 開發:『Runtime』詳解(四)獲取類詳細屬性、方法
尚未完成:
- iOS 開發:『Runtime』詳解(五)Crash 防護系統
- iOS 開發:『Runtime』詳解(六)Objective-C 2.0 結構解析
- iOS 開發:『Runtime』詳解(七)KVO 底層實現