Runtime 運行時:方法與消息

SEL

SEL又叫方法選擇器,表示一個方法的selector的指針,其定義以下:ios

typedef struct objc_selector *SEL;

方法的selector表示運行時方法的名字。Objective-C編譯時,會依據每個方法的名字參數序列,生成一個惟一的整型標識 ( Int類型的地址 ),這個標識就是SEL。以下代碼所示:git

SEL sel1 = @selector(method1);
NSLog(@"sel : %p", sel1);

輸出結果:github

 

兩個類之間,無論它們是父類與子類的關係,仍是之間沒有這種關係,只要方法名相同,那麼方法的SEL就是同樣的。每個方法都對應着一個SEL因此在OC同一個類中,不能存在2個同名的方法,即便參數類型不一樣也不行。相同的方法只能對應一個SELobjective-c

不一樣的類能夠擁有相同的selector,這個沒有問題。不一樣類的實例對象執行相同的selector時,會在各自的方法列表中去根據selector去尋找本身對應的IMP緩存

工程中的全部的SEL組成一個Set集合,Set的特色就是惟一,所以SEL是惟一的。數據結構

本質上,SEL只是一個指向方法的指針(準確的說,只是一個根據方法名hash化了的KEY值,能惟一表明一個方法).架構

 

咱們能夠在運行時添加新的selector,也能夠在運行時獲取已存在的selectorapp

咱們能夠經過下面三種方法來獲取SEL:框架

  1. sel_registerName函數
  2. Objective-C編譯器提供的@selector()
  3. NSSelectorFromString()方法

 

 

IMP

IMP其實是一個指向函數的指針,指向方法實現的首地址。其定義以下:ide

id (*IMP)(id, SEL, ...)

這個函數使用當前CPU架構實現的標準的C調用約定。第一個參數是指向self的指針(若是是對象方法,則是類對象的內存地址;若是是類方法,則是指向元類的指針),第二個參數是方法選擇器(selector),接下來是方法的實際參數列表。

SEL就是爲了查找方法的最終實現IMP。因爲每一個方法對應惟一的SEL,所以咱們能夠經過SEL方便快速準確地得到它所對應的IMP。取得IMP後,咱們就得到了執行這個方法代碼的入口點,此時,咱們就能夠像調用普通的C語言函數同樣來使用這個函數指針了。

經過取得IMP咱們能夠跳過Runtime的消息傳遞機制,直接執行IMP指向的函數實現這樣省去了Runtime消息傳遞過程當中所作的一系列查找操做,會比直接向對象發送消息高效一些。

 

 

Method

Method用於表示類定義中的方法,則定義以下:

typedef struct objc_method *Method;
struct objc_method {
    SEL method_name                	OBJC2_UNAVAILABLE;	// 方法名
    char *method_types                	OBJC2_UNAVAILABLE;
    IMP method_imp             			OBJC2_UNAVAILABLE;	// 方法實現
}

該結構體中包含一個SELIMP,實際上至關於在SELIMP之間做了一個映射。有了SEL,咱們即可以找到對應的IMP,從而調用方法的實現代碼。

 

 

objc_method_description

objc_method_description定義了一個Objective-C方法,其定義以下:

struct objc_method_description { SEL name; char *types; };

 

 

方法相關操做函數

Runtime提供了一系列的方法  用來處理與方法相關的操做。包括方法自己及SEL。

方法自己

方法操做相關函數:

// 調用指定方法的實現
id method_invoke ( id receiver, Method m, ... );

// 調用返回一個數據結構的方法的實現
void method_invoke_stret ( id receiver, Method m, ... );

// 獲取方法名
SEL method_getName ( Method m );

// 返回方法的實現
IMP method_getImplementation ( Method m );

// 獲取描述方法參數和返回值類型的字符串
const char * method_getTypeEncoding ( Method m );

// 獲取方法的返回值類型的字符串
char * method_copyReturnType ( Method m );

// 獲取方法的指定位置參數的類型字符串
char * method_copyArgumentType ( Method m, unsigned int index );

// 經過引用返回方法的返回值類型字符串
void method_getReturnType ( Method m, char *dst, size_t dst_len );

// 返回方法的參數的個數
unsigned int method_getNumberOfArguments ( Method m );

// 經過引用返回方法指定位置參數的類型字符串
void method_getArgumentType ( Method m, unsigned int index, char *dst, size_t dst_len );

// 返回指定方法的方法描述結構體
struct objc_method_description * method_getDescription ( Method m );

// 設置方法的實現
IMP method_setImplementation ( Method m, IMP imp );

// 交換兩個方法的實現
void method_exchangeImplementations ( Method m1, Method m2 );
  • method_invoke函數,返回的是實際實現的返回值。參數receiver不能爲空。這個方法的效率會比method_getImplementationmethod_getName更快。
  • method_getName函數,返回的是一個SEL。若是想獲取方法名的C字符串,可使用sel_getName(method_getName(method))
  • method_getReturnType函數,類型字符串會被拷貝到dst中。
  • method_setImplementation函數,注意該函數返回值是方法以前的實現。

 

 

方法選擇器

選擇器相關的操做函數

// 返回給定選擇器指定的方法的名稱
const char * sel_getName ( SEL sel );

// 在Objective-C Runtime系統中註冊一個方法,將方法名映射到一個選擇器,並返回這個選擇器
SEL sel_registerName ( const char *str );

// 在Objective-C Runtime系統中註冊一個方法
SEL sel_getUid ( const char *str );

// 比較兩個選擇器
BOOL sel_isEqual ( SEL lhs, SEL rhs );
  • sel_registerName函數:在咱們將一個方法添加到類定義時,咱們必須在Objective-C Runtime系統中註冊一個方法名以獲取方法的選擇器。

 

 

方法調用流程

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

objc_msgSend(receiver, selector)

若是消息中還有其餘參數

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

這個函數完成了動態綁定的全部事情:

  1. 首先它找到selector對應的方法實現。由於同一個方法可能在不一樣的類中有不一樣的實現,因此咱們須要依賴於接收者的類來找到確切的實現。
  2. 它調用 方法實現,並將接收者對象方法的全部參數傳給 方法實現。
  3. 最後,它將 實現返回的值 做爲它本身的返回值。

消息的關鍵在於 結構體objc_class,這個結構體有兩個字段是咱們在分發消息時要關注的:

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

當咱們建立一個新對象時,先爲其分配內存,並初始化其成員變量。其中isa指針也會被初始化,讓對象能夠訪問類及類的繼承體系。

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

image

 

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

爲了加速消息的處理,運行時系統緩存使用過的selector及對應的方法的地址。

 

 

隱藏參數

objc_msgSend有兩個隱藏參數:

  1. 消息接收對象
  2. 方法的selector

這兩個參數爲 方法的實現 提供了調用者的信息。之因此說是隱藏的,是由於它們在定義方法的源代碼中沒有聲明。它們是在編譯期被插入實現代碼的。

雖然這些參數沒有顯示聲明,但在代碼中仍然能夠引用它們。可使用 self來引用接收者對象,使用_cmd來引用選擇器

 

獲取方法地址

若是想要避開這種動態綁定方式,咱們能夠獲取方法實現的地址,而後像調用函數同樣來直接調用它。特別是當咱們須要在一個循環內頻繁地調用一個特定的方法時,經過這種方式能夠提升程序的性能。

NSObject類提供了methodForSelector:方法,能夠獲取到方法的指針(指向函數的指針),而後經過這個指針來調用實現代碼。咱們須要將methodForSelector:返回的指針轉換爲合適的函數類型函數參數返回值都須要匹配上。

void (*setter)(id, SEL, BOOL);
int i;
setter = (void (*)(id, SEL, BOOL))[target methodForSelector:@selector(setFilled:)];
for (i = 0 ; i < 1000 ; i++){
    setter(targetList[i], @selector(setFilled:), YES);
}

注意:函數指針的前兩個參數必須是idSEL

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

 

 

消息轉發

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

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

if ([self respondsToSelector:@selector(method)]) {
    [self performSelector:@selector(method)];
}

不過,我想討論下不使用respondsToSelector:判斷的狀況。

當一個對象沒法接收某一消息時,就會啓動所謂」消息轉發(message forwarding)「機制,經過這一機制,咱們能夠告訴對象如何處理未知的消息。默認狀況下,對象接收到未知的消息,會致使程序崩潰,經過控制檯,咱們能夠看到如下異常信息:

-[SUTRuntimeMethod method]: unrecognized selector sent to instance 0x100111940
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[SUTRuntimeMethod method]: unrecognized selector sent to instance 0x100111940'

這段異常信息其實是由NSObject的」doesNotRecognizeSelector「方法拋出的。不過,咱們能夠採起一些措施,讓咱們的程序執行特定的邏輯,而避免程序的崩潰。

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

  1. 動態方法解析
  2. 備用接收者
  3. 完整轉發

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

動態方法解析

對象在接收到未知的消息時,首先會調用所屬類的類方法+resolveInstanceMethod:或者+resolveClassMethod:。在這個方法中,咱們有機會爲該未知消息新增一個」處理方法」」。不過使用該方法的前提是咱們已經實現了該」處理方法」,只須要在運行時經過class_addMethod函數動態添加到類裏面就能夠了。以下代碼所示:

不過這種方案更多的是爲了實現@dynamic屬性。

 

備用接收者

若是在上一步沒法處理消息,則Runtime會繼續調如下方法:

- (id)forwardingTargetForSelector:(SEL)aSelector

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

使用這個方法一般是在對象內部,可能還有一系列其它對象能處理該消息,咱們即可借這些對象來處理消息並返回,這樣在對象外部看來,仍是由該對象親自處理了這一消息。以下代碼所示:

@interface SUTRuntimeMethodHelper : NSObject
- (void)method2;
@end
@implementation SUTRuntimeMethodHelper
- (void)method2 {
    NSLog(@"%@, %p", self, _cmd);
}
@end
#pragma mark -
@interface SUTRuntimeMethod () {
    SUTRuntimeMethodHelper *_helper;
}
@end
@implementation SUTRuntimeMethod
+ (instancetype)object {
    return [[self alloc] init];
}
- (instancetype)init {
    self = [super init];
    if (self != nil) {
        _helper = [[SUTRuntimeMethodHelper alloc] init];
    }
    return self;
}
- (void)test {
    [self performSelector:@selector(method2)];
}
- (id)forwardingTargetForSelector:(SEL)aSelector {
    NSLog(@"forwardingTargetForSelector");
    NSString *selectorString = NSStringFromSelector(aSelector);
    // 將消息轉發給_helper來處理
    if ([selectorString isEqualToString:@"method2"]) {
        return _helper;
    }
    return [super forwardingTargetForSelector:aSelector];
}
@end

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

 

完整消息轉發

http://southpeak.github.io/2014/11/03/objective-c-runtime-3/

相關文章
相關標籤/搜索