本文用來介紹 iOS 開發中『Runtime』相關的基礎知識。通過本文,您將瞭解到:
- 什麼是 Runtime?
- 消息機制的基本原理
- Runtime 中的概念解析(objc_msgSend 、Class、Object、Meta Class、Method)
- 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,…)
(帶參數)
-
- 運行時階段:消息接受者
recevier
尋找對應的selector
。- 通過
recevier
的isa 指針
找到recevier
的Class(類)
; - 在
Class(類)
的cache(方法緩存)
的散列表中尋找對應的IMP(方法實現)
; - 如果在
cache(方法緩存)
中沒有找到對應的IMP(方法實現)
的話,就繼續在Class(類)
的method list(方法列表)
中找對應的selector
,如果找到,填充到cache(方法緩存)
中,並返回selector
; - 如果在
Class(類)
中沒有找到這個selector
,就繼續在它的superClass(父類)
中尋找; - 一旦找到對應的
selector
,直接執行recevier
對應selector
方法實現的IMP(方法實現)
。 - 若找不到對應的
selector
,消息被轉發或者臨時向recevier
添加這個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 指針
去找對應的 object_class 結構體
,然後在 object_class 結構體
的 methodLists(方法列表)
中找到我們調用的方法,然後執行。
3.4 Meta Class(元類)
從上邊我們看出,對象(objc_object 結構體)
的 isa 指針
指向的是對應的 類對象(object_class 結構體)
。那麼 類對象(object_class 結構體
)的 isa 指針
又指向什麼呢?
object_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(方法)
object_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
,消息被轉發或者臨時向 recevier
添加這個 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,…)
(帶參數)
-
- 運行時階段:消息接受者
recevier
尋找對應的selector
。- 通過
recevier
的isa 指針
找到recevier
的class(類)
; - 在
Class(類)
的cache(方法緩存)
的散列表中尋找對應的IMP(方法實現)
; - 如果在
cache(方法緩存)
中沒有找到對應的IMP(方法實現)
的話,就繼續在Class(類)
的method list(方法列表)
中找對應的selector
,如果找到,填充到cache(方法緩存)
中,並返回selector
; - 如果在
class(類)
中沒有找到這個selector
,就繼續在它的superclass(父類)
中尋找; - 一旦找到對應的
selector
,直接執行recevier
對應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』的實戰應用。
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 底層實現
- 本文作者: 行走少年郎
- 本文鏈接: https://www.jianshu.com/p/633e5d8386a8
- 版權聲明: 本文章採用 CC BY-NC-SA 3.0 許可協議。轉載請在文字開頭註明『本文作者』和『本文鏈接』!