iOS Runtime 詳解(一)

>Runtime 介紹

C 語言 作為一門靜態(tài)類語言,在編譯階段就已經(jīng)確定了所有變量的數(shù)據(jù)類型,同時(shí)也確定好了要調(diào)用的函數(shù),以及函數(shù)的實(shí)現(xiàn)。而 Objective-C 語言是一門動(dòng)態(tài)語言。在編譯階段并不知道變量的具體數(shù)據(jù)類型,也不知道所真正調(diào)用的哪個(gè)函數(shù)。只有在運(yùn)行時(shí)間才檢查變量的數(shù)據(jù)類型,同時(shí)在運(yùn)行時(shí)才會(huì)根據(jù)函數(shù)名查找要調(diào)用的具體函數(shù)。這樣在程序沒運(yùn)行的時(shí)候,我們并不知道調(diào)用一個(gè)方法具體會(huì)發(fā)生什么。

Objective-C 擴(kuò)展了 C 語言,并加入了面向?qū)ο筇匦院?Smalltalk 式的消息傳遞機(jī)制。而這個(gè)擴(kuò)展的核心是一個(gè)用 C 和 編譯語言 寫的 Runtime 庫(kù)。它是 Objective-C 面向?qū)ο蠛蛣?dòng)態(tài)機(jī)制的基石。

Objective-C 是一個(gè)動(dòng)態(tài)語言,這意味著它不僅需要一個(gè)編譯器,也需要一個(gè)運(yùn)行時(shí)系統(tǒng)來動(dòng)態(tài)得創(chuàng)建類和對(duì)象、進(jìn)行消息傳遞和轉(zhuǎn)發(fā)。理解 Objective-C 的 Runtime 機(jī)制可以幫我們更好的了解這個(gè)語言,適當(dāng)?shù)臅r(shí)候還能對(duì)語言進(jìn)行擴(kuò)展,從系統(tǒng)層面解決項(xiàng)目中的一些設(shè)計(jì)或技術(shù)問題。了解 Runtime ,要先了解它的核心 - 消息傳遞 (Messaging)。

Runtime其實(shí)有兩個(gè)版本: modernlegacy。我們現(xiàn)在用的 Objective-C 2.0 采用的是現(xiàn)行 (Modern) 版的 Runtime 系統(tǒng),只能運(yùn)行在 iOS 和 macOS 10.5 之后的 64 位程序中。而 macOS 較老的32位程序仍采用 Objective-C 1 中的(早期)Legacy版本的 Runtime 系統(tǒng)。這兩個(gè)版本最大的區(qū)別在于當(dāng)你更改一個(gè)類的實(shí)例變量的布局時(shí),在早期版本中你需要重新編譯它的子類,而現(xiàn)行版就不需要。

Runtime 基本是用 C匯編寫的,可見蘋果為了動(dòng)態(tài)系統(tǒng)的高效而作出的努力。你可以在這里下到蘋果維護(hù)的開源代碼。蘋果和GNU各自維護(hù)一個(gè)開源的 runtime 版本,這兩個(gè)版本之間都在努力的保持一致。平時(shí)的業(yè)務(wù)中主要是使用官方Api,解決我們框架性的需求。

高級(jí)編程語言想要成為可執(zhí)行文件需要先編譯為匯編語言再匯編為機(jī)器語言,機(jī)器語言也是計(jì)算機(jī)能夠識(shí)別的唯一語言,但是OC并不能直接編譯為匯編語言,而是要先轉(zhuǎn)寫為純C語言再進(jìn)行編譯和匯編的操作,從OCC語言的過渡就是由runtime來實(shí)現(xiàn)的。然而我們使用OC進(jìn)行面向?qū)ο箝_發(fā),而C語言更多的是面向過程開發(fā),這就需要將面向?qū)ο蟮念愞D(zhuǎn)變?yōu)槊嫦蜻^程的結(jié)構(gòu)體。

>數(shù)據(jù)結(jié)構(gòu)

runtime數(shù)據(jù)結(jié)構(gòu).png

>消息機(jī)制的基本原理

基本消息發(fā)送框架.png

Objective-C 語言 中,對(duì)象方法調(diào)用都是類似[receiver selector];的形式,其本質(zhì)就是讓對(duì)象在運(yùn)行時(shí)發(fā)送消息的過程。編譯器轉(zhuǎn)成消息發(fā)送objc_msgSend(receiver, selector)objc_msgSend(recevier,selector,org1,org2,…),runtime時(shí)執(zhí)行的流程是這樣的:

  • 通過recevierisa 指針找到recevierClass(類);
  • Classcache(方法緩存)的散列表中尋找對(duì)應(yīng)的IMP(方法實(shí)現(xiàn))
  • 如果在cache中沒有找到對(duì)應(yīng)的IMP,就繼續(xù)在Classmethod list(方法列表)中找對(duì)應(yīng)的selector,如果找到則填充到 cache中,并返回selector;
  • 如果在Class中沒有找到這個(gè)selector,就繼續(xù)在它的 superClass(父類)中尋找;
  • 一旦找到對(duì)應(yīng)的selector,直接執(zhí)行recevier對(duì)應(yīng)selector方法實(shí)現(xiàn)的IMP
  • 若找不到對(duì)應(yīng)的selector,消息被轉(zhuǎn)發(fā)或者臨時(shí)向recevier添加這個(gè)selector對(duì)應(yīng)的實(shí)現(xiàn)方法,否則就會(huì)發(fā)生崩潰。

但這種實(shí)現(xiàn)有個(gè)問題,效率低。一個(gè)class往往只有20%的函數(shù)會(huì)被經(jīng)常調(diào)用,可能占總調(diào)用次數(shù)的80%。每個(gè)消息都需要遍歷一次objc_method_list并不合理。如果把經(jīng)常被調(diào)用的函數(shù)緩存下來,那可以大大提高函數(shù)查詢的效率。這也就是objc_class中另一個(gè)重要成員objc_cache做的事情 - 再找到selector之后,把selectormethod_name作為key,method_imp作為value給存起來。當(dāng)再次收到recevier消息的時(shí)候,可以直接在cache里找到,避免去遍歷objc_method_list。從前面的源代碼可以看到objc_cache是存在objc_class結(jié)構(gòu)體中的。

objec_msgSend的方法定義如下:

OBJC_EXPORT id objc_msgSend(id self, SEL op, ...)

>Runtime 中的概念解析

  • 類對(duì)象(objc_class)
struct objc_class {
    // objc_class 結(jié)構(gòu)體的實(shí)例指針
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;

#if !__OBJC2__
    // 指向父類的指針
    Class _Nullable super_class                                     OBJC2_UNAVAILABLE;
    // 類的名字
    const char * _Nonnull name                                      OBJC2_UNAVAILABLE;
    // 類的版本信息,默認(rèn)為 0
    long version                                                    OBJC2_UNAVAILABLE;
    // 類的信息,供運(yùn)行期使用的一些位標(biāo)識(shí)
    long info                                                       OBJC2_UNAVAILABLE;
    // 該類的實(shí)例變量大小;
    long instance_size                                              OBJC2_UNAVAILABLE;
    // 該類的實(shí)例變量列表
    struct objc_ivar_list * _Nullable ivars                         OBJC2_UNAVAILABLE;
    // 方法定義的列表
    struct objc_method_list * _Nullable * _Nullable methodLists     OBJC2_UNAVAILABLE;
    // 方法緩存
    struct objc_cache * _Nonnull cache                              OBJC2_UNAVAILABLE;
    // 遵守的協(xié)議列表
    struct objc_protocol_list * _Nullable protocols                 OBJC2_UNAVAILABLE;
#endif

} OBJC2_UNAVAILABLE;

類對(duì)象就是一個(gè)結(jié)構(gòu)體struct objc_class,這個(gè)結(jié)構(gòu)體存放的數(shù)據(jù)稱為元數(shù)據(jù)(metadata),該結(jié)構(gòu)體的第一個(gè)成員變量也是isa指針,這就說明了Class本身其實(shí)也是一個(gè)對(duì)象,因此我們稱之為類對(duì)象,類對(duì)象在編譯期產(chǎn)生用于創(chuàng)建實(shí)例對(duì)象,是單例。

  • 實(shí)例(objc_object)

Object(對(duì)象)被定義為objc_object結(jié)構(gòu)體,其數(shù)據(jù)結(jié)構(gòu)如下:

/// Represents an instance of a class.
struct objc_object {
    // objc_object 結(jié)構(gòu)體的實(shí)例指針
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;
};

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

這里的id被定義為一個(gè)指向objc_object 結(jié)構(gòu)體的指針。從中可以看出objc_object 結(jié)構(gòu)體只包含一個(gè)Class類型的isa 指針。換句話說,一個(gè)Object(對(duì)象)唯一保存的就是它所屬 Class(類)的地址。當(dāng)我們對(duì)一個(gè)對(duì)象,進(jìn)行方法調(diào)用時(shí),比如[receiver selector];,它會(huì)通過objc_object 結(jié)構(gòu)體isa 指針去找對(duì)應(yīng)的object_class 結(jié)構(gòu)體,然后在object_class 結(jié)構(gòu)體methodLists(方法列表)中找到我們調(diào)用的方法,然后執(zhí)行。

  • 元類(Meta Class)

對(duì)象(objc_object 結(jié)構(gòu)體)isa 指針指向的是對(duì)應(yīng)的類對(duì)象(object_class 結(jié)構(gòu)體)。而類對(duì)象(object_class 結(jié)構(gòu)體)isa 指針實(shí)際上指向的的是類對(duì)象自身的Meta Class(元類)。

Meta Class(元類)就是一個(gè)類對(duì)象所屬的類。一個(gè)對(duì)象所屬的類叫做類對(duì)象,而一個(gè)類對(duì)象所屬的類就叫做元類。

Runtime 中把類對(duì)象所屬類型就叫做Meta Class(元類),用于描述類對(duì)象本身所具有的特征,而在元類methodLists中,保存了類的方法鏈表,即所謂的類方法。并且類對(duì)象中的isa 指針指向的就是元類。每個(gè)類對(duì)象有且僅有一個(gè)與之相關(guān)的元類。

對(duì)象方法的調(diào)用過程,我們是通過對(duì)象的isa 指針找到對(duì)應(yīng)的 Class(類);然后在Class(類)method list(方法列表)中找對(duì)應(yīng)的selector。而 類方法的調(diào)用過程 和對(duì)象方法調(diào)用差不多,流程如下:

  • 通過類對(duì)象isa 指針找到所屬的Meta Class(元類);
  • Meta Class(元類)method list(方法列表)中找到對(duì)應(yīng)的selector
  • 執(zhí)行對(duì)應(yīng)的selector

下面我們通過一張圖來清晰地表示出實(shí)例對(duì)象(Object)、類(Class)Meta Class(元類)的指向關(guān)系。

object-class-meta.png

通過上圖我們可以看出整個(gè)體系構(gòu)成了一個(gè)自閉環(huán),struct objc_object結(jié)構(gòu)體實(shí)例它的isa指針指向類對(duì)象類對(duì)象isa指針指向了元類,super_class指針指向了父類的類對(duì)象,而元類super_class指針指向了父類的元類,那元類的isa指針又指向了自己。

元類(Meta Class)是一個(gè)類對(duì)象的類。

在上面我們提到,所有的類自身也是一個(gè)對(duì)象,我們可以向這個(gè)對(duì)象發(fā)送消息(即\color{#45B787}{調(diào)用類方法})。為了調(diào)用類方法,這個(gè)類的isa指針必須指向一個(gè)包含這些類方法的一個(gè)objc_class結(jié)構(gòu)體。這就引出了meta-class的概念,元類中保存了創(chuàng)建類對(duì)象以及類方法所需的所有信息。任何NSObject繼承體系下的meta-class都使用NSObjectmeta-class作為自己的所屬類,而基類的meta-classisa指針是指向它自己。

@interface NSObject (Test)

+ (void)foo;

@end


@implementation NSObject (Test)

- (void) foo {
    NSLog(@"%@",NSStringFromSelector(_cmd));
    return;
}
@end

int main(int argc, char * argv[]) {
    @autoreleasepool {
        [NSObject foo];
        [[NSObject new] foo];
    }
    return 0;
}
輸出結(jié)果:
foo
foo

解析:類的實(shí)例方法是存儲(chǔ)在類的methodLists中,而類方法則是存儲(chǔ)在元類的methodLists中,NSObject元類superclass是指向Class,當(dāng)調(diào)用[NSObject foo]的時(shí)候,因?yàn)檫@是一個(gè)類方法調(diào)用,所以從元類中查找簽名為foo的方法,沒有發(fā)現(xiàn),然后再沿superclass繼續(xù)查找,結(jié)果在Class中查找到該方法,于是調(diào)用該方法輸出。但如果將NSObject的分類換成其他類的分類(如NSString),會(huì)發(fā)現(xiàn)程序崩潰,這是因?yàn)楹灻麨?code>foo的函數(shù)在NSString中,而當(dāng)我們進(jìn)行類方法調(diào)用的時(shí)候,最后會(huì)查找到NSObjectClass中,但該Class中并沒有對(duì)應(yīng)的方法簽名,于是再沿superclass向上查找,由于NSObjectsuperclassnil,于是拋出unrecognized selector。

  • Method(objc_method)

object_class 結(jié)構(gòu)體methodLists(方法列表)中存放的元素就是Method(方法)。在objc/runtime.h中,表示Method(方法)objc_method 結(jié)構(gòu)體的數(shù)據(jù)結(jié)構(gòu):

/// An opaque type that represents a method in a class definition.
/// 代表類定義中一個(gè)方法的不透明類型
typedef struct objc_method *Method;

struct objc_method {
    // 方法名
    SEL _Nonnull method_name                                 OBJC2_UNAVAILABLE;
    // 方法類型
    char * _Nullable method_types                            OBJC2_UNAVAILABLE;
     // 方法實(shí)現(xiàn)
    IMP _Nonnull method_imp                                  OBJC2_UNAVAILABLE;
}                                                            OBJC2_UNAVAILABLE;

objc_method 結(jié)構(gòu)體中包含了method_name(方法名)method_types(方法類型)method_imp(方法實(shí)現(xiàn))。說明SELIMP其實(shí)都是Method的屬性。下面,我們來了解下這三個(gè)變量。

1. SEL(objc_selector)
Objc.h
/// An opaque type that represents a method selector.代表一個(gè)方法的不透明類型
typedef struct objc_selector *SEL;

objc_msgSend函數(shù)第二個(gè)參數(shù)類型為SEL,它是selectorObjective-C中的表示類型(Swift中是Selector類)。selector是方法選擇器,可以理解為區(qū)分方法的ID,而這個(gè)ID的數(shù)據(jù)結(jié)構(gòu)是SEL。SEL是一個(gè)指向objc_selector 結(jié)構(gòu)體的指針。
runtime 相關(guān)頭文件中并沒有找到明確的定義。不過,通過測(cè)試我們可以得出:SEL只是一個(gè)保存方法名的字符串。

SEL sel = @selector(viewDidLoad);
NSLog(@"%s", sel);              // 輸出:viewDidLoad
SEL sel1 = @selector(test);
NSLog(@"%s", sel1);             // 輸出:test
@property SEL selector;

可以看到selectorSEL的一個(gè)實(shí)例。

A method selector is a C string that has been registered (or “mapped“) with the Objective-C runtime. Selectors generated by the compiler are automatically mapped by the runtime when the class is loaded.

其實(shí)selector就是個(gè)映射到方法的C字符串,你可以用Objective-C編譯器命令@selector()或者Runtime系統(tǒng)的sel_registerName函數(shù)來獲得一個(gè)SEL類型的方法選擇器。

selector既然是一個(gè)string,是類似className+method的組合,命名規(guī)則有兩條:

  • 同一個(gè)類,selector不能重復(fù)
  • 不同的類,selector可以重復(fù)

這也帶來了一個(gè)弊端,我們?cè)趯?code>C代碼的時(shí)候,經(jīng)常會(huì)用到函數(shù)重載,就是函數(shù)名相同,參數(shù)不同,但是這在Objective-C中是行不通的,因?yàn)?code>selector只記了methodname,沒有參數(shù),所以沒法區(qū)分不同的method
比如:

- (void)caculate(NSInteger)num;
- (void)caculate(CGFloat)num;

是會(huì)報(bào)錯(cuò)的。
我們只能通過命名來區(qū)別:

- (void)caculateWithInt(NSInteger)num;
- (void)caculateWithFloat(CGFloat)num;

在不同類中相同名字的方法所對(duì)應(yīng)的方法選擇器是相同的,即使方法名字相同而變量類型不同也會(huì)導(dǎo)致它們具有相同的方法選擇器。

2. 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的實(shí)質(zhì)是一個(gè)函數(shù)指針,所指向的就是方法的實(shí)現(xiàn)。IMP用來找到函數(shù)地址,然后執(zhí)行函數(shù)。在iOSRuntime中,Method通過selectorIMP兩個(gè)屬性,實(shí)現(xiàn)了快速查詢方法及實(shí)現(xiàn),相對(duì)提高了性能,又保持了靈活性。

IMP和SEL關(guān)系:

每一個(gè)繼承于NSObject的類都能自動(dòng)獲得runtime的支持。在這樣的一個(gè)類中有一個(gè)isa指針指向該類定義的數(shù)據(jù)結(jié)構(gòu)體,這個(gè)結(jié)構(gòu)體是由編譯器編譯時(shí)為類(需繼承于NSObject)創(chuàng)建的。在這個(gè)結(jié)構(gòu)體中有包括了指向其父類類定義的指針以及 Dispatch table。Dispatch table是一張SELIMP的對(duì)應(yīng)表。也就是說方法編號(hào)SEL最后還是要通過Dispatch table表尋找到對(duì)應(yīng)的IMPIMP就是一個(gè)函數(shù)指針,然后執(zhí)行這個(gè)方法:
1)通過方法獲得方法的編號(hào):SEL methodId=@selector(methodName);或者SEL methodId = NSSelectorFromString(methodName);
2)通過方法編號(hào)執(zhí)行該編號(hào)的方法:[self performSelector:methodId withObject:nil];
3)通過方法編號(hào)獲取該編號(hào)的方法名 NSString *methodName = NSStringFromSelector(methodId);
4)通過方法編號(hào)獲得IMPIMP methodPoint = [self methodForSelector:methodId];
5)執(zhí)行IMPvoid (*func)(id, SEL, id) = (void *)imp;func(self, methodName,param);

注意:如果方法沒有傳入?yún)?shù)時(shí):void (*func)(id, SEL) = (void *)imp;、func(self, methodName);。如果方法傳入一個(gè)參數(shù)時(shí):void (*func)(id, SEL,id) = (void *)imp;、func(self, methodName,param);。如果方法傳入倆個(gè)參數(shù)時(shí):void (*func)(id, SEL,id,id) = (void *)imp;func(self, methodName,param1,param2);。

想更深入了解 IMP 的小伙伴請(qǐng)戳這里。

3. char *method_types

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

  • 類緩存(objc_cache)

當(dāng)Objective-C運(yùn)行時(shí)通過跟蹤它的isa 指針檢查對(duì)象時(shí),它可以找到一個(gè)實(shí)現(xiàn)許多方法的對(duì)象。然而,你可能只調(diào)用它們的一小部分,并且每次查找時(shí),搜索所有選擇器的類分派表沒有意義。所以類實(shí)現(xiàn)一個(gè)緩存,每當(dāng)你搜索一個(gè)類分派表,并找到相應(yīng)的選擇器,它把它放入它的緩存。所以當(dāng)objc_msgSend查找一個(gè)類的選擇器,它首先搜索類緩存。這是基于這樣的理論:如果你在類上調(diào)用一個(gè)消息,你可能以后再次調(diào)用該消息。

為了加速消息分發(fā), 系統(tǒng)會(huì)對(duì)方法和對(duì)應(yīng)的地址進(jìn)行緩存,就放在上述的objc_cache,所以在實(shí)際運(yùn)行中,大部分常用的方法都是會(huì)被緩存起來的,Runtime系統(tǒng)實(shí)際上非常快,接近直接執(zhí)行內(nèi)存地址的程序速度。

  • Runtime消息轉(zhuǎn)發(fā)

進(jìn)行一次發(fā)送消息會(huì)在相關(guān)的類對(duì)象中搜索方法列表,如果找不到則會(huì)沿著繼承樹向上一直搜索知道繼承樹根部(通常為NSObject),如果還是找不到并且消息轉(zhuǎn)發(fā)都失敗了就回執(zhí)行doesNotRecognizeSelector:方法報(bào)unrecognized selector錯(cuò)。那么消息轉(zhuǎn)發(fā)到底是什么呢?接下來將會(huì)逐一介紹最后的三次機(jī)會(huì)。

  • 動(dòng)態(tài)方法解析(消息動(dòng)態(tài)解析)
  • 備用接收者(消息接受者重定向)
  • 完整消息轉(zhuǎn)發(fā)(消息重定向)
消息轉(zhuǎn)發(fā)流程簡(jiǎn)圖.png
1.動(dòng)態(tài)方法解析(消息動(dòng)態(tài)解析)

首先,Objective-C運(yùn)行時(shí)會(huì)調(diào)用+resolveInstanceMethod:或者+resolveClassMethod:,讓你有機(jī)會(huì)提供一個(gè)函數(shù)實(shí)現(xiàn)。前者在對(duì)象方法未找到時(shí)調(diào)用,后者在類方法未找到時(shí)調(diào)用。我們可以通過重寫這兩個(gè)方法,添加其他函數(shù)實(shí)現(xiàn),并返回YES, 那運(yùn)行時(shí)系統(tǒng)就會(huì)重新啟動(dòng)一次消息發(fā)送的過程。

主要用的的方法如下:

// 類方法未找到時(shí)調(diào)起,可以在此添加方法實(shí)現(xiàn)
+ (BOOL)resolveClassMethod:(SEL)sel;
// 對(duì)象方法未找到時(shí)調(diào)起,可以在此添加方法實(shí)現(xiàn)
+ (BOOL)resolveInstanceMethod:(SEL)sel;

/** 
 * class_addMethod    向具有給定名稱和實(shí)現(xiàn)的類中添加新方法
 * @param cls         被添加方法的類
 * @param name        selector 方法名
 * @param imp         實(shí)現(xiàn)方法的函數(shù)指針
 * @param types imp   指向函數(shù)的返回值與參數(shù)類型
 * @return            如果添加方法成功返回 YES,否則返回 NO
 */
BOOL class_addMethod(Class cls, SEL name, IMP imp, 
                const char * _Nullable types);

舉個(gè)例子:

#import "ViewController.h"
#include "objc/runtime.h"

@interface ViewController ()

@end

@implementation ViewController

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

// 重寫 resolveInstanceMethod: 添加對(duì)象方法實(shí)現(xiàn)
+ (BOOL)resolveInstanceMethod:(SEL)sel {
    if (sel == @selector(fun)) { // 如果是執(zhí)行 fun 函數(shù),就動(dòng)態(tài)解析,指定新的 IMP
        class_addMethod([self class], sel, (IMP)funMethod, "v@:");
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}

void funMethod(id obj, SEL _cmd) {
    NSLog(@"funMethod"); //新的 fun 函數(shù)
}

@end
輸出結(jié)果:
funMethod

從上邊的例子中,我們可以看出,雖然我們沒有實(shí)現(xiàn)fun方法,但是通過重寫resolveInstanceMethod:,利用class_addMethod方法添加對(duì)象方法實(shí)現(xiàn)funMethod方法并執(zhí)行。從打印結(jié)果來看,成功調(diào)起了funMethod 方法。

我們注意到 class_addMethod 方法中的特殊參數(shù) v@:,具體可參考官方文檔中關(guān)于Type Encodings的說明:傳送門

2.備用接收者(消息接受者重定向)

如果上一步中+resolveInstanceMethod:或者+resolveClassMethod:沒有添加其他函數(shù)實(shí)現(xiàn),運(yùn)行時(shí)就會(huì)進(jìn)行下一步——消息接受者重定向。

如果當(dāng)前對(duì)象實(shí)現(xiàn)了-forwardingTargetForSelector: 或者 +forwardingTargetForSelector:方法,Runtime就會(huì)調(diào)用這個(gè)方法,允許我們將消息的接受者轉(zhuǎn)發(fā)給其他對(duì)象。

其中用到的方法:

// 重定向類方法的消息接收者,返回一個(gè)類或?qū)嵗龑?duì)象
+ (id)forwardingTargetForSelector:(SEL)aSelector;
// 重定向方法的消息接收者,返回一個(gè)類或?qū)嵗龑?duì)象
- (id)forwardingTargetForSelector:(SEL)aSelector;

注意:
1.類方法和對(duì)象方法消息轉(zhuǎn)發(fā)第二步調(diào)用的方法不一樣,前者是+forwardingTargetForSelector:方法,后者是-forwardingTargetForSelector:方法。
2.這里+resolveInstanceMethod:或者+resolveClassMethod:無論是返回YES還是NO,只要其中沒有添加其他函數(shù)實(shí)現(xiàn),運(yùn)行時(shí)都會(huì)進(jìn)行下一步。

舉個(gè)例子:

#import "ViewController.h"
#include "objc/runtime.h"

@interface Person : NSObject

- (void)fun;

@end

@implementation Person

- (void)fun {
    NSLog(@"fun");
}

@end



@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    // 執(zhí)行 fun 方法
    [self performSelector:@selector(fun)];
}

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    return YES; // 為了進(jìn)行下一步 消息接受者重定向
}

// 消息接受者重定向
- (id)forwardingTargetForSelector:(SEL)aSelector {
    if (aSelector == @selector(fun)) {
        return [[Person alloc] init];
        // 返回 Person 對(duì)象,讓 Person 對(duì)象接收這個(gè)消息
    }
    
    return [super forwardingTargetForSelector:aSelector];
}
輸出結(jié)果:
fun

可以看到,雖然當(dāng)前ViewController沒有實(shí)現(xiàn)fun方法,+resolveInstanceMethod:也沒有添加其他函數(shù)實(shí)現(xiàn)。但是我們通過forwardingTargetForSelector把當(dāng)前ViewController的方法轉(zhuǎn)發(fā)給了Person對(duì)象去執(zhí)行了。打印結(jié)果也證明我們成功實(shí)現(xiàn)了轉(zhuǎn)發(fā)。

我們通過forwardingTargetForSelector可以修改消息的接收者,該方法返回參數(shù)是一個(gè)對(duì)象,如果這個(gè)對(duì)象是不是nil,也不是self,系統(tǒng)會(huì)將運(yùn)行的消息轉(zhuǎn)發(fā)給這個(gè)對(duì)象執(zhí)行。否則,繼續(xù)進(jìn)行下一步——消息重定向流程。

3.完整消息轉(zhuǎn)發(fā)(消息重定向)

如果經(jīng)過消息動(dòng)態(tài)解析、消息接受者重定向,Runtime系統(tǒng)還是找不到相應(yīng)的方法實(shí)現(xiàn)而無法響應(yīng)消息,Runtime系統(tǒng)會(huì)利用-methodSignatureForSelector:或者 +methodSignatureForSelector:方法獲取函數(shù)的參數(shù)和返回值類型。

如果methodSignatureForSelector:返回了一個(gè) NSMethodSignature 對(duì)象(函數(shù)簽名),Runtime系統(tǒng)就會(huì)創(chuàng)建一個(gè)NSInvocation對(duì)象,并通過forwardInvocation:消息通知當(dāng)前對(duì)象,給予此次消息發(fā)送最后一次尋找IMP的機(jī)會(huì)。如果methodSignatureForSelector:返回nil。則Runtime系統(tǒng)會(huì)發(fā)出doesNotRecognizeSelector:消息,程序也就崩潰了。所以我們可以在forwardInvocation:方法中對(duì)消息進(jìn)行轉(zhuǎn)發(fā)。

注意:類方法和對(duì)象方法消息轉(zhuǎn)發(fā)第三步調(diào)用的方法同樣不一樣。

  • 類方法調(diào)用的是:
    1.methodSignatureForSelector:
    2.forwardInvocation:
    3.doesNotRecognizeSelector:
  • 對(duì)象方法調(diào)用的是:
    1.methodSignatureForSelector:
    2.forwardInvocation:
    3.doesNotRecognizeSelector:

用到的方法:

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

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

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

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

舉個(gè)例子:

#import "ViewController.h"
#include "objc/runtime.h"

@interface Person : NSObject

- (void)fun;

@end

@implementation Person

- (void)fun {
    NSLog(@"fun");
}

@end


@implementation ViewController

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

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    return YES; // 為了進(jìn)行下一步 消息接受者重定向
}

- (id)forwardingTargetForSelector:(SEL)aSelector {
    return nil; // 為了進(jìn)行下一步 消息重定向
}

// 獲取函數(shù)的參數(shù)和返回值類型,返回簽名
- (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 對(duì)象方法是否可以響應(yīng) sel
        [anInvocation invokeWithTarget:p];  // 若可以響應(yīng),則將消息轉(zhuǎn)發(fā)給其他對(duì)象處理
    } else {
        [self doesNotRecognizeSelector:sel];  // 若仍然無法響應(yīng),則報(bào)錯(cuò):找不到響應(yīng)方法
    }
}
@end
輸出結(jié)果:
fun

可以看到,我們?cè)?code>-forwardInvocation:方法里面讓Person 對(duì)象去執(zhí)行了fun函數(shù)。

既然-forwardingTargetForSelector:-forwardInvocation:都可以將消息轉(zhuǎn)發(fā)給其他對(duì)象處理,那么兩者的區(qū)別在哪?區(qū)別就在于-forwardingTargetForSelector:只能將消息轉(zhuǎn)發(fā)給一個(gè)對(duì)象。而-forwardInvocation:可以將消息轉(zhuǎn)發(fā)給多個(gè)對(duì)象。

以上就是 Runtime 消息轉(zhuǎn)發(fā)的整個(gè)流程。

參考鏈接:
http://www.itdecent.cn/p/6ebda3cd8052
http://www.itdecent.cn/p/633e5d8386a8
https://www.imooc.com/article/38310
官方文檔:
runtime源碼地址(開源)
蘋果官方Runtime編程指南
objc_msgSend
objc_msgSend_fpret
objc_msgsend_stret
objc_msgsendsuper
objc_msgsendsuper_stret
大神入口:
http://yulingtianxia.com/blog/2016/06/15/Objective-C-Message-Sending-and-Forwarding/

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

相關(guān)閱讀更多精彩內(nèi)容

  • 我們常常會(huì)聽說 Objective-C 是一門動(dòng)態(tài)語言,那么這個(gè)「動(dòng)態(tài)」表現(xiàn)在哪呢?我想最主要的表現(xiàn)就是 Obje...
    Ethan_Struggle閱讀 2,336評(píng)論 0 7
  • 本文轉(zhuǎn)載自:http://yulingtianxia.com/blog/2014/11/05/objective-...
    ant_flex閱讀 887評(píng)論 0 1
  • 本文詳細(xì)整理了 Cocoa 的 Runtime 系統(tǒng)的知識(shí),它使得 Objective-C 如虎添翼,具備了靈活的...
    lylaut閱讀 866評(píng)論 0 4
  • 繼上Runtime梳理(四) 通過前面的學(xué)習(xí),我們了解到Objective-C的動(dòng)態(tài)特性:Objective-C不...
    小名一峰閱讀 848評(píng)論 0 3
  • 簡(jiǎn)介 Runtime 又叫運(yùn)行時(shí),是一套底層的 C 語言 API,其為 iOS 內(nèi)部的核心之一,我們平時(shí)編寫的 O...
    專業(yè)男神經(jīng)閱讀 961評(píng)論 0 2

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