在面試過程當中你也許會被問到消息轉發機制。這篇文章就是對消息的轉發機制進行一個梳理。主要包括什麼是消息、靜態綁定/動態綁定、消息的傳遞和消息的轉發。接下來開始進入正題。html
在其餘語言裏面,咱們能夠用一個類去調用某個方法,在OC裏面,這個方法就是消息。某個類調用一個方法就是向這個類發送一條消息。舉個例子:git
People *zhangSan = [[People alloc] init]; People *lisi = [[People alloc] init]; [zhangSan beFriendWith:lisi];
咱們有個People的類,zhangSan這個實例發送了一條beFriendWith:的消息。你也許還看過這種調用方式:github
[zhangSan performSelector:@selector(beFriendWith:) withObject:lisi];
其目的和上面的同樣,都是向zhangSan發送了一條beFriendWith:的消息,傳人的參數都是lisi。
這裏簡單介紹一下SEL和IMP:面試
SEL:類成員方法的指針,但和C的函數指針還不同,函數指針直接保存了方法的地址,可是SEL只是方法編號。
IMP:函數指針,保存了方法地址。緩存
咱們叫@selector(beFriendWith:)爲消息的選擇子或者選擇器。(A selector identifying the message to send)ide
所謂靜態綁定,就是在編譯期就能決定運行時所調用的函數,例如:函數
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成爲一門動態語言。ui
先看一下一條簡單的消息:指針
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。
其餘的就是要傳入的參數。
這樣消息派發系統就在接收者所屬類中查找器方法列表,若是找到和選擇器名稱相符的方法就跳轉其實現代碼,若是找不到,就再其父類找,等找到合適的方法在跳轉到實現代碼。這裏跳轉到實現代碼這一操做利用了尾遞歸優化。
若是該消息沒法被該類或者其父類解讀,就會開始進行消息轉發。
不要把消息轉發機制想象得很難,其實看過下面的你就會發現,沒有那麼難。
咱們有的時候會遇到這樣的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查找。若是一直查找到根類仍舊沒有實現,則執行消息轉發。
一、調用resolveInstanceMethod:方法。容許用戶在此時爲該Class動態添加實現。若是有實現了,則調用並返回YES,從新開始objc_msgSend流程。此次對象會響應這個選擇器,通常是由於它已經調用過了class_addMethod。若是仍沒有實現,繼續下面的動做。
二、調用forwardingTargetForSelector:方法,嘗試找到一個能響應該消息的對象。若是獲取到,則直接把消息轉發給它,返回非nil對象。不然返回nil,繼續下面的動做。注意這裏不要返回self,不然會造成死循環。
三、調用methodSignatureForSelector:方法,嘗試得到一個方法簽名。若是獲取不到,則直接調用doesNotRecognizeSelector拋出異常。若是能獲取,則返回非nil;傳給一個NSInvocation並傳給forwardInvocation:。
四、調用forwardInvocation:方法,將第三步獲取到的方法簽名包裝成Invocation傳入,如何處理就在這裏面了,並返回非nil。
五、調用doesNotRecognizeSelector:,默認的實現是拋出異常。若是第三步沒能得到一個方法簽名,執行該步驟 。
另附相關雜亂代碼(裏面有動態方法解析demo)。
轉載請註明來源:http://www.cnblogs.com/zhanggui/p/7731394.html