前言 在面試過程中你也許會被問到消息轉發機制。這篇文章就是對消息的轉發機制進行一個梳理。主要包括什麼是消息、靜態綁定/動態綁定、消息的傳遞和消息的轉發。接下來開發進入正題。 消息的解釋 在其他語言裡面,我們可以用一個類去調用某個方法,在OC裡面,這個方法就是消息。某個類調用一個方法就是向這個類發送一 ...
前言
在面試過程中你也許會被問到消息轉發機制。這篇文章就是對消息的轉發機制進行一個梳理。主要包括什麼是消息、靜態綁定/動態綁定、消息的傳遞和消息的轉發。接下來開發進入正題。
消息的解釋
在其他語言裡面,我們可以用一個類去調用某個方法,在OC裡面,這個方法就是消息。某個類調用一個方法就是向這個類發送一條消息。舉個例子:
People *zhangSan = [[People alloc] init];
People *lisi = [[People alloc] init];
[zhangSan beFriendWith:lisi];
我們有個People的類,zhangSan這個實例發送了一條beFriendWith:的消息。你也許還看過這種調用方式:
[zhangSan performSelector:@selector(beFriendWith:) withObject:lisi];
其目和上面的一樣,都是向zhangSan發送了一條beFriendWith:的消息,傳人的參數都是lisi。
這裡簡單介紹一下SEL和IMP:
SEL:類成員方法的指針,但和C的函數指針還不一樣,函數指針直接保存了方法的地址,但是SEL只是方法編號。
IMP:函數指針,保存了方法地址。
我們叫@selector(beFriendWith:)為消息的選擇子或者選擇器。(A selector identifying the message to send)
靜態綁定/動態綁定
所謂靜態綁定,就是在編譯期就能決定運行時所調用的函數,例如:
void printHello() {
printf("Hello,world!\n");
}
void printGoodBye() {
printf("Goodbye,world!\n");
}
void doTheThing(int type) {
if (type == 0) {
printHello();
}else {
printGoodBye();
}
}
所謂動態綁定,就是在運行期才能確定調用函數:
void printHello() {
printf("Hello,world!\n");
}
void printGoodBye() {
printf("Goodbye,world!\n");
}
void doTheThing(int type) {
void (*fnc)(void);
if (type == 0) {
fnc = printHello;
}else {
fnc = printGoodBye;
}
fnc();
}
在OC中,對象發送消息,就會使用動態綁定機制來決定需要調用的方法。其實底層都是C語言實現的函數,當對象收到消息後,究竟調用那個方法完全決定於運行期,甚至你也可以直接在運行時改變方法,這些特性都使OC成為一門動態語言。
消息的傳遞
先看一下一條簡單的消息:
id returnValue = [someObject messageName:parameter];
其中:
someObject叫做接收者(receiver)。
messageName叫做選擇器(selector)
選擇器和參數合起來成為消息(message)
當編譯器看到這條消息,就會轉換成一條標準的C函數:objc_msgSend,此時會變成:
id returnValue = objc_msgSend(someObject,@selector(messageName:),parameter);
objc_msgSend可以在objc裡面的message.h中看到:
根據官方註釋可以看到:
When it encounters a method call, the compiler generates a call to one of the functions objc_msgSend, objc_msgSend_stret, objc_msgSendSuper, or objc_msgSendSuper_stret. Messages sent to an object’s superclass (using the super keyword) are sent using objc_msgSendSuper; other messages are sent using objc_msgSend. Methods that have data structures as return values are sent using objc_msgSendSuper_stret and objc_msgSend_stret.
它的作用是向一個實例類發送一個帶有簡單返回值的message。是一個參數個數不定的函數。當遇到一個方法調用,編譯器會生成一個objc_msgSend的調用,有:objc_msgSend_stret、objc_msgSendSuper或者是objc_msgSendSuper_stret。發送個父類的message會使用objc_msgSendSuper,其他的消息會使用objc_msgSend。如果方法的返回值是一個結構體(structures),那麼就會使用objc_msgSendSuper_stret或者objc_msgSend_stret。
第一個參數是:指向接收該消息的類的實例的指針
第二個參數是:要處理的消息的selector。
其他的就是要傳入的參數。
這樣消息派發系統就在接收者所屬類中查找器方法列表,如果找到和選擇器名稱相符的方法就跳轉其實現代碼,如果找不到,就再起父類找,等找到合適的方法在跳轉到實現代碼。這裡跳轉到實現代碼這一操作利用了尾遞歸優化。
如果該消息無法被該類或者其父類解讀,就會開始進行消息轉發。
理解消息轉發機制(message forwarding)
動態方法解析
不要把消息轉發機制想象得很難,其實看過下麵的你就會發現,沒有那麼難。
我們有的時候會遇到這樣的crash:
我們都知道crash的原因是People沒有gotoschool這個方法,但是你調用了該方法,所以會產生NSInvalidArgumentException,reason:
-[People gotoschool]: unrecognized selector sent to instance 0x1d4201780'
接下來讓我們看看從發送消息到此crash的過程。前面消息的傳遞沒有成功找到實現,所以會走到消息轉發裡面,我先在People類裡面實現了這樣一個方法:
void gotoSchool(id self,SEL _cmd,id value) {
printf("go to school");
}
//對象在收到無法解讀的消息後,首先將調用所屬類的該方法。
+ (BOOL)resolveInstanceMethod:(SEL)sel {
NSString *selectorString = NSStringFromSelector(sel);
if ([selectorString isEqualToString:@"gotoschool"]) {
class_addMethod(self, sel, (IMP)gotoSchool, "@@:");
}
return [super resolveInstanceMethod:sel];
}
然後再次運行程式,你會發現沒有crash了,而且順利列印出來"go to school"。
這個是什麼個情況呢?先看看這個方法:
+ (BOOL)resolveInstanceMethod:(SEL)sel OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);
+ (BOOL)resolveClassMethod:(SEL)sel OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);
這個方法是objc裡面NSObject.h裡面的方法。從字面理解就是處理實例方法(處理類方法)。下麵是對其的介紹:
它的作用就是給一個實例方法(給定的選擇器)動態提供一個實現。註釋也提供了一個demo告訴我們如何動態添加實現。
也就是說當消息傳遞無法處理的時候,首先會看一下所屬類,是否能動態添加方法,以處理當前未知的選擇子。這個過程叫做“動態方法解析”(dynamic method resolution)。
這裡我在動態方法解析這裡動態添加了實現,然後程式就不會崩潰啦。
如果是類方法,就調用resolveClassMethod:方法進行操作,和上面的resolveInstanceMethod一樣的處理方式。
這裡還用到了calss_addMethod,後面會單獨寫篇博客對其介紹。感興趣的可以先自行查看API。
備援接收者
當動態方法解析沒有實現或者無法處理的時候,就會執行
- (id)forwardingTargetForSelector:(SEL)aSelector OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);
這個方法也是objc裡面NSObject.h裡面的方法。我對People進行瞭如下處理:
- (id)forwardingTargetForSelector:(SEL)aSelector {
NSString *selectorString = NSStringFromSelector(aSelector);
if ([selectorString isEqualToString:@"gotoschool"]) {
return self.student;
}
return nil;
}
我在People裡面添加了一個Student類實例,然後實現了forwardingTargetForSelector:方法。然後運行,奇跡地發現程式也沒有崩潰。該方法的作用是(上圖也有介紹):
返回一個對未識別消息處理的對象。如果實現了該方法,並且該方法沒有返回nil,那麼這個返回的對象就會作為新的接收對象,這個未知的消息將會被新對象處理。通過此方案,我們可以用組合來模擬多重繼承的某些特性,比如我返回多個類的組合,那麼就像繼承多個類一樣進行處理。在對外調用者來說,好像就是該對象親自處理的這些消息。
消息轉發
當動態方法解析和備援接收者都沒有進行處理的話,就會執行:
- (void)forwardInvocation:(NSInvocation *)anInvocation OBJC_SWIFT_UNAVAILABLE("");
這個方法也是objc裡面NSObject.h裡面的方法,我對People進行如下處理:
- (void)forwardInvocation:(NSInvocation *)anInvocation {
NSLog(@"%@ can't handle by People",NSStringFromSelector([anInvocation selector]));
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
NSMethodSignature *sign = [NSMethodSignature signatureWithObjCTypes:"@@:"];
return sign;
}
再次運行程式,發現程式沒有崩潰,只不過列印出來了“gotoschool can't handle by People”。
forwardInvocation:方法是將消息轉發給其他對象。
從註釋看:對一個你的對象不識別的消息進行相應,你必須重寫methodSignatureForSelector:方法,該方法返回一個NSMethodSIgnature對象,該對象包含了給定選擇器所標識方法的描述。主要包含返回值的信息和參數信息。
實現forwardInvocation:方法時,若發現調用的message不是由本類處理,則續調用超類的同名方法。這樣所有父類均有機會處理此消息,直到NSObject。如果最後調用了NSObject的方法,那麼該方法就會調用“doesNotRecognizerSelector:”,拋出異常,標明選擇器最終未能得到處理。也就是上面的crash:NSInvalidArgumentException。
至此,真個消息轉發全流程結束。
上一個王圖:
總結
接收者在每一步都有機會對未知消息進行處理,一句話:越早處理越好。如果能在第一步做完,就不進行其他操作,因為動態方法解析會將此方法緩存。如果動態方法解析不了,就放到第二步備援接收者,因為第三步還要創建完整的NSInvocation。
在完整來一遍:
Q:說一下你理解的消息轉發機制?
A:
先會調用objc_msgSend方法,首先在Class中的緩存查找IMP,沒有緩存則初始化緩存。如果沒有找到,則向父類的Class查找。如果一直查找到根類仍舊沒有實現,則執行消息轉發。
1、調用resolveInstanceMethod:方法。允許用戶在此時為該Class動態添加實現。如果有實現了,則調用並返回YES,重新開始objc_msgSend流程。這次對象會響應這個選擇器,一般是因為它已經調用過了class_addMethod。如果仍沒有實現,繼續下麵的動作。
2、調用forwardingTargetForSelector:方法,嘗試找到一個能響應該消息的對象。如果獲取到,則直接把消息轉發給它,返回非nil對象。否則返回nil,繼續下麵的動作。註意這裡不要返回self,否則會形成死迴圈。
3、調用methodSignatureForSelector:方法,嘗試獲得一個方法簽名。如果獲取不到,則直接調用doesNotRecognizeSelector拋出異常。如果能獲取,則返回非nil;傳給一個NSInvocation並傳給forwardInvocation:。
4、調用forwardInvocation:方法,將第三步獲取到的方法簽名包裝成Invocation傳入,如何處理就在這裡面了,並返回非nil。
5、調用doesNotRecognizeSelector:,預設的實現是拋出異常。如果第三步沒能獲得一個方法簽名,執行該步驟 。
另附相關雜亂代碼(裡面有動態方法解析demo)。
轉載請註明來源:http://www.cnblogs.com/zhanggui/p/7731394.html