Objective-C Runtime 運行時之三:方法與消息

前面咱們討論了Runtime中對類和對象的處理,及對成員變量與屬性的處理。這一章,咱們就要開始討論Runtime中最有意思的一部分:消息處理機制。咱們將詳細討論消息的發送及消息的轉發。不過在討論消息以前,咱們先來了解一下與方法相關的一些內容。html

基礎數據類型ios

SEL緩存

SEL又叫選擇器,是表示一個方法的selector的指針,其定義以下:數據結構

1
typedef struct objc_selector *SEL;

objc_selector結構體的詳細定義沒有在頭文件中找到。方法的selector用於表示運行時方 法的名字。Objective-C在編譯時,會依據每個方法的名字、參數序列,生成一個惟一的整型標識(Int類型的地址),這個標識就是SEL。以下 代碼所示:架構

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

上面的輸出爲:app

1
2014-10-30 18:40:07.518 RuntimeTest[52734:466626] sel : 0x100002d72

兩個類之間,無論它們是父類與子類的關係,仍是之間沒有這種關係,只要方法名相同,那麼方法的SEL就是同樣的。每個方法都對應着一個SEL。因此在 Objective-C同一個類(及類的繼承體系)中,不能存在2個同名的方法,即便參數類型不一樣也不行。相同的方法只能對應一個SEL。這也就致使 Objective-C在處理相同方法名且參數個數相同但類型不一樣的方法方面的能力不好。如在某個類中定義如下兩個方法:框架

1
2
- (void)setWidth:(int)width;
- (void)setWidth:(double)width;

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

工程中的全部的SEL組成一個Set集合,Set的特色就是惟一,所以SEL是惟一的。所以,若是咱們想到這個方法集合中查找某個方法時,只須要去 找到這個方法對應的SEL就好了,SEL實際上就是根據方法名hash化了的一個字符串,而對於字符串的比較僅僅須要比較他們的地址就能夠了,能夠說速度 上無語倫比!!可是,有一個問題,就是數量增多會增大hash衝突而致使的性能降低(或是沒有衝突,由於也可能用的是perfect hash)。可是無論使用什麼樣的方法加速,若是可以將總量減小(多個方法可能對應同一個SEL),那將是最犀利的方法。那麼,咱們就不難理解,爲何 SEL僅僅是函數名了。性能

本質上,SEL只是一個指向方法的指針(準確的說,只是一個根據方法名hash化了的KEY值,能惟一表明一個方法),它的存在只是爲了加快方法的查詢速度。這個查找過程咱們將在下面討論。編碼

咱們能夠在運行時添加新的selector,也能夠在運行時獲取已存在的selector,咱們能夠經過下面三種方法來獲取SEL:

1. sel_registerName函數

2. Objective-C編譯器提供的@selector()

3. NSSelectorFromString()方法

IMP

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

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

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

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

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

Method

介紹完SEL和IMP,咱們就能夠來說講Method了。Method用於表示類定義中的方法,則定義以下:

1
2
3
4
5
6
7
typedef struct objc_method *Method;
 
struct objc_method {
     SEL method_name                 OBJC2_UNAVAILABLE;   // 方法名
     char *method_types                  OBJC2_UNAVAILABLE;
     IMP method_imp                      OBJC2_UNAVAILABLE;   // 方法實現
}

咱們能夠看到該結構體中包含一個SEL和IMP,實際上至關於在SEL和IMP之間做了一個映射。有了SEL,咱們即可以找到對應的IMP,從而調用方法的實現代碼。具體操做流程咱們將在下面討論。

objc_method_description

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

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

方法相關操做函數

Runtime提供了一系列的方法來處理與方法相關的操做。包括方法自己及SEL。本節咱們介紹一下這些函數。

方法

方法操做相關函數包括下以:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// 調用指定方法的實現
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_getImplementation和method_getName更快。

● method_getName函數,返回的是一個SEL。若是想獲取方法名的C字符串,可使用sel_getName(method_getName(method))。

● method_getReturnType函數,類型字符串會被拷貝到dst中。

● method_setImplementation函數,注意該函數返回值是方法以前的實現。

方法選擇器

選擇器相關的操做函數包括:

1
2
3
4
5
6
7
8
9
10
11
// 返回給定選擇器指定的方法的名稱
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系統中註冊一個方法名以獲取方法的選擇器。

方法調用流程

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

1
objc_msgSend(receiver, selector)

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

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

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

1. 首先它找到selector對應的方法實現。由於同一個方法可能在不一樣的類中有不一樣的實現,因此咱們須要依賴於接收者的類來找到的確切的實現。

2. 它調用方法實現,並將接收者對象及方法的全部參數傳給它。

3. 最後,它將實現返回的值做爲它本身的返回值。

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

1. 指向父類的指針

2. 一個類的方法分發表,即methodLists。

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

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

messaging1.gif

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

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

隱藏參數

objc_msgSend有兩個隱藏參數:

1. 消息接收對象

2. 方法的selector

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

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

1
2
3
4
5
6
7
8
9
- strange
{
     id  target = getTheReceiver();
     SEL method = getTheMethod();
 
     if  ( target == self || method == _cmd )
         return  nil;
     return  [target performSelector:method];
}

固然,這兩個參數咱們用得比較多的是self,_cmd在實際中用得比較少。

獲取方法地址

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

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

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

咱們經過如下代碼來看看methodForSelector:的使用:

1
2
3
4
5
6
7
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);

這裏須要注意的就是函數指針的前兩個參數必須是id和SEL。

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

消息轉發

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

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

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

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

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

1
2
-[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函數動態添加到類裏面就能夠了。以下代碼所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void functionForMethod1(id self, SEL _cmd) {
    NSLog(@ "%@, %p" , self, _cmd);
}
 
+ (BOOL)resolveInstanceMethod:(SEL)sel {
 
     NSString *selectorString = NSStringFromSelector(sel);
 
     if  ([selectorString isEqualToString:@ "method1" ]) {
         class_addMethod(self.class, @selector(method1), (IMP)functionForMethod1,  "@:" );
     }
 
     return  [ super  resolveInstanceMethod:sel];
}

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

備用接收者

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

1
- (id)forwardingTargetForSelector:(SEL)aSelector

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
@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

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

完整消息轉發

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

1
- (void)forwardInvocation:(NSInvocation *)anInvocation

運行時系統會在這一步給消息接收者最後一次機會將消息轉發給其它對象。對象會建立一個表示消息的NSInvocation對象,把與還沒有處理的消息 有關的所有細節都封裝在anInvocation中,包括selector,目標(target)和參數。咱們能夠在forwardInvocation 方法中選擇將消息轉發給其它對象。

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

1. 定位能夠響應封裝在anInvocation中的消息的對象。這個對象不須要能處理全部未知消息。

2. 使用anInvocation做爲參數,將消息發送到選中的對象。anInvocation將會保留調用結果,運行時系統會提取這一結果並將其發送到消息的原始發送者。

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

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

1
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector

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

完整的示例以下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
     NSMethodSignature *signature = [ super  methodSignatureForSelector:aSelector];
 
     if  (!signature) {
         if  ([SUTRuntimeMethodHelper instancesRespondToSelector:aSelector]) {
             signature = [SUTRuntimeMethodHelper instanceMethodSignatureForSelector:aSelector];
         }
     }
 
     return  signature;
}
 
- (void)forwardInvocation:(NSInvocation *)anInvocation {
     if  ([SUTRuntimeMethodHelper instancesRespondToSelector:anInvocation.selector]) {
         [anInvocation invokeWithTarget:_helper];
     }
}

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

從某種意義上來說,forwardInvocation:就像一個未知消息的分發中心,將這些未知的消息轉發給其它對象。或者也能夠像一個運輸站同樣將全部未知消息都發送給同一個接收對象。這取決於具體的實現。

消息轉發與多重繼承

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

不過消息轉發雖然相似於繼承,但NSObject的一些方法仍是能區分二者。如respondsToSelector:和isKindOfClass:只能用於繼承體系,而不能用於轉發鏈。便若是咱們想讓這種消息轉發看起來像是繼承,則能夠重寫這些方法,如如下代碼所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
- (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操做等。

相關文章
相關標籤/搜索