參考文章:
繼承自NSObject的不常用又很有用的函數【重點推薦】
Objective-C Runtime 1小時入門教程【重點推薦】
類的本質-類對象
運行時消息傳遞與轉發機制
深入淺出理解消息的傳遞和轉發機制
消息轉發機制原理和實際用途
一、消息傳遞過程
objc_msgSend()函數會依據接收者(調用方法的對象)的類型和選擇子(方法名)來調用適當的方法。
1、接收者會根據isa指針找到接收者自己所屬的類,然后在該類的緩存中查找對應的IMP,如果找到了,則根據IMP指針跳轉到方法的實現代碼,調用這個方法的實現;如果沒有緩存則初始化緩存,進入步驟2
方法緩存:
發現調用一個方法并不像我們想的那么簡單,更不像我們寫的那么簡單,一個方法的執行其實底層需要很多步驟。
正因如此,objc_msgSend()會將調用過且匹配到的方法緩存在”快速映射表(fast map)“中,快速映射表就是方法的緩存表。每個類都有這樣一個緩存。
所以,即便子類實例從父類的方法列表中取過了某個對象方法,那么子類的方法緩存表中也會緩存父類的這個方法,下次調用這個方法,會優先去當前類(對象所屬的類)的方法緩存表中查找這個方法,這樣的好處是顯而易見的,減少了漫長的方法查找過程,使得方法的調用更快。
同樣,如果父類實例對象調用了同樣的方法,也會在父類的方法緩存表中緩存這個方法。
同理,如果用一個子類對象調用某個類方法,也會在子類的metaclass里緩存一份。而當用一個父類對象去調用那個類方法的時候,也會在父類的metaclass里緩存一份。
2、在所屬類的”方法列表“(method list)中從上向下遍歷。如果能找到與選擇子名稱相符的方法,就根據IMP指針跳轉到方法的實現代碼,調用這個方法的實現。
3、如果找不到與選擇子名稱相符的方法,接收者會根據所屬類的superClass指針,沿著類的繼承體系繼續向上查找(向父類查找),如果能找到與名稱相符的方法,就根據IMP指針跳轉到方法的實現代碼,調用這個方法的實現。
4、如果在繼承體系中還是找不到與選擇子相符的方法,此時就會執行”消息轉發(message forwarding)“操作。
二、消息轉發過程
Q:說一下你理解的消息轉發機制?
解說如下:
先會調用objc_msgSend方法,根據消息接收者對象的isa指針,找到接收者對象所屬的類Class,首先在Class的緩存中查找IMP,沒有緩存則初始化緩存。如果沒有找到,則通過Class的superClass指針向父類的Class查找。如果一直查找到根類【即在繼承體系中查找】仍舊沒有實現,則執行消息轉發。
當遇到一個方法調用,編譯器會生成一個objc_msgSend的調用,有4種:
objc_msgSend:其他的消息會使用
objc_msgSend_stret:
objc_msgSendSuper: 發送給父類的message會使用
objc_msgSendSuper_stret:
如果方法的返回值是一個結構體(structures),那么就會使用objc_msgSendSuper_stret或者objc_msgSend_stret。
1、動態方法解析:
調用resolveInstanceMethod:方法。允許用戶在此時為該Class動態添加實現。如果有實現了,則調用并返回YES,重新開始objc_msgSend流程。這次對象會響應這個選擇器,一般是因為它已經調用過了class_addMethod。如果仍沒有實現,繼續下面的動作。
在當前類中重寫此方法:
void gotoSchool(id self,SEL _cmd,id value) {
printf("go to school");
}
//第一步:對象在收到無法解讀的消息后,首先將調用所屬類的該方法。
//這個函數在運行時(runtime),沒有找到SEL的IML時就會執行。
//這個函數是給類利用class_addMethod添加函數的機會。
//根據文檔,如果實現了添加函數代碼則返回YES,未實現返回NO。
+ (BOOL)resolveInstanceMethod:(SEL)sel {
NSString *selectorString = NSStringFromSelector(sel);
if ([selectorString isEqualToString:@"gotoschool"]) {
class_addMethod(self, sel, (IMP)gotoSchool, "@@:");
}
return [super resolveInstanceMethod:sel];
}
如果運行期系統已經執行完了動態方法解析,那么消息接受者自己就無法再以動態新增方法的形式來響應包含該未知選擇子的消息了,此時就進入了第二階段——完整的消息轉發。運行期系統會請求消息接受者以其他手段來處理與消息相關的方法調用。
2、備援接收者
調用forwardingTargetForSelector:方法,嘗試找到一個能響應該消息的對象。如果獲取到,則直接把消息轉發給它,返回非nil對象。否則返回nil,繼續下面的動作。注意這里不要返回self,否則會形成死循環。
//第三步:備援接收者,讓其他對象進行處理
- (id)forwardingTargetForSelector:(SEL)aSelector {
NSString *selectorString = NSStringFromSelector(aSelector);
if ([selectorString isEqualToString:@"gotoschool"]) {
return self.student;
}
return nil;
}
3、完整的消息轉發
3.1 調用methodSignatureForSelector:方法,嘗試獲得一個方法簽名。如果獲取不到,則直接調用doesNotRecognizeSelector拋出異常。如果能獲取,則返回非nil;傳給一個NSInvocation并傳給forwardInvocation:。
對一個你的對象不識別的消息進行響應,你必須重寫methodSignatureForSelector:方法,該方法返回一個NSMethodSIgnature對象,該對象包含了給定選擇器所標識方法的描述(如:方法名SEL、方法參數、方法返回值、接收者等信息)。主要包含返回值的信息和參數信息。
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
NSMethodSignature *sign = [NSMethodSignature signatureWithObjCTypes:"@@:"];
return sign;
}
3.2 調用forwardInvocation:方法,將上一步獲取到的方法簽名包裝成Invocation傳入,如何處理就在這里面了,并返回非nil。
- (void)forwardInvocation:(NSInvocation *)anInvocation {
NSLog(@"%@ can't handle by People",NSStringFromSelector([anInvocation selector]));
}
forwardInvocation:真正執行從methodSignatureForSelector:返回的NSMethodSignature。在forwardInvocation:函數里可以將NSInvocation多次轉發到多個對象中,這也是這種方式靈活的地方。(forwardingTargetForSelector只能以Selector的形式轉向一個對象)
// 第一步:我們不動態添加方法,返回NO,進入第二步;
+ (BOOL)resolveInstanceMethod:(SEL)sel
{
return NO;
}
// 第二部:我們不指定備選對象響應aSelector,進入第三步;
- (id)forwardingTargetForSelector:(SEL)aSelector
{
return nil;
}
// 第三步:返回方法選擇器,然后進入第四部;
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
if ([NSStringFromSelector(aSelector) isEqualToString:@"sing"]) {
return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}
return [super methodSignatureForSelector:aSelector];
}
// 第四部:這步我們修改調用方法
- (void)forwardInvocation:(NSInvocation *)anInvocation
{
[anInvocation setSelector:@selector(dance)];
// 這還要指定是哪個對象的方法
[anInvocation invokeWithTarget:self];
}
// 若forwardInvocation沒有實現,則會調用此方法
- (void)doesNotRecognizeSelector:(SEL)aSelector
{
NSLog(@"消息無法處理:%@", NSStringFromSelector(aSelector));
}
- (void)dance
{
NSLog(@"跳舞!!!come on!");
}
消息派發系統觸發消息前,會以某種方式改變消息內容,包括但不限于額外追加一個參數、改變選擇子等。
實現此方法時,如果發現調用操作不應該由本類處理,則需要沿著繼承體系,調用父類的同名方法,這樣一來,繼承體系中的每個類都有機會處理這個調用請求,直至rootClass,也就是NSObject類。
如果最后調用了NSObject的類方法,那么該方法還會繼而調用”doesNotRecognizeSelector:“以拋出異常,此異常表明選擇子最終也未能得到處理。消息轉發到此結束。
3.3調用doesNotRecognizeSelector:,默認的實現是拋出異常。如果第三步沒能獲得一個方法簽名,執行該步驟 。
擴展
1、對象調用method代碼示例
iOS 使用NSMethodSignature和 NSInvocation進行 method 或 block的調用
一個實例對象可以通過三種方式調用其方法。
- (void)test{
//type1
[self printStr1:@"hello world 1"];
//type2
[self performSelector:@selector(printStr1:) withObject:@"hello world 2"];
//type3
//獲取方法簽名
NSMethodSignature *sigOfPrintStr = [self methodSignatureForSelector:@selector(printStr1:)];
//獲取方法簽名對應的invocation
NSInvocation *invocationOfPrintStr = [NSInvocation invocationWithMethodSignature:sigOfPrintStr];
/**
設置消息接受者,與[invocationOfPrintStr setArgument:(__bridge void * _Nonnull)(self) atIndex:0]等價
*/
[invocationOfPrintStr setTarget:self];
/**設置要執行的selector。與[invocationOfPrintStr setArgument:@selector(printStr1:) atIndex:1] 等價*/
[invocationOfPrintStr setSelector:@selector(printStr1:)];
//設置參數
NSString *str = @"hello world 3";
[invocationOfPrintStr setArgument:&str atIndex:2];
//開始執行
[invocationOfPrintStr invoke];
}
- (void)printStr1:(NSString*)str{
NSLog(@"printStr1 %@",str);
}
2、ObjcTypes
它是一個是字符串數組,該數組包含了方法的類型編碼。
如:"v@:@"。
那究竟是如何得來該字符串呢?其實我們有兩種方式:
- 直接查表。在Type Encodings里面列出了對應關系。
- 使用 @encode()計算。( NSLog(@"%s",@encode(BOOL))的結果為B )
在OC中,每一種數據類型可以通過一個字符編碼來表示(Objective-C type encodings)。例如字符‘@’代表一個object, 'i'代表int。 那么,由這些字符組成的字符數組就可以表示方法類型了。
舉個例子:printStr1:
對應的ObjCTypes 為 v@:@。
// 第三步:返回方法選擇器,然后進入第四部;
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
if ([NSStringFromSelector(aSelector) isEqualToString:@"sing"]) {
return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}
return [super methodSignatureForSelector:aSelector];
}
’v‘ : void類型,第一個字符代表返回值類型
’@‘ : 一個id類型的對象,第一個參數類型
’:‘ : 對應SEL,第二個參數類型
’@‘ : 一個id類型的對象,第三個參數類型,也就是- (void)printStr1:(NSString*)str中的str。
消息發送會被轉換成objc _ msgSend(id reciever,SEL sel,prarams1,params2,....)。所以上面的:
- (void)printStr1:(NSString*)str{
NSLog(@"printStr1 %@",str);
}
[zhagnsan printStr1:lisi]
//方法會被轉換成
void objc_msgSend(zhangsan,@selector(printStr1:),lisi); //包含兩個隱藏參數
這里的 “v@:@”就代表:
"v":代表返回值void
"@":代表一個對象,這里指代的id類型zhangsan,也就是消息的receiver
":":代表SEL
"@":代表參數lisi