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

本文用來介紹 iOS 開發中『Runtime』相關的基礎知識。通過本文,您將瞭解到:

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

1. 什麼是 Runtime?

我們都知道,將源代碼轉換爲可執行的程序,通常要經過三個步驟:編譯鏈接運行。不同的編譯語言,在這三個步驟中所進行的操作又有些不同。

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

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

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

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

Runtime 實際上是一個庫,這個庫使我們可以在程序運行時動態的創建對象、檢查對象,修改類和對象的方法。


2. 消息機制的基本原理

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

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

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

類方法的調用過程 和對象方法調用差不多,流程如下:

  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(方法)

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(方法實現)。下面,我們來了解下這三個變量。

  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,消息被轉發或者臨時向 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]; 後,進行的流程:

  1. 編譯階段:[receiver selector]; 方法被編譯器轉換爲:
    1. objc_msgSend(receiver,selector) (不帶參數)
    2. objc_msgSend(recevier,selector,org1,org2,…)(帶參數)
  2. 運行時階段:消息接受者 recevier 尋找對應的 selector
    1. 通過 recevierisa 指針 找到 recevierclass(類)
    2. Class(類)cache(方法緩存) 的散列表中尋找對應的 IMP(方法實現)
    3. 如果在 cache(方法緩存) 中沒有找到對應的 IMP(方法實現) 的話,就繼續在 Class(類)method list(方法列表) 中找對應的 selector,如果找到,填充到 cache(方法緩存) 中,並返回 selector
    4. 如果在 class(類) 中沒有找到這個 selector,就繼續在它的 superclass(父類)中尋找;
    5. 一旦找到對應的 selector,直接執行 recevier 對應 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』的實戰應用。


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

尚未完成:

  • iOS 開發:『Runtime』詳解(五)Crash 防護系統
  • iOS 開發:『Runtime』詳解(六)Objective-C 2.0 結構解析
  • iOS 開發:『Runtime』詳解(七)KVO 底層實現

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章