最近開始學習Runtime相關的內容.之前的話知道OC是一門動態的語言(動態類型,動態綁定,動態加載),卻不知道到底是些什么東西,了解過Runtime之后,對OC的了解更加深刻了,感覺什么都是動態的了.
在了解消息轉發機制之前,我們先要知道以下幾個概念:SEL, IMP, Method
SEL
SEL又叫做方法選擇器,是一個方法的selector指針,定義如下
typedef struct objc_selector *SEL;
struct objc_selector {
char *name; OBJC2_UNAVAILABLE;// 名稱
char *types; OBJC2_UNAVAILABLE;// 類型
};
@selector(test)里面的test表示方法運行時的名字.OC在編譯時,會依據每一個方法的名字、序列,生成一個唯一的整形標識(Int類型的地址),這個標識就是SEL
SEL test = @selector(test);
兩個類之間,無論有沒有父子關系或者兄弟關系,只要方法名相同,那么方法的SEL就是一樣的.每一個方法都對應著一個SEL,所以在OC中同一個類(及該類的子類或者父類中),不能存在兩個同名的方法,即使參數的類型不同也不行.相同的方法只能對應一個SEL.
工程中的SEL會組成一個set集合,set里面的元素是不能重復的,所以里面的SEL都是唯一的.因此,如果我們想到這個方法集合中查找某個方法時,只需要去找到這個方法對應的SEL就行了,SEL實際上就是根據方法名hash化了的一個字符串,而對于字符串的比較僅僅需要比較他們的地址就可以了,所以說速度上比較快
本質上,SEL只是一個指向方法的指針(準確的來說,只是一個根據方法名hash化了的KEY值),它的存在是為了加快方法的查詢速度
IMP
IMP(implementation的縮寫)不是打LOL的那個imp,他實際上是一個函數指針,指向方法實現的首地址,定義如下
typedef id (*IMP)(id, SEL, ...);
- id----指向self的指針(如果是實例方法,則是實例的內存地址;如果是類方法,則是指向元類的指針)
- SEL----方法選擇器
- ...----方法的實際參數列表
前面說過,每一個方法都只有唯一的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; // 方法實現
}
方法操作
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 );
基本消息傳遞
在OC中,對象調用方法叫做發送消息. 在編譯時,程序的源代碼會將從消息發送消息轉化成Runtime中objc_msgSend函數調用.
id objc_msgSend(id self, SEL op, ...)
- self----一個指向接收消息類的實例指針
- op-----消息的SEL
- ...------該方法包含的參數
當發生一個objc_msgsend函數調用時,編譯器會根據函數類型自動轉化成下列的某一個函數
- objc_msgSend 普通的消息都會通過該函數發送
- objc_msgSend_stret 消息中有數據結構作為返回值(不是簡單值)時,通過此函數發送和接收返回值
- objc_msgSendSuper 這里把消息發送給父類的實例
- objc_msgSendSuper_stret 這里把消息發送給父類的實例并接收返回值
需要注意的地方:
像objc_msgSend(self, sayHello, @"大家好!");
這樣子調用objc_msgSend函數可能會以下的錯誤:
報錯Too many arguments to function call ,expected 0,have3
直接通過objc_msgSend(self, setter, value)
會報錯,說參數過多。請這樣解決:Build Setting–> Apple LLVM 7.0 – Preprocessing–> Enable Strict Checking of objc_msgSend Calls 改為 NO
當然你也可以這樣(推薦):
((void (*)(id, SEL, id))objc_msgSend)(self, sayHello, @"大家好");
強制轉換objc_msgSend函數類型為帶三個參數且返回值為void函數,然后才能傳三個參數。
objc_msgSend函數的調用過程:
- 第一步:檢測這個selector是不是要忽略的。
- 第二步:檢測這個target是不是nil對象。nil對象發送任何一個消息都會被忽略掉。
- 第三步:
- 1.調用實例方法時,它會首先在自身isa指針指向的類methodLists中查找該方法,如果找不到則會通過class的super_class指針找到父類的類對象結構體,然后從methodLists中查找該方法,如果仍然找不到,則繼續通過super_class向上一級父類結構體中查找,直至根class;
- 2.當我們調用某個某個類方法時,它會首先通過自己的isa指針找到metaclass,并從其中methodLists中查找該類方法,如果找不到則會通過metaclass的super_class指針找到父類的metaclass對象結構體,然后從methodLists中查找該方法,如果仍然找不到,則繼續通過super_class向上一級父類結構體中查找,直至根metaclass;
- 第四部:前三部都找不到就會進入動態方法解析.
消息的動態解析
動態解析流程圖(圖片來自網絡):
- 第一步: 通過
resolveInstanceMethod:
方法決定是否動態添加方法。如果返回Yes則通過class_addMethod()
動態添加方法,消息得到處理,結束;如果返回No,則進入下一步(開始動態方法解析); - 第二步: 這步會進入
forwardingTargetForSelector:
方法,用于指定備選對象響應這個消息,不能指定為self(會出現死循環)。如果返回某個對象則會調用對象的方法,結束。如果返回nil,則進入第三步; - 第三部: 這步我們要通過
methodSignatureForSelector:
方法簽名,如果返回nil,則消息無法處理。如果返回methodSignature,則進入第四步; - 第四部: 這步調用
forwardInvocation:
方法,我們可以通過anInvocation對象做很多處理,比如修改實現方法,修改響應對象等,如果方法調用成功,則結束。如果失敗,則進入doesNotRecognizeSelector
方法,若我們沒有實現這個方法,那么就會crash。
第一步:動態方法解析
當對象接受到未知消息或者該方法還沒有實現的時候,就會調用所屬類的類方法+(Bool)resolveInstanceMethod:
在這個Demo里面的Person.h文件代碼如下
#import <Foundation/Foundation.h>
@interface Person : NSObject
- (void)play;
@end
Person.m文件里面的代碼如下:
#import "Person.h"
#import <objc/runtime.h>
#import <objc/message.h>
@implementation Person
+ (BOOL)resolveInstanceMethod:(SEL)sel
{
// 我們沒有給People類實現play方法,我們這里動態添加方法
if ([NSStringFromSelector(sel) isEqualToString:@"play"]) {
class_addMethod(self, sel, (IMP)myPlay, "v@:");
}
return [super resolveInstanceMethod:sel];
}
void myPlay(id self, SEL cmd)
{
NSLog(@"我要出去玩啦");
}
@end
在main.m中的代碼如下:
#import <Foundation/Foundation.h>
#import "Person.h"
int main(int argc, const char * argv[]) {
@autoreleasepool {
Person *aPerson = [[Person alloc] init];
[aPerson play];
}
return 0;
}
結果如下:
在Person.m文件里面,我們沒有實現play
這個方法,所以如果調用了這個方法的話,就會調用+ (Bool)resolveInstanceMethod
,動態的添加方法.
Demo地址----->第一步的Demo里面的RuntimeMsg01
第二步:備選響應者
如果在上一步無法處理消息,那么Runtime會繼續調用以下方法,Person.m中的關鍵代碼如下:
//第一步:我們不動態添加方法,返回NO,進入第二步;
+ (BOOL)resolveInstanceMethod:(SEL)sel
{
return NO;
}
//第二部:我們指定備選對象響應aStudent;
- (id)forwardingTargetForSelector:(SEL)aSelector
{
Student *aStudent = [[Student alloc] init];
return aStudent;
}
在上面的代碼中,我們指定了Student的一個實例對象aStudent來作為新的消息響應者. 在student.m中的關鍵代碼如下:
+ (BOOL)resolveInstanceMethod:(SEL)sel
{
// 我們沒有給Student聲明和實現play方法,我們這里動態添加方法
if ([NSStringFromSelector(sel) isEqualToString:@"play"]) {
class_addMethod(self, sel, (IMP)myPlay, "v@:");
return YES;
}
return [super resolveInstanceMethod:sel];
}
void myPlay(id self, SEL cmd)
{
NSLog(@"小學生要開始玩啦");
}
forwardingTargetForSelector
如果返回了一個對象(不能是self自身,不然會出現死循環),那么這個對象就會作為消息的新接受者,并且消息會被分發到這個對象.
使用這個方法通常是在對象的內部,這樣子在外部看起來,就好像由self(本身)處理了這個消息一樣.
這一步適合我們指向將消息轉發到另一個能處理該消息的對象上. 但這一步無法對消息進行處理,如操作消息的參數和返回值.
Demo地址---->第二步的Demo里面的RuntimeMsg02
第三步和第四步
如果第二步那里還不能處理未知消息,那么就會進入第三步. 代碼如下:
// 第一步:我們不動態添加方法,返回NO,進入第二步;
+ (BOOL)resolveInstanceMethod:(SEL)sel
{
return NO;
}
// 第二部:我們不指定備選對象響應aSelector,進入第三步;
- (id)forwardingTargetForSelector:(SEL)aSelector
{
return nil;
}
// 第三步:返回方法選擇器,然后進入第四部;
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
if ([NSStringFromSelector(aSelector) isEqualToString:@"play"]) {
return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}
return [super methodSignatureForSelector:aSelector];
}
// 第四部:這步我們修改調用方法
- (void)forwardInvocation:(NSInvocation *)anInvocation
{
// 在這里,dance的類型需要跟第三步中函數類型'v@:'相同
[anInvocation setSelector:@selector(dance)];
// 這還要指定是哪個對象的方法
[anInvocation invokeWithTarget:self];
}
// 若forwardInvocation沒有實現,則會調用此方法
- (void)doesNotRecognizeSelector:(SEL)aSelector
{
NSLog(@"消息無法處理:%@", NSStringFromSelector(aSelector));
}
- (void)dance
{
NSLog(@"我要開始跳舞啦");
}
運行時系統會在這一步給消息接收者最后一次機會將消息轉發給其它對象。對象會創建一個表示消息的NSInvocation對象,把與尚未處理的消息 有關的全部細節都封裝在anInvocation中,包括selector,目標(target)和參數。我們可以在這個方法中選擇將消息轉發給其它對象,更改方法的實現等.
在這個方法中我們可以實現一些更加復雜的功能,我們可以對消息的內容進行修改,比如
- (void)setArgument:(void *)argumentLocation atIndex:(NSInteger)idx;
修改NSInvocation對象里面的參數然后再去觸發消息.
從某種意義上講,forwardInvocation:就像一個未知消息的分發中心,將這些未知消息轉發給不同對象,或者執行不同的方法實現
Demo地址---->第三步的Demo里面的RuntimeMsg03
消息轉發與多重繼承
回過頭來看第二和第三步,通過這兩個方法我們可以允許一個對象與其它對象建立關系, 以處理某些未知消息,而表面上看仍然是該對象在處理消息。通過這種方法,我們可以模擬“多重繼承”的某些特性,讓對象可以“繼承”其它對象的特性來處理一些事情。不過,這兩者間有一個重要的區別:多重繼承將不同的功能 集成到一個對象中,它會讓對象變得過大,涉及的東西過多;而消息轉發將功能分解到獨立的小的對象中,并通過某種方式將這些對象連接起來,并做相應的消息轉發。總之,Objective-C通過這種方式,一定程度上減小了自己不支持多繼承的劣勢。
結尾
通過這幾天的學習跟整理, 才寫完這篇文章,在此,我們已經了解了Runtime中消息發送和轉發的基本機制. 文筆的原因, 文章結構不是很清晰, 還請見諒。對運行時理解不到位,或者是有錯誤的地方,還請廣大博友指出,感激不盡!
本文如果對您有幫助的話請隨手給個喜歡哈,謝謝!
Demo地址---->全部的Demo
參考資料:
Objective-C Runtime 1小時入門教程
Objective-C Runtime 運行時之三:方法與消息