Runtime 運行時之二:方法調用流程與消息轉發

方法調用流程

在Objective-C中,消息直到運行時才綁定到方法實現上。編譯器會將消息表達式[receiver message]轉化為一個消息函數的調用,即objc_msgSend。這個函數將消息接收者和方法名作為其基礎參數,如以下所示:


objc_msgSend(receiver, selector)

如果消息中還有其它參數,則該方法的形式如下所示:


objc_msgSend(receiver, selector, arg1, arg2, ...)

這個函數完成了動態綁定的所有事情:

  1. 首先它找到selector對應的方法實現。因為同一個方法可能在不同的類中有不同的實現,所以我們需要依賴于接收者的類來找到的確切的實現。
  2. 它調用方法實現,并將接收者對象及方法的所有參數傳給它。
  3. 最后,它將實現返回的值作為它自己的返回值。

消息的關鍵在于我們前面章節討論過的結構體objc_class,這個結構體有兩個字段是我們在分發消息的關注的:

  1. 指向父類的指針
  2. 一個類的方法分發表,即methodLists。

當我們創建一個新對象時,先為其分配內存,并初始化其成員變量。其中isa指針也會被初始化,讓對象可以訪問類及類的繼承體系。

下圖演示了這樣一個消息的基本框架:

messaging1-1.gif

當消息發送給一個對象時,objc_msgSend通過對象的isa指針獲取到類的結構體,然后在方法分發表里面查找方法的selector。如果沒有找到selector,則通過objc_msgSend結構體中的指向父類的指針找到其父類,并在父類的分發表里面查找方法的selector。依此,會一直沿著類的繼承體系到達NSObject類。一旦定位到selector,函數會就獲取到了實現的入口點,并傳入相應的參數來執行方法的具體實現。如果最后沒有定位到selector,則會走消息轉發流程,這個我們在后面討論。

為了加速消息的處理,運行時系統緩存使用過的selector及對應的方法的地址。這點我們在前面討論過,不再重復。

獲取方法地址

Runtime中方法的動態綁定讓我們寫代碼時更具靈活性,如我們可以把消息轉發給我們想要的對象,或者隨意交換一個方法的實現等。不過靈活性的提升也帶來了性能上的一些損耗。畢竟我們需要去查找方法的實現,而不像函數調用來得那么直接。當然,方法的緩存一定程度上解決了這一問題。

我們上面提到過,如果想要避開這種動態綁定方式,我們可以獲取方法實現的地址,然后像調用函數一樣來直接調用它。特別是當我們需要在一個循環內頻繁地調用一個特定的方法時,通過這種方式可以提高程序的性能。

NSObject類提供了methodForSelector:方法,讓我們可以獲取到方法的指針,然后通過這個指針來調用實現代碼。我們需要將methodForSelector:返回的指針轉換為合適的函數類型,函數參數和返回值都需要匹配上。

我們通過以下代碼來看看methodForSelector:的使用:

@implementation ViewController
- (void)viewDidLoad{
  ///通過runtime獲取IMP
   Method nameLogMethod = class_getInstanceMethod([self class], sel1);
    IMP nameLogImp = method_getImplementation(nameLogMethod);
    nameLogImp();
    
    ///通過NSObject獲取IMP
    ///methodForSelector 如果接受者是一個類對象,則返回類對象的方法;如果接受者是一個實例對象,則返回實例對象的方法;
    IMP nameLogImp2 = [self methodForSelector:sel1];
    nameLogImp2();
    ///instanceMethodForSelector 是通過遍歷自身中的函數列表換句話說,如果調用的一個元類,那么返回的是一個類實例的函數,如果調用的是一個類,那么返回的就是實例對象的函數。
    IMP nameLogImp3 = [ViewController instanceMethodForSelector:sel1];
    nameLogImp3();
   ///http://www.lxweimin.com/p/2007e03b6296
}
-(void)nameLog{
    
    NSLog(@"我的名字3--");
   
}

這里需要注意的就是函數指針的前兩個參數必須是idSEL

當然這種方式只適合于在類似于for循環這種情況下頻繁調用同一方法,以提高性能的情況。另外,methodForSelector:是由Cocoa運行時提供的;它不是Objective-C語言的特性。

消息轉發

當一個對象能接收一個消息時,就會走正常的方法調用流程。但如果一個對象無法接收指定消息時,又會發生什么事呢?默認情況下,如果是以[object message]的方式調用方法,如果object無法響應message消息時,編譯器會報錯。但如果是以performSelector的形式來調用,則需要等到運行時才能確定object是否能接收message消息。如果不能,則程序崩潰。

通常,當我們不能確定一個對象是否能接收某個消息時,會先調用respondsToSelector:來判斷一下。如下代碼所示:


if ([self respondsToSelector:@selector(runtimeAction)]) {

    [self performSelector:@selector(runtimeAction)];

}

不過,我們這邊想討論下不使用respondsToSelector:判斷的情況。這才是我們這一節的重點。

當一個對象無法接收某一消息時,就會啟動所謂”消息轉發(message forwarding)“機制,通過這一機制,我們可以告訴對象如何處理未知的消息。默認情況下,對象接收到未知的消息,會導致程序崩潰,通過控制臺,我們可以看到以下異常信息:


libc++abi.dylib: terminating with uncaught exception of type NSException
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[runtimeViewController runtimeAction]: unrecognized selector sent to instance 0x7fd89460dc30'
terminating with uncaught exception of type NSException

這段異常信息實際上是由NSObject的”doesNotRecognizeSelector“方法拋出的。不過,我們可以采取一些措施,讓我們的程序執行特定的邏輯,而避免程序的崩潰。

消息轉發機制基本上分為三個步驟:

  1. 動態方法解析
  2. 備用接收者\替換消息接收者(快速轉發)
  3. 完整轉發
2145446-c73a54d9b48e0417.png

下面我們詳細討論一下這三個步驟。

第一步、動態方法解析

對象在接收到未知的消息時,首先會調用所屬類的類方法

///實例方法
+ (BOOL)resolveInstanceMethod:(SEL)sel
///類方法
+ (BOOL)resolveClassMethod:(SEL)sel

在這方法中,我們有機會為該未知消息新增一個”處理方法””。不過使用該方法的前提是我們已經實現了該”處理方法”,只需要在運行時通過class_addMethod函數動態添加到類里面就可以了。如下代碼所示:

//第一步、動態方法解析
+ (BOOL)resolveInstanceMethod:(SEL)sel {
 //  SEL獲取IMP
//    [self instanceMethodForSelector:@selector(runtimeFuncAction)]
//  SEL轉字符串
//    NSStringFromSelector(sel)


   if (sel == @selector(runtimeAction)) {
        ///第一種方式
        ///class_addMethod(self, sel, [self instanceMethodForSelector:@selector(runtimeFuncAction)], "v@:");

        ///第二種方式
        ///v@:代表形參
        class_addMethod(self, sel, (IMP)runtimeFuncActionC, "v@:");

        return YES;
    }

    return [super resolveInstanceMethod:sel];
}

///第一種方式
- (void)runtimeFuncAction {
    
    NSLog(@"runtimeActionq未實現-第一步、動態方法解析-runtimeFuncAction-執行");
    
}
///第二種方式
void runtimeFuncActionC(id self, SEL _cmd)

{
    
    NSLog(@"runtimeActionq未實現-第一步、動態方法解析-runtimeFuncActionC-執行-%@-%@",self,NSStringFromSelector(_cmd));
    
}

不過這種方案更多的是為了實現@dynamic屬性,dynamic修飾的屬性,無set,get方法。

擴展-@dynamic
@synthesize 和 @dynamic分別有什么作用

  • @property有兩個對應的詞,一個是@synthesize,一個是@dynamic。如果@synthesize和@dynamic都沒寫,那么默認的就是@syntheszie var = _var;
  • @synthesize的語義是如果你沒有手動實現setter方法和getter方法,那么編譯器會自動為你加上這兩個方法
  • @dynamic告訴編譯器:屬性的setter與getter方法由用戶自己實現,不自動生成(當然對于readonly的屬性只需提供getter即可)
  • 假如一個屬性被聲明為@dynamic var,然后你沒有提供@setter方法和@getter方法,編譯的時候沒問題,但是當程序運行到instance.var = someVar,由于缺setter方法會導致程序崩潰;或者當運行到 someVar = instance.var時,由于缺getter方法同樣會導致崩潰。編譯時沒問題,運行時才執行相應的方法,這就是所謂的動態綁定

第二步、備用接收者\替換消息接收者(快速轉發)

如果在上一步無法處理消息,則Runtime會繼續調以下方法:

///實例方法
- (id)forwardingTargetForSelector:(SEL)aSelector
///類方法
+ (id)forwardingTargetForSelector:(SEL)sel

如果一個對象實現了這個方法,并返回一個非nil的結果,則這個對象會作為消息的新接收者,且消息會被分發到這個對象。當然這個對象不能是self自身,否則就是出現無限循環。當然,如果我們沒有指定相應的對象來處理aSelector,則應該調用父類的實現來返回結果。

使用這個方法通常是在對象內部,可能還有一系列其它對象能處理該消息,我們便可借這些對象來處理消息并返回,這樣在對象外部看來,還是由該對象親自處理了這一消息。如下代碼所示:

runtimeViewController.m文件
#import "runtimeViewController.h"
///運行時頭文件
#import <objc/runtime.h>

#import "notificationViewController.h"
@interface runtimeViewController ()

@end


@implementation runtimeViewController

- (void)viewDidLoad {
 [self performSelector:@selector(runtimeAction)];
}

- (id)forwardingTargetForSelector:(SEL)sel{
    
    if(sel == @selector(runtimeAction)) {
///將消息轉發給notificationViewController來處理
//        return [[notificationViewController alloc]init];
        return NSClassFromString(@"notificationViewController");
    }
    return [super forwardingTargetForSelector:sel];
    
}

notificationViewController.m文件
- (void)runtimeAction {
    
    NSLog(@"runtimeActionq未實現-第二步、備用接收者或第三步、完整消息轉發-(對象)runtimeAction-執行");
    
}

這一步合適于我們只想將消息轉發到另一個能處理該消息的對象上。但這一步無法對消息進行處理,如操作消息的參數和返回值

完整消息轉發

如果在上一步還不能處理未知消息,則唯一能做的就是啟用完整的消息轉發機制了。此時會調用以下方法:


- (void)forwardInvocation:(NSInvocation *)anInvocation

運行時系統會在這一步給消息接收者最后一次機會將消息轉發給其它對象。對象會創建一個表示消息的NSInvocation對象,把與尚未處理的消息有關的全部細節都封裝在anInvocation中,包括selector,目標(target)和參數。我們可以在forwardInvocation方法中選擇將消息轉發給其它對象。

forwardInvocation:方法的實現有兩個任務:

  1. 定位可以響應封裝在anInvocation中的消息的對象。這個對象不需要能處理所有未知消息。
  2. 使用anInvocation作為參數,將消息發送到選中的對象。anInvocation將會保留調用結果,運行時系統會提取這一結果并將其發送到消息的原始發送者。

不過,在這個方法中我們可以實現一些更復雜的功能,我們可以對消息的內容進行修改,比如追回一個參數等,然后再去觸發消息。另外,若發現某個消息不應由本類處理,則應調用父類的同名方法,以便繼承體系中的每個類都有機會處理此調用請求。

還有一個很重要的問題,我們必須重寫以下方法:


- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector

消息轉發機制使用從這個方法中獲取的信息來創建NSInvocation對象。因此我們必須重寫這個方法,為給定的selector提供一個合適的方法簽名。

完整的示例如下所示:

runtimeViewController.m文件
#import "runtimeViewController.h"
///運行時頭文件
#import <objc/runtime.h>

#import "notificationViewController.h"
@interface runtimeViewController ()

@end


@implementation runtimeViewController

- (void)viewDidLoad {
 [self performSelector:@selector(runtimeAction)];
}
///第三步、完整消息轉發
-(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
    if (aSelector == @selector(runtimeAction))
    {
        return [NSMethodSignature signatureWithObjCTypes:"v@:"];
    }
    return nil;
}

- (void)forwardInvocation:(NSInvocation *)invocation{

    SEL aSelector = [invocation selector];
    ///判斷notificationViewController是否能響應aSelector方法
    if ([notificationViewController respondsToSelector:aSelector]){
        ///將 invocation 消息轉發給其它對象 notificationViewController 執行
        [invocation invokeWithTarget:[[notificationViewController alloc]init]];
    }else{
        [super forwardInvocation:invocation];
    }

}
notificationViewController.m文件
- (void)runtimeAction {
    
    NSLog(@"runtimeActionq未實現-第二步、備用接收者或第三步、完整消息轉發-(對象)runtimeAction-執行");
    
}

NSObject的forwardInvocation:方法實現只是簡單調用了doesNotRecognizeSelector:方法,它不會轉發任何消息。這樣,如果不在以上所述的三個步驟中處理未知消息,則會引發一個異常。

從某種意義上來講,forwardInvocation:就像一個未知消息的分發中心,將這些未知的消息轉發給其它對象?;蛘咭部梢韵褚粋€運輸站一樣將所有未知消息都發送給同一個接收對象。這取決于具體的實現。

消息轉發與多重繼承

回過頭來看第二和第三步,通過這兩個方法我們可以允許一個對象與其它對象建立關系,以處理某些未知消息,而表面上看仍然是該對象在處理消息。通過這種關系,我們可以模擬“多重繼承”的某些特性,讓對象可以“繼承”其它對象的特性來處理一些事情。不過,這兩者間有一個重要的區別:多重繼承將不同的功能集成到一個對象中,它會讓對象變得過大,涉及的東西過多;而消息轉發將功能分解到獨立的小的對象中,并通過某種方式將這些對象連接起來,并做相應的消息轉發。

不過消息轉發雖然類似于繼承,但NSObject的一些方法還是能區分兩者。如respondsToSelector:isKindOfClass:只能用于繼承體系,而不能用于轉發鏈。便如果我們想讓這種消息轉發看起來像是繼承,則可以重寫這些方法,如以下代碼所示:


- (BOOL)respondsToSelector:(SEL)aSelector

{

    if ( [super respondsToSelector:aSelector])

        return YES;

    else {

        /* Here, test whether the aSelector message can     *

         * be forwarded to another object and whether that  *

         * object can respond to it. Return YES if it can.  */

    }

    return NO;  

}

小結

在此,我們已經了解了Runtime中消息發送和轉發的基本機制。這也是Runtime的強大之處,通過它,我們可以為程序增加很多動態的行為,雖然我們在實際開發中很少直接使用這些機制(如直接調用objc_msgSend),但了解它們有助于我們更多地去了解底層的實現。其實在實際的編碼過程中,我們也可以靈活地使用這些機制,去實現一些特殊的功能,如hook操作等。

為什么Objective-C的消息轉發要設計三個階段?

第一階段意義在于動態添加方法實現,第二階段直接把消息轉發給其他對象,第三階段是對第二階段的擴充,可以實現多次轉發,轉發給多個對象等。這也許就是設計這三個階段的意義。

補充:下面這張圖(來自:一縷殤流化隱半邊冰霜(侵刪))我覺得更符合消息轉發的流程,更容易理解。


20200407161830450.png

參考:
http://www.lxweimin.com/p/8d4f2f1d8482
https://www.jb51.net/article/157079.htm
https://blog.csdn.net/fishmai/article/details/73468952
http://www.lxweimin.com/p/19c5736c5d9a
http://southpeak.github.io/categories/objectivec/

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,345評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,494評論 3 416
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 176,283評論 0 374
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,953評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,714評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,186評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,255評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,410評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,940評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,776評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,976評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,518評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,210評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,642評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,878評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,654評論 3 391
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,958評論 2 373

推薦閱讀更多精彩內容