一、SEL、IMP、Method
1. SEL
SEL又叫選擇器,是表示一個方法的selector的指針,其定義如下:
typedef struct objc_selector *SEL;
方法的selector用于表示運(yùn)行時方法的名字。OC在編譯時,會依據(jù)每一個方法的名字、參數(shù)序列,生成一個唯一的整型標(biāo)識(Int類型的地址),這個標(biāo)識就是SEL。同一個類(及類的繼承體系)中,不能存在2個同名的方法,即使參數(shù)類型不同也不行。相同的方法只能對應(yīng)一個SEL。這也就導(dǎo)致 OC在處理相同方法名且參數(shù)個數(shù)相同但類型不同的方法方面的能力很差。當(dāng)然,不同的類可以擁有相同的selector,這個沒有問題。不同類的實(shí)例對象執(zhí)行相同的selector時,會在各自的方法列表中去根據(jù)selector去尋找自己對應(yīng)的IMP。
工程中的所有的SEL組成一個Set集合,Set的特點(diǎn)就是唯一,因此SEL是唯一的。因此,如果我們想到這個方法集合中查找某個方法時,只需要去找到這個方法對應(yīng)的SEL就行了,SEL實(shí)際上就是根據(jù)方法名hash化了的一個字符串,而對于字符串的比較僅僅需要比較他們的地址就可以了,可以說速度上無語倫比!
本質(zhì)上,SEL只是一個指向方法的指針(準(zhǔn)確的說,只是一個根據(jù)方法名hash化了的KEY值,能唯一代表一個方法),它的存在只是為了加快方法的查詢速度。
通過下面三種方法來獲取SEL:
-
sel_registerName函數(shù) - 編譯器提供的
@selector() -
NSSelectorFromString()方法
2. IMP
IMP實(shí)際上是一個函數(shù)指針,指向方法實(shí)現(xiàn)的首地址。其定義如下:
id (*IMP)(id, SEL, ...)
第一個參數(shù)是指向self的指針(如果是實(shí)例方法,則是類實(shí)例的內(nèi)存地址;如果是類方法,則是指向元類的指針)。第二個參數(shù)是方法選擇器(selector),接下來是方法的實(shí)際參數(shù)列表。
SEL就是為了查找方法的最終實(shí)現(xiàn)IMP的。由于每個方法對應(yīng)唯一的SEL,因此我們可以通過SEL方便快速準(zhǔn)確地獲得它所對應(yīng)的 IMP,取得IMP后,我們就獲得了執(zhí)行這個方法代碼的入口點(diǎn)。
3. 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; // 方法實(shí)現(xiàn)
}
我們可以看到該結(jié)構(gòu)體中包含一個SEL和IMP,實(shí)際上相當(dāng)于在SEL和IMP之間作了一個映射。有了SEL,我們便可以找到對應(yīng)的IMP,從而調(diào)用方法的實(shí)現(xiàn)代碼。
二、相關(guān)操作函數(shù)
1.方法相關(guān)函數(shù)
// 調(diào)用指定方法的實(shí)現(xiàn)
id method_invoke ( id receiver, Method m, ... );
// 調(diào)用返回一個數(shù)據(jù)結(jié)構(gòu)的方法的實(shí)現(xiàn)
void method_invoke_stret ( id receiver, Method m, ... );
// 獲取方法名
SEL method_getName ( Method m );
// 返回方法的實(shí)現(xiàn)
IMP method_getImplementation ( Method m );
// 獲取描述方法參數(shù)和返回值類型的字符串
const char * method_getTypeEncoding ( Method m );
// 獲取方法的返回值類型的字符串
char * method_copyReturnType ( Method m );
// 獲取方法的指定位置參數(shù)的類型字符串
char * method_copyArgumentType ( Method m, unsigned int index );
// 通過引用返回方法的返回值類型字符串
void method_getReturnType ( Method m, char *dst, size_t dst_len );
// 返回方法的參數(shù)的個數(shù)
unsigned int method_getNumberOfArguments ( Method m );
// 通過引用返回方法指定位置參數(shù)的類型字符串
void method_getArgumentType ( Method m, unsigned int index, char *dst, size_t dst_len );
// 返回指定方法的方法描述結(jié)構(gòu)體
struct objc_method_description * method_getDescription ( Method m );
// 設(shè)置方法的實(shí)現(xiàn)
IMP method_setImplementation ( Method m, IMP imp );
// 交換兩個方法的實(shí)現(xiàn)
void method_exchangeImplementations ( Method m1, Method m2 );
-
method_invoke函數(shù),返回的是實(shí)際實(shí)現(xiàn)的返回值。參數(shù)receiver不能為空。這個方法的效率會比method_getImplementation和method_getName更快。 -
method_getName函數(shù),返回的是一個SEL。如果想獲取方法名的C字符串,可以使用sel_getName(method_getName(method))。 -
method_getReturnType函數(shù),類型字符串會被拷貝到dst中。 -
method_setImplementation函數(shù),注意該函數(shù)返回值是方法之前的實(shí)現(xiàn)。
2.方法選擇器相關(guān)函數(shù)
// 返回給定選擇器指定的方法的名稱
const char * sel_getName ( SEL sel );
// 在Objective-C Runtime系統(tǒng)中注冊一個方法,將方法名映射到一個選擇器,并返回這個選擇器
SEL sel_registerName ( const char *str );
// 在Objective-C Runtime系統(tǒng)中注冊一個方法
SEL sel_getUid ( const char *str );
// 比較兩個選擇器
BOOL sel_isEqual ( SEL lhs, SEL rhs );
-
sel_registerName函數(shù):在我們將一個方法添加到類定義時,我們必須在 Runtime系統(tǒng)中注冊一個方法名以獲取方法的選擇器。
三、方法調(diào)用流程
1.流程介紹
在OC中,消息直到運(yùn)行時才綁定到方法實(shí)現(xiàn)上。編譯器會將消息表達(dá)式[receiver message]轉(zhuǎn)化為一個消息函數(shù)的調(diào)用,即objc_msgSend。這個函數(shù)將消息接收者和方法名作為其基礎(chǔ)參數(shù),如以下所示:
//無參
objc_msgSend(receiver, selector)
//有參
objc_msgSend(receiver, selector, arg1, arg2, ...)
這個函數(shù)完成了動態(tài)綁定的所有事情:
(1)首先它找到selector對應(yīng)的方法實(shí)現(xiàn)。因?yàn)橥粋€方法可能在不同的類中有不同的實(shí)現(xiàn),所以我們需要依賴于接收者的類來找到的確切的實(shí)現(xiàn)。
(2)它調(diào)用方法實(shí)現(xiàn),并將接收者對象及方法的所有參數(shù)傳給它。
(3)最后,它將實(shí)現(xiàn)返回的值作為它自己的返回值。
當(dāng)消息發(fā)送給一個對象時,objc_msgSend通過對象的isa指針獲取到類的結(jié)構(gòu)體,然后在方法分發(fā)表里面查找方法的selector。如果沒有找到selector,則通過objc_msgSend結(jié)構(gòu)體中的指向父類的指針找到其父類,并在父類的分發(fā)表里面查找方法的selector。依次,會一直沿著類的繼承體系到達(dá)NSObject類。一旦定位到selector,函數(shù)會就獲取到了實(shí)現(xiàn)的入口點(diǎn),并傳入相應(yīng)的參數(shù)來執(zhí)行方法的具體實(shí)現(xiàn)。如果最后沒有定位到selector,則會走消息轉(zhuǎn)發(fā)流程。
2.隱藏參數(shù)
objc_msgSend有兩個隱藏參數(shù):消息接收對象和方法的selector。
這兩個參數(shù)為方法的實(shí)現(xiàn)提供了調(diào)用者的信息。之所以說是隱藏的,是因?yàn)樗鼈冊诙x方法的源代碼中沒有聲明。它們是在編譯期被插入實(shí)現(xiàn)代碼的。
雖然這些參數(shù)沒有顯示聲明,但在代碼中仍然可以引用它們。我們可以使用self來引用接收者對象,使用_cmd來引用選擇器。
3.獲取方法地址
NSObject類提供了methodForSelector:方法,讓我們可以獲取到方法的指針,然后通過這個指針來調(diào)用實(shí)現(xiàn)代碼。我們需要將methodForSelector:返回的指針轉(zhuǎn)換為合適的函數(shù)類型,函數(shù)參數(shù)和返回值都需要匹配上。
四、消息轉(zhuǎn)發(fā)
通常,當(dāng)我們不能確定一個對象是否能接收某個消息時,會先調(diào)用respondsToSelector:來判斷一下。如下代碼所示:
if ([self respondsToSelector:@selector(method)]) {
[self performSelector:@selector(method)];
}
不使用respondsToSelector:判斷的情況下,當(dāng)一個對象無法接收某一消息時,就會啟動所謂--消息轉(zhuǎn)發(fā)(message forwarding)機(jī)制,通過這一機(jī)制,我們可以告訴對象如何處理未知的消息。默認(rèn)情況下,對象接收到未知的消息,會導(dǎo)致程序崩潰,通過控制臺,我們可以看到以下異常信息:
-[SUTRuntimeMethod method]: unrecognized selector sent to instance 0x100111940
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[SUTRuntimeMethod method]: unrecognized selector sent to instance 0x100111940'
消息轉(zhuǎn)發(fā)機(jī)制基本上分為三個步驟:
- 動態(tài)方法解析
- 備用接收者
- 完整轉(zhuǎn)發(fā)
1.動態(tài)方法解析
對象在接收到未知的消息時,首先會調(diào)用所屬類的類方法+resolveInstanceMethod:(實(shí)例方法)或者+resolveClassMethod:(類方法)。在這個方法中,我們有機(jī)會為該未知消息新增一個處理方法。不過使用該方法的前提是我們已經(jīng)實(shí)現(xiàn)了該處理方法,只需要在運(yùn)行時通過class_addMethod函數(shù)動態(tài)添加到類里面就可以了。如下代碼所示:
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];
}
不過這種方案更多的是為了實(shí)現(xiàn)@dynamic屬性。
2.備用接收者
如果在上一步無法處理消息,則Runtime會繼續(xù)調(diào)用forwardingTargetForSelector:方法。
- (id)forwardingTargetForSelector:(SEL)aSelector
如果一個對象實(shí)現(xiàn)了這個方法,并返回一個非nil的結(jié)果,則這個對象會作為消息的新接收者,且消息會被分發(fā)到這個對象。當(dāng)然這個對象不能是self自身,否則就是出現(xiàn)無限循環(huán)。當(dāng)然,如果我們沒有指定相應(yīng)的對象來處理aSelector,則應(yīng)該調(diào)用父類的實(shí)現(xiàn)來返回結(jié)果。
使用這個方法通常是在對象內(nèi)部,可能還有一系列其它對象能處理該消息,我們便可借這些對象來處理消息并返回,這樣在對象外部看來,還是由該對象親自處理了這一消息。
@interface DJMethod : NSObject
//聲明方法,未實(shí)現(xiàn)方法
- (void)method2;
@end
#import "DJMethod.h"
#import "DJMethodHelper.h"
DJMethodHelper *_helper;
@implementation DJMethod
+ (instancetype)object {
return [[self alloc] init];
}
- (instancetype)init {
self = [super init];
if (self != nil) {
_helper = [[DJMethodHelper alloc] init];
}
return self;
}
- (id)forwardingTargetForSelector:(SEL)aSelector {
NSLog(@"forwardingTargetForSelector");
NSString *selectorString = NSStringFromSelector(aSelector);
// 將消息轉(zhuǎn)發(fā)給_helper來處理
if ([selectorString isEqualToString:@"method2"]) {
return _helper;
}
return [super forwardingTargetForSelector:aSelector];
}
@end
@interface DJMethodHelper : NSObject
- (void)method2;
@end
#import "DJMethodHelper.h"
@implementation DJMethodHelper
- (void)method2 {
NSLog(@"%@, %p", self, _cmd);
}
@end
調(diào)用的時候
DJMethod *method = [[DJMethod alloc]init];
[method method2];
這一步合適于我們只想將消息轉(zhuǎn)發(fā)到另一個能處理該消息的對象上。但這一步無法對消息進(jìn)行處理,如操作消息的參數(shù)和返回值。
3.完整消息轉(zhuǎn)發(fā)
如果在上一步還不能處理未知消息,則唯一能做的就是啟用完整的消息轉(zhuǎn)發(fā)機(jī)制了。此時會調(diào)用以下方法:
- (void)forwardInvocation:(NSInvocation *)anInvocation
運(yùn)行時系統(tǒng)會在這一步給消息接收者最后一次機(jī)會將消息轉(zhuǎn)發(fā)給其它對象。對象會創(chuàng)建一個表示消息的NSInvocation對象,把與尚未處理的消息 有關(guān)的全部細(xì)節(jié)都封裝在anInvocation中,包括selector,目標(biāo)(target)和參數(shù)。
forwardInvocation:方法的實(shí)現(xiàn)有兩個任務(wù):
(1)定位可以響應(yīng)封裝在anInvocation中的消息的對象。這個對象不需要能處理所有未知消息。
(2)使用anInvocation作為參數(shù),將消息發(fā)送到選中的對象。anInvocation將會保留調(diào)用結(jié)果,運(yùn)行時系統(tǒng)會提取這一結(jié)果并將其發(fā)送到消息的原始發(fā)送者。
不過,在這個方法中我們可以實(shí)現(xiàn)一些更復(fù)雜的功能,我們可以對消息的內(nèi)容進(jìn)行修改,比如追回一個參數(shù)等,然后再去觸發(fā)消息。另外,若發(fā)現(xiàn)某個消息不應(yīng)由本類處理,則應(yīng)調(diào)用父類的同名方法,以便繼承體系中的每個類都有機(jī)會處理此調(diào)用請求。
還有一個很重要的問題,我們必須重寫以下方法:
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
消息轉(zhuǎn)發(fā)機(jī)制使用從這個方法中獲取的信息來創(chuàng)建NSInvocation對象。因此我們必須重寫這個方法,為給定的selector提供一個合適的方法簽名。
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
NSMethodSignature *signature = [super methodSignatureForSelector:aSelector];
if (!signature) {
if ([DJMethodHelper instancesRespondToSelector:aSelector]) {
signature = [DJMethodHelper instanceMethodSignatureForSelector:aSelector];
}
}
return signature;
}
- (void)forwardInvocation:(NSInvocation *)anInvocation {
if ([DJMethodHelper instancesRespondToSelector:anInvocation.selector]) {
[anInvocation invokeWithTarget:_helper];
}
}
NSObject的forwardInvocation:方法實(shí)現(xiàn)只是簡單調(diào)用了doesNotRecognizeSelector:方法,它不會轉(zhuǎn)發(fā)任何消息。這樣,如果不在以上所述的三個步驟中處理未知消息,則會引發(fā)一個異常。
從某種意義上來講,forwardInvocation:就像一個未知消息的分發(fā)中心,將這些未知的消息轉(zhuǎn)發(fā)給其它對象。或者也可以像一個運(yùn)輸站一樣將所有未知消息都發(fā)送給同一個接收對象。這取決于具體的實(shí)現(xiàn)。
4.消息轉(zhuǎn)發(fā)與多重繼承
回過頭來看第二和第三步,通過這兩個方法我們可以允許一個對象與其它對象建立關(guān)系,以處理某些未知消息,而表面上看仍然是該對象在處理消息。通過這種關(guān)系,我們可以模擬“多重繼承”的某些特性,讓對象可以“繼承”其它對象的特性來處理一些事情。不過,這兩者間有一個重要的區(qū)別:多重繼承將不同的功能集成到一個對象中,它會讓對象變得過大,涉及的東西過多;而消息轉(zhuǎn)發(fā)將功能分解到獨(dú)立的小的對象中,并通過某種方式將這些對象連接起來,并做相應(yīng)的消息轉(zhuǎn)發(fā)。
不過消息轉(zhuǎn)發(fā)雖然類似于繼承,但NSObject的一些方法還是能區(qū)分兩者。如respondsToSelector:和isKindOfClass:只能用于繼承體系,而不能用于轉(zhuǎn)發(fā)鏈。