iOS 開發:『Runtime』詳解(一)基礎知識

本文首發於個人我的博客:『不羈閣』html

文章連接:bujige.net/blog/iOS-Ru…objective-c


本文用來介紹 iOS開發中 『Runtime』的基礎知識。經過本文您將瞭解到:編程

  1. 什麼是 Runtime?
  2. 消息機制的基本原理
  3. Runtime 中的概念解析
  4. Runtime 消息轉發
  5. 消息發送以及轉發機制總結

1. 什麼是 Runtime?

咱們都知道,將源代碼轉換爲可執行的程序,一般要通過三個步驟:編譯連接運行。不一樣的編譯語言,在這三個步驟中所進行的操做又有些不一樣。緩存

C 語言 做爲一門靜態類語言,在編譯階段就已經肯定了全部變量的數據類型,同時也肯定好了要調用的函數,以及函數的實現。bash

Objective-C 語言 是一門動態語言。在編譯階段並不知道變量的具體數據類型,也不知道所真正調用的哪一個函數。只有在運行時間才檢查變量的數據類型,同時在運行時纔會根據函數名查找要調用的具體函數。這樣在程序沒運行的時候,咱們並不知道調用一個方法具體會發生什麼。數據結構

Objective-C 語言 把一些決定性的工做從編譯階段、連接階段推遲到 運行時階段 的機制,使得 Objective-C 變得更加靈活。咱們甚至能夠在程序運行的時候,動態的去修改一個方法的實現,這也爲大爲流行的『熱更新』提供了可能性。app

而實現 Objective-C 語言 運行時機制 的一切基礎就是 Runtimeide

Runtime 其實是一個庫,這個庫使咱們能夠在程序運行時動態的建立對象、檢查對象,修改類和對象的方法。函數


2. 消息機制的基本原理

Objective-C 語言 中,對象方法調用都是相似 [receiver selector]; 的形式,其本質就是讓對象在運行時發送消息的過程。post

咱們來看看方法調用 [receiver selector]; 在『編譯階段』和『運行階段』分別作了什麼?

  1. 編譯階段:[receiver selector]; 方法被編譯器轉換爲:
    1. objc_msgSend(receiver,selector) (不帶參數)
    2. objc_msgSend(recevier,selector,org1,org2,…)(帶參數)
  2. 運行時階段:消息接受者 recever 尋找對應的 selector
    1. 經過 recevierisa 指針 找到 recevierClass(類)
    2. Class(類)cache(方法緩存) 的散列表中尋找對應的 IMP(方法實現)
    3. 若是在 cache(方法緩存) 中沒有找到對應的 IMP(方法實現) 的話,就繼續在 Class(類)method list(方法列表) 中找對應的 selector,若是找到,填充到 cache(方法緩存) 中,並返回 selector
    4. 若是在 Class(類) 中沒有找到這個 selector,就繼續在它的 superClass(父類)中尋找;
    5. 一旦找到對應的 selector,直接執行 recever 對應 selector 方法實現的 IMP(方法實現)
    6. 若找不到對應的 selector,消息被轉發或者臨時向 recever 添加這個 selector 對應的實現方法,不然就會發生崩潰。

在上述過程當中涉及了好幾個新的概念:objc_msgSendisa 指針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

類方法的調用過程 和對象方法調用差很少,流程以下:

  1. 經過類對象 isa 指針 找到所屬的 Meta Class(元類)
  2. Meta Class(元類)method list(方法列表) 中找到對應的 selector;
  3. 執行對應的 selector

下面看一個示例:

NSString *testString = [NSString stringWithFormat:@"%d,%s",3, "test"];
複製代碼

上邊的示例中,stringWithFormat: 被髮送給了 NSString 類NSString 類 經過 isa 指針 找到 NSString 元類,而後在該元類的方法列表中找到對應的 stringWithFormat: 方法,而後執行該方法。

3.5 實例對象、類、元類之間的關係

上面,咱們講解了 實例對象(Object)類(Class)Meta Class(元類) 的基本概念,以及簡單的指向關係。下面咱們經過一張圖來清晰地表示出這種關係。

咱們先來看 isa 指針

  1. 水平方向上,每一級中的 實例對象isa 指針 指向了對應的 類對象,而 類對象isa 指針 指向了對應的 元類。而全部元類的 isa 指針 最終指向了 NSObject 元類,所以 NSObject 元類 也被稱爲 根源類
  2. 垂直方向上, 元類isa 指針父類元類isa 指針 都指向了 根元類。而 根源類isa 指針 又指向了本身。

咱們再來看 父類指針

  1. 類對象父類指針 指向了 父類的類對象父類的類對象 又指向了 根類的類對象根類的類對象 最終指向了 nil。
  2. 元類父類指針 指向了 父類對象的元類父類對象的元類父類指針指向了 根類對象的元類,也就是 根元類。而 根元類父親指針 指向了 根類對象,最終指向了 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)。下面,咱們來了解下這三個變量。

  1. 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
複製代碼
  1. 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用來找到函數地址,而後執行函數。

  1. char * method_types; // 方法類型

方法類型 method_types 是個字符串,用來存儲方法的參數類型和返回值類型。

到這裏, Method 的結構就已經很清楚了,MethodSEL(方法名)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]; 後,進行的流程:

  1. 編譯階段:[receiver selector]; 方法被編譯器轉換爲:
    1. objc_msgSend(receiver,selector) (不帶參數)
    2. objc_msgSend(recevier,selector,org1,org2,…)(帶參數)
  2. 運行時階段:消息接受者 recever 尋找對應的 selector
    1. 經過 recevierisa 指針 找到 recevierclass(類)
    2. Class(類)cache(方法緩存) 的散列表中尋找對應的 IMP(方法實現)
    3. 若是在 cache(方法緩存) 中沒有找到對應的 IMP(方法實現) 的話,就繼續在 Class(類)method list(方法列表) 中找對應的 selector,若是找到,填充到 cache(方法緩存) 中,並返回 selector
    4. 若是在 class(類) 中沒有找到這個 selector,就繼續在它的 superclass(父類)中尋找;
    5. 一旦找到對應的 selector,直接執行 recever 對應 selector 方法實現的 IMP(方法實現)
    6. 若找不到對應的 selector,Runtime 系統進入消息轉發機制。
  3. 運行時消息轉發階段:
    1. 動態解析:經過重寫 +resolveInstanceMethod: 或者 +resolveClassMethod:方法,利用 class_addMethod方法添加其餘函數實現;
    2. 消息接受者重定向:若是上一步添加其餘函數實現,可在當前對象中利用 -forwardingTargetForSelector: 方法將消息的接受者轉發給其餘對象;
    3. 消息重定向:若是上一步沒有返回值爲 nil,則利用 -methodSignatureForSelector:方法獲取函數的參數和返回值類型。
      1. 若是 -methodSignatureForSelector: 返回了一個 NSMethodSignature 對象(函數簽名),Runtime 系統就會建立一個 NSInvocation 對象,並經過 -forwardInvocation: 消息通知當前對象,給予這次消息發送最後一次尋找 IMP 的機會。
      2. 若是 -methodSignatureForSelector: 返回 nil。則 Runtime 系統會發出 -doesNotRecognizeSelector: 消息,程序也就崩潰了。

參考資料


以上就是 iOS 開發:『Runtime』詳解(一):基礎知識 的全部內容了。 整篇文章主要就講了一件事:消息發送以及轉發機制的原理和流程。這也是 Runtime 系統的工做原理。

下一篇筆者準備講一下『Runtime』的黑魔法 Method Swizzling


iOS 開發:『Runtime』詳解 系列文章:

還沒有完成:

  • iOS 開發:『Runtime』詳解(五)Crash 防禦系統
  • iOS 開發:『Runtime』詳解(六)Objective-C 2.0 結構解析
  • iOS 開發:『Runtime』詳解(七)KVO 底層實現
相關文章
相關標籤/搜索