iOS Runtime 詳解 基礎(chǔ)篇

閱讀本文將了解下面幾個問題:

  1. 什么是 Runtime?
  2. 消息機制的基本原理
  3. Runtime中的數(shù)據(jù)結(jié)構(gòu)
  4. Runtime消息發(fā)送原理
  5. 消息發(fā)送流程總結(jié)

1 什么是 Runtime?

C/C++ 是靜態(tài)語言的代表,它們在編譯階段就已經(jīng)確定好了要調(diào)用的函數(shù),以及函數(shù)的實現(xiàn),如果函數(shù)未實現(xiàn)就會編譯報錯。

Objective-C 是一個動態(tài)語言,在編譯階段并不知道真正調(diào)用的是哪個函數(shù)。只有在運行時才知道具體會發(fā)生什么。

Objective-C 的動態(tài)性,決定了它不僅需要一個編譯器,也需要一個運行時系統(tǒng)來動態(tài)得創(chuàng)建類和對象、進行消息傳遞和轉(zhuǎn)發(fā)。這個運行時系統(tǒng)就是Runtime,它基本上是用C和匯編寫的一套API,這個庫使C語言有了面向?qū)ο蟮哪芰Α?/p>

2 消息機制的基本原理

OC的方法調(diào)用都是類似[receiver selector]的形式,其實每次都是一個運行時消息發(fā)送過程。

  1. 編譯階段:[receiver selector]方法被編譯器轉(zhuǎn)化:
    1.objc_msgSend(receiver,selector)(不帶參數(shù))
    2.objc_msgSend(recevier,selector,org1,org2,…)(帶參數(shù))

  2. 運行時階段:消息接收者recever尋找對應(yīng)的selector:
    1.recever能找到對應(yīng)的selector,直接執(zhí)行接收receiver對象的selector方法。
    2.recever找不到對應(yīng)的selector,消息被轉(zhuǎn)發(fā)或者臨時向recever添加這個selector對應(yīng)的實現(xiàn)內(nèi)容,否則崩潰。

OC調(diào)用方法[receiver selector],編譯階段確定了要向哪個接收者發(fā)送message消息,但是接收者如何響應(yīng)決定于運行時的判斷。

Objective-C代碼在編譯和運行階段會被轉(zhuǎn)化為Runtime方式運行,Objective-C類、對象和方法等都對應(yīng)了C中的結(jié)構(gòu)體,我們來看一下它們是如何定義的。

3 Runtime中的數(shù)據(jù)結(jié)構(gòu)

Runtime代碼如何查看呢,我們可以通過下面的方式:

1.導(dǎo)入下面的頭文件,然后Command +鼠標右鍵點擊,即可進入Runtime的源碼文件。

#import <objc/runtime.h>

2.我們也可以通過組合鍵 [Cmd + Shift + O ] ,搜索相應(yīng)的文件進入。

下面我們就分析一下Objective-C代碼在C中對應(yīng)的結(jié)構(gòu)。

3.1 objc_msgSend

上面也提到過,Objective-C方法調(diào)用在編譯時都會轉(zhuǎn)化為對應(yīng)C函數(shù)的調(diào)用:objc_msgSend(receiver,selector)。

3.2 Object(實例)

objc/objc.h中,我們來看一下Object(對象),是如何定義的:

/// Represents an instance of a class.
struct objc_object {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;
};

/// A pointer to an instance of a class.
typedef struct objc_object *id;

我們知道id是一種通用的對象類型,它可以指向?qū)儆谌魏晤惖膶ο?。在這里id被定義為一個指向 objc_object結(jié)構(gòu)體的指針。

objc_object結(jié)構(gòu)體只包含一個Class類型的isa 指針,也就是說,一個Object(對象)唯一保存的就是它所屬Class(類) 的地址。下面我們看一下 Class是如何定義的。

3.3 Class(類)

objc/objc.h中,可以看到Class是一個指向objc_class結(jié)構(gòu)體的指針:

typedef struct objc_class *Class;

objc/runtime.h中,是objc_class結(jié)構(gòu)體的具體定義:

struct objc_class {
        Class _Nonnull isa  OBJC_ISA_AVAILABILITY;

    #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             // 遵守的協(xié)議列表;
    #endif

    } OBJC2_UNAVAILABLE;

我們可以看到objc_class結(jié)構(gòu)體中定義了很多的成員變量:指向父類的指針、類的名字、版本、實例大小、實例變量列表、方法列表、緩存、遵守的協(xié)議列表等。這個結(jié)構(gòu)體存放的數(shù)據(jù)稱為元數(shù)據(jù)(metadata)。

我們還能注意到objc_class結(jié)構(gòu)體中也有一個isa指針。這就說明了Class本身其實也是一個對象,因此我們稱之為類對象,類對象在編譯期產(chǎn)生,用于創(chuàng)建實例對象,是單例。

3.4 元類(Meta Class):

我們可以發(fā)現(xiàn)實例對象和類對象結(jié)構(gòu)體中都擁有一個isa指針,實例對象的isa指針指向他所屬的類(Class),那么類對象的isa指針指向哪兒里呢?

類對象的isa指針指向了元類,元類(Meta Class)是一個類對象的類。在上面我們提到,所有的類自身也是一個對象,我們可以向這個對象發(fā)送消息(即調(diào)用類方法)。
為了調(diào)用類方法,這個類的isa指針必須指向一個包含這些類方法的一個objc_class結(jié)構(gòu)體。這就引出了meta-class的概念,元類中保存了創(chuàng)建類對象以及類方法所需的所有信息。

3.5 實例對象、類、元類之間的關(guān)系

上面,我們已經(jīng)了解了 實例對象(Object)、類(Class)、Meta Class(元類) 的基本概念。

下面,我們通過一張圖,來清晰的了解下它們之間的繼承關(guān)系,以及isa的指向關(guān)系:

isa指針指向:

  1. 實例對象的isa指針指向了對應(yīng)的類對象,而類對象的isa指針指向了對應(yīng)的元類。
  2. 所有元類的isa指針最終指向了NSObject元類,因此NSObject元類也被稱為根元類。根元類的isa指針又指向了自己。

super_class指針指向:

  1. 類對象的super_class指針指向了父類對象,父類對象又指向了根類對象,根類對象最終指向了nil
  2. 元類的super_class指針指向了父元類。父元類又指向了根元類。而根元類的super_class指針指向了根類對象,最終指向了nil。

3.6 Method(方法)

object_classmethodLists(方法列表)存放的元素就是Method(方法)。

objc/runtime.h中,看下定義:

/// 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                     // 方法實現(xiàn);
}

下面,我們來了解下它的三個成員變量。

1. SEL(方法名)

/// An opaque type that represents a method selector.
typedef struct objc_selector *SEL;

SEL是一個指向objc_selector結(jié)構(gòu)體的指針,然而我們并不能在Runtime中找到它的結(jié)構(gòu)體的詳細定義。Objective-C在編譯時,會依據(jù)每一個方法的名字、參數(shù)序列,生成一個唯一的整型標識(Int類型的地址),這個標識就是SEL。

2. IMP(方法實現(xiàn))

/// 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的實質(zhì)是一個函數(shù)指針,它指向了方法實現(xiàn)的首地址。IMP用來找到函數(shù)地址,然后執(zhí)行函數(shù)。

3. char * method_types (方法類型)

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

到這里,Method的結(jié)構(gòu)就已經(jīng)很清楚了,MethodSEL(方法名) 和IMP(函數(shù)指針)關(guān)聯(lián)起來,當(dāng)對一個對象發(fā)送消息時,會通過給出的SEL(方法名)去找到IMP(函數(shù)指針),然后執(zhí)行。

3.7 類緩存(objc_cache)

objc_cache用于緩存最近使用的方法。一個類只有一部分方法是常用的,每次調(diào)用一個方法之后,這個方法就被緩存到objc_cache中,下次調(diào)用時runtime會先在objc_cache中查找,如果objc_cache中沒有,才會去methodList中查找。相比直接在類的方法列表中遍歷查找,效率更高。

4 深入理解Rutime消息發(fā)送

我們在分析了OC語言對應(yīng)的底層C結(jié)構(gòu)之后,現(xiàn)在可以進一步理解運行時的消息發(fā)送機制。先前講到,OC調(diào)用方法被編譯轉(zhuǎn)化為如下的形式:

id _Nullable objc_msgSend(id _Nullable self, SEL _Nonnull op, ...)

最后一步中我們提到:若找不到對應(yīng)的selector,消息被轉(zhuǎn)發(fā)或者臨時向recevier添加這個selector應(yīng)的實現(xiàn)方法,否則就會發(fā)生崩潰。

當(dāng)一個方法找不到的時候,Runtime提供了 消息動態(tài)解析消息接受者重定向、消息重定向 三種方法來處理,這三種方法的調(diào)用關(guān)系如下圖:

4.1 動態(tài)方法解析(Dynamic Method Resolution)

所謂動態(tài)解析,我們可以理解為通過cache和方法列表沒有找到方法時,Runtime為我們提供一次動態(tài)添加方法實現(xiàn)的機會,主要用到的方法如下:

//OC方法:
//類方法未找到時調(diào)起,可于此添加類方法實現(xiàn)
+ (BOOL)resolveClassMethod:(SEL)sel
//實例方法未找到時調(diào)起,可于此添加實例方法實現(xiàn)
+ (BOOL)resolveInstanceMethod:(SEL)sel

//Runtime方法:
/**
 運行時方法:向指定類中添加特定方法實現(xiàn)的操作
 @param cls 被添加方法的類
 @param name selector方法名
 @param imp 指向?qū)崿F(xiàn)方法的函數(shù)指針
 @param types imp函數(shù)實現(xiàn)的返回值與參數(shù)類型
 @return 添加方法是否成功
 */
BOOL class_addMethod(Class _Nullable cls,
                     SEL _Nonnull name,
                     IMP _Nonnull imp,
                     const char * _Nullable types)

下面使用一個示例來說明動態(tài)解析:ViewController類中聲明方法卻未添加實現(xiàn),我們通過Runtime動態(tài)方法解析的操作為其添加方法實現(xiàn),具體代碼如下:

#import "ViewController.h"
#import <objc/runtime.h>

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    [[self class] performSelector:@selector(run)];
    [self performSelector:@selector(walk)];
}

//重寫父類方法:處理類方法
+ (BOOL)resolveClassMethod:(SEL)sel {
    if(sel == @selector(run)){
        class_addMethod(object_getClass(self), sel, class_getMethodImplementation(object_getClass(self), @selector(ssl_run)), "v@");
        return YES;   //添加函數(shù)實現(xiàn),返回YES
    }
    return [class_getSuperclass(self) resolveClassMethod:sel];
}

//重寫父類方法:處理實例方法
+ (BOOL)resolveInstanceMethod:(SEL)sel {
    if(sel == @selector(walk)){
        class_addMethod([self class], sel, class_getMethodImplementation([self class], @selector(ssl_walk)), "v@");
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}

+ (void)ssl_run {
    NSLog(@"%s ",__func__);
}

- (void)ssl_walk {
    NSLog(@"%s ",__func__);
}

運行程序,代碼沒有崩潰,并打印如下結(jié)果:

+[ViewController ssl_run]
-[ViewController ssl_walk]

class_addMethod 方法中的特殊參數(shù)“v@”,具體可參考官方文檔中關(guān)于 Type Encodings 的說明:點擊查看

這里+resolveInstanceMethod:或者 +resolveClassMethod:無論是返回YES,還是返回NO,只要其中沒有添加其他函數(shù)實現(xiàn),Runtime都會進行下一步:消息接受者重定向。

4.2 消息接收者重定向

這一步會調(diào)用下面兩個方法:

//重定向類方法的消息接收者,返回一個類
+ (id)forwardingTargetForSelector:(SEL)aSelector

//重定向?qū)嵗椒ǖ南⒔邮苷?,返回一個實例對象
- (id)forwardingTargetForSelector:(SEL)aSelector

如果當(dāng)前對象實現(xiàn)了這兩個方法,Runtime就會調(diào)用這兩個方法,允許我們將消息的接受者轉(zhuǎn)發(fā)給其他對象。

下面使用一個示例來說明消息接收者的重定向:

#import "ViewController.h"
#import <objc/runtime.h>

@interface Person : NSObject

@end

@implementation Person

+ (void)run {
    NSLog(@"%s ",__func__);
}

- (void)walk {
    NSLog(@"%s ",__func__);
}

@end

@interface ViewController ()

@property (nonatomic, strong) Person *person;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    [[self class] performSelector:@selector(run)];
    [self performSelector:@selector(walk)];
}

//重定向類方法:返回一個類對象
+ (id)forwardingTargetForSelector:(SEL)aSelector {
    if (aSelector == @selector(run)) {
         
        return [Person class];
    }
    return [super forwardingTargetForSelector:aSelector];
}

//重定向?qū)嵗椒ǎ悍祷仡惖膶嵗?- (id)forwardingTargetForSelector:(SEL)aSelector {
    if (aSelector == @selector(walk)) {
        return self.person;
    }
    return [super forwardingTargetForSelector:aSelector];
}

- (Person *)person {
    if (!_person) {
        _person = [Person new];
    }
    return _person;
}

@end

代碼沒有崩潰,并打印如下結(jié)果:

+[ViewController ssl_run]
-[ViewController ssl_walk]

動態(tài)方法解析階段無效時,我們可以通過forwardingTargetForSelector修改消息的接收者,該方法返回參數(shù)是一個對象,如果這個對象是非nil,非self,系統(tǒng)會將運行的消息轉(zhuǎn)發(fā)給這個對象執(zhí)行。否則,進行下一步:消息重定向。

4.3 消息重定向

這一步中首先會發(fā)送-methodSignatureForSelector:消息獲得函數(shù)的參數(shù)和返回值類型。如果-methodSignatureForSelector:返回nil ,Runtime則會發(fā)出-doesNotRecognizeSelector: 消息,程序這時也就掛掉了。如果返回了一個函數(shù)簽名,Runtime就會創(chuàng)建一個NSInvocation對象并發(fā)送 -forwardInvocation:消息給目標對象。

看下方法的定義:

// 獲取類方法函數(shù)的參數(shù)和返回值類型,返回簽名
+ (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector;

// 類方法消息重定向
+ (void)forwardInvocation:(NSInvocation *)anInvocation;

// 獲取對象方法函數(shù)的參數(shù)和返回值類型,返回簽名
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector;

// 對象方法消息重定向
- (void)forwardInvocation:(NSInvocation *)anInvocation;

舉個例子:

#import "ViewController.h"
#import <objc/runtime.h>

@interface Person : NSObject

@end

@implementation Person

- (void)walk {
    NSLog(@"%s ",__func__);
}

@end

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    // 執(zhí)行walk函數(shù)
    [self performSelector:@selector(walk)];
}

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    return YES;// 返回YES,進入下一步轉(zhuǎn)發(fā)
}

- (id)forwardingTargetForSelector:(SEL)aSelector {
    return nil;// 返回nil,進入下一步轉(zhuǎn)發(fā)
}

// 獲取函數(shù)的參數(shù)和返回值類型,返回簽名
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    if ([NSStringFromSelector(aSelector) isEqualToString:@"walk"]) {
        return [NSMethodSignature signatureWithObjCTypes:"v@:"];// 簽名,進入forwardInvocation
    }
    
    return [super methodSignatureForSelector:aSelector];
}

// 消息重定向
- (void)forwardInvocation:(NSInvocation *)anInvocation {
    SEL sel = anInvocation.selector;// 從anInvocation中獲取消息

    Person *p = [Person new];
    if([p respondsToSelector:sel]) {
        [anInvocation invokeWithTarget:p];// 將消息轉(zhuǎn)發(fā)給其他對象處理
    }
    else {
        [self doesNotRecognizeSelector:sel];// 報錯,代碼崩潰
    }
}

@end

代碼沒有崩潰,并打印如下結(jié)果:

-[Person walk]

這一步中,通過簽名,Runtime生成了一個對象anInvocation,發(fā)送給了forwardInvocation,我們在forwardInvocation方法里面讓Person對象去執(zhí)行了walk函數(shù)。

以上就是Runtime的三次轉(zhuǎn)發(fā)流程。

5 消息發(fā)送流程總結(jié)

調(diào)用[receiver selector]后,進行的流程:

  1. 編譯階段:[receiver selector]方法被編譯器轉(zhuǎn)化:

    1. objc_msgSend(receiver,selector)(不帶參數(shù))。
    2. objc_msgSend(recevier,selector,org1,org2,…)(帶參數(shù))。
  2. 運行時階段:recevier尋找對應(yīng)的selector

    1. 通過recevierisa指針找到recevierclass(類)。
    2. Class(類)的cache(方法緩存)中尋找對應(yīng)的selector。
    3. 如果在cache(方法緩存)中沒有找到對應(yīng)的 selector ,就繼續(xù)在Class(類)的methodList(方法列表)中查找,如果找到,緩存到cache 中,并返回selector。
    4. 如果在class(類)中沒有找到這個selector,就繼續(xù)在它的superclass(父類)中尋找。
    5. 一旦找到selector,直接執(zhí)行相關(guān)聯(lián)的IMP(方法實現(xiàn))。
    6. 若找不到對應(yīng)的selector,Runtime系統(tǒng)進入消息轉(zhuǎn)發(fā)階段。
  3. 消息轉(zhuǎn)發(fā)階段:

    1. 動態(tài)解析:通過重寫+resolveInstanceMethod: 或者+resolveClassMethod:方法,利用 class_addMethod方法添加其他函數(shù)實現(xiàn)。
    2. 消息接受者重定向:如果上一步?jīng)]有添加其他函數(shù)實現(xiàn),可在當(dāng)前對象中利用 forwardingTargetForSelector:方法將消息的接受者轉(zhuǎn)發(fā)給其他對象。
    3. 消息重定向:如果上一步返回值為nil,則利用 methodSignatureForSelector:方法獲取函數(shù)的參數(shù)和返回值類型。
      1. 如果methodSignatureForSelector:返回nil。則 Runtime系統(tǒng)會發(fā)出doesNotRecognizeSelector:消息,程序也就崩潰了。
      2. 如果methodSignatureForSelector:返回了一個函數(shù)簽名,Runtime就會創(chuàng)建一個NSInvocation對象并發(fā)送-forwardInvocation:消息給目標對象。

參考資料

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容