本文首發於個人我的博客:『不羈閣』html
文章連接:bujige.net/blog/iOS-Ru…objective-c
本文用來介紹 iOS開發中 『Runtime』的基礎知識。經過本文您將瞭解到:編程
- 什麼是 Runtime?
- 消息機制的基本原理
- Runtime 中的概念解析
- Runtime 消息轉發
- 消息發送以及轉發機制總結
咱們都知道,將源代碼轉換爲可執行的程序,一般要通過三個步驟:編譯、連接、運行。不一樣的編譯語言,在這三個步驟中所進行的操做又有些不一樣。緩存
C 語言
做爲一門靜態類語言,在編譯階段就已經肯定了全部變量的數據類型,同時也肯定好了要調用的函數,以及函數的實現。bash
而 Objective-C 語言
是一門動態語言。在編譯階段並不知道變量的具體數據類型,也不知道所真正調用的哪一個函數。只有在運行時間才檢查變量的數據類型,同時在運行時纔會根據函數名查找要調用的具體函數。這樣在程序沒運行的時候,咱們並不知道調用一個方法具體會發生什麼。數據結構
Objective-C 語言
把一些決定性的工做從編譯階段、連接階段推遲到 運行時階段 的機制,使得 Objective-C
變得更加靈活。咱們甚至能夠在程序運行的時候,動態的去修改一個方法的實現,這也爲大爲流行的『熱更新』提供了可能性。app
而實現 Objective-C 語言
運行時機制 的一切基礎就是 Runtime
。ide
Runtime
其實是一個庫,這個庫使咱們能夠在程序運行時動態的建立對象、檢查對象,修改類和對象的方法。函數
Objective-C 語言
中,對象方法調用都是相似 [receiver selector];
的形式,其本質就是讓對象在運行時發送消息的過程。post
咱們來看看方法調用 [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(方法實現)
等,下面咱們來具體講解一下各個概念的含義。
全部 Objective-C 方法調用在編譯時都會轉化爲對 C 函數 objc_msgSend
的調用。objc_msgSend(receiver,selector);
是 [receiver selector];
對應的 C 函數。
在 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(類)
的本質其實就是一個對象,咱們稱之爲 類對象。
接下來,咱們再來看看 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(方法列表)
中找到咱們調用的方法,而後執行。
從上邊咱們看出,對象(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:
方法,而後執行該方法。
上面,咱們講解了 實例對象(Object)、類(Class)、Meta Class(元類) 的基本概念,以及簡單的指向關係。下面咱們經過一張圖來清晰地表示出這種關係。
咱們先來看 isa 指針
:
實例對象
的 isa 指針
指向了對應的 類對象
,而 類對象
的 isa 指針
指向了對應的 元類
。而全部元類的 isa 指針
最終指向了 NSObject 元類
,所以 NSObject 元類
也被稱爲 根源類
。元類
的 isa 指針
和 父類元類
的 isa 指針
都指向了 根元類
。而 根源類
的 isa 指針
又指向了本身。咱們再來看 父類指針
:
類對象
的 父類指針
指向了 父類的類對象
,父類的類對象
又指向了 根類的類對象
,根類的類對象
最終指向了 nil。元類
的 父類指針
指向了 父類對象的元類
。父類對象的元類
的 父類指針
指向了 根類對象的元類
,也就是 根元類
。而 根元類
的 父親指針
指向了 根類對象
,最終指向了 nil。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(函數指針)
,而後執行。
在 2. 消息機制的基本原理 最後一步中咱們提到:若找不到對應的 selector
,消息被轉發或者臨時向 recever
添加這個 selector
對應的實現方法,不然就會發生崩潰。
當一個方法找不到的時候,Runtime 提供了 消息動態解析、消息接受者重定向、消息重定向 等三步處理消息,具體流程以下圖所示:
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
的說明:傳送門
若是上一步中 +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
,系統會將運行的消息轉發給這個對象執行。不然,繼續進行下一步:消息重定向流程。
若是通過消息動態解析、消息接受者重定向,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. 消息機制的基本原理,就構成了整個消息發送以及轉發的流程。下面咱們來總結一下整個流程。
調用 [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:
消息,程序也就崩潰了。以上就是 iOS 開發:『Runtime』詳解(一):基礎知識 的全部內容了。 整篇文章主要就講了一件事:消息發送以及轉發機制的原理和流程。這也是 Runtime 系統的工做原理。
下一篇筆者準備講一下『Runtime』的黑魔法 Method Swizzling。
iOS 開發:『Runtime』詳解 系列文章:
還沒有完成: