iOS開發筆記(三):訊息傳遞與轉發機制

卡布達巨人發表於2018-01-11

1.前言

在iOS開發中經常會遇到unrecognized selector sent to instance 0x100111df0的問題,這是為什麼呢,從字面上理解來說是無法識別的selector子傳送給物件,其實呼叫一個不存在的方法就會遇到這個問題。

嚴格來說iOS中不存在方法呼叫的說法,應該說是訊息的傳遞。訊息傳遞和函式呼叫的區別就是,你可以在任意的時候對一個物件傳送任何訊息,而不需要在編譯的時候宣告。但是函式呼叫就不行。

先理解C語言的函式呼叫方式。C語言使用靜態繫結,也就是說,在編譯期就能決定程式執行時所應該呼叫的函式,所以在C語言中,如果某個函式沒有實現,編譯時是不能通過的。

在Objective-C中,如果向某物件傳遞訊息,那就會使用動態繫結機制來決定需要呼叫的方法。在底層,所有方法都是普通的C語言函式,然而物件收到訊息後,究竟該呼叫哪個方法則完全於執行期決定,甚至可以在程式執行時改變,這些特性使得Objecttive-C成為一門真正的動態語言。

2.訊息傳遞機制

2.1 objc_msgSend()

訊息有名稱或選擇子,可以接受引數,而且可能還有返回值。 給物件傳送訊息可以這樣寫:

id returnValue = [someObject messageName:parameter];
複製程式碼

someObject:接收者

messageName:選擇子(選擇器)

選擇子與引數合起來稱為“訊息”

編譯器看到此訊息後,將其轉換成一條標準的C語言函式呼叫,所呼叫的函式乃是訊息傳遞機制中的核心函式,叫做objc_msgSend,其“原型”如下:

void objc_msgSend(id self, SEL cmd, ...)

這是個“引數個數可變的函式,能接受兩個或兩個以上的引數。第一個引數代表接收者,第二個引數代表選擇子(SEL是選擇子的型別),後續引數就是訊息中的那些引數,其順序不變。選擇子指的就是方法的名字。“選擇子”與“方法”這兩個詞經常交替使用。編譯器會把剛才那個例子中的訊息轉換為如下函式:

id returnValue =  objc_msgSend(someObject, @selector(messageName:), parameter);
複製程式碼

2.2 訊息傳遞流程

objc_msgSend()函式會一句接受者(呼叫方法的物件)的型別和選擇子(方法名)來呼叫適當的方法。

  • 接收者會根據isa指標找到接收者自己所屬的類,然後在所屬類的“方法列表(method list)”中從上向下遍歷。如果能找到與選擇子名稱相符的方法,就根據IMP指標跳轉到方法的實現程式碼,呼叫這個方法的實現。
  • 如果找不到與選擇子名稱相符的方法,接收者會根據所屬類的superClass指標,沿著類的繼承體系繼續向上查詢(向父類查詢),如果能找到與名稱相符的方法,就根據IMP指標跳轉到方法的實現程式碼,呼叫這個方法的實現。
  • 如果在繼承體系中還是找不到與選擇子相符的方法,此時就會執行“訊息轉發(message forwarding)”操作。

SEL:類成員方法的指標,但和C的函式指標還不一樣,函式指標直接儲存了方法的地址,但是SEL只是方法編號。

isa指標:每個物件都有一個標識物件類的isa例項變數。執行時使用此指標來確定物件需要時的實際類。

IMP:函式指標,儲存了方法地址。

2.3 快速對映表

我們發現呼叫一個方法並不像我們想的那麼簡單,更不像我們寫的那麼簡單,一個方法的執行其實底層需要很多步驟。正因如此,objc_msgSend()會將呼叫過且匹配到的方法快取在“快速對映表(fast map)”中,快速對映表就是方法的快取表。每個類都有這樣一個快取。所以,即便子類例項從父類的方法列表中取過了某個物件方法,那麼子類的方法快取表中也會快取父類的這個方法,下次呼叫這個方法,會優先去當前類(物件所屬的類)的方法快取表中查詢這個方法,這樣的好處是顯而易見的,減少了漫長的方法查詢過程,使得方法的呼叫更快。同樣,如果父類例項物件呼叫了同樣的方法,也會在父類的方法快取表中快取這個方法。

同理,如果用一個子類物件呼叫某個類方法,也會在子類的metaclass裡快取一份。而當用一個父類物件去呼叫那個類方法的時候,也會在父類的metaclass裡快取一份。

3.訊息轉發機制

3.1 動態方法解析

動態方法解析的意思就是,徵詢訊息接受者所屬的類,看其是否能動態新增方法,以處理當前“這個未知的選擇子(unknown selector)”。例項物件在接受到無法解讀的訊息後,首先會呼叫其所屬類的下列類方法:

+ (BOOL)resolveInstanceMethod:(SEL)selector
複製程式碼

類物件在接受到無法解讀的訊息後,那麼執行期系統就會呼叫另外的一個方法,如下:

+ (BOOL)resolveClassMethod:(SEL)selector
複製程式碼

如果執行期系統已經執行完了動態方法解析,那麼訊息接受者自己就無法再以動態新增方法的形式來響應包含該未知選擇子的訊息了,此時就進入了第二階段——完整的訊息轉發。執行期系統會請求訊息接受者以其他手段來處理與訊息相關的方法呼叫。

3.2 完整的訊息轉發

完整的訊息轉發又分為兩個階段,第一階段稱為備援接受者,第二階段才是啟動完整的訊息轉發機制。

  • 備援接收者

    當前接受者如果不能處理這條訊息,執行期系統會請求當前接受者讓其他接受者處理這條訊息,與之對應的方法是:

      - (id)forwardingTargetForSelector:(SEL)selector
    複製程式碼

    方法引數代表未知的選擇子,返回值為備援接受者,若當前接受者能找到備援接受者,就直接返回,這個未知的選擇子將會交由備援接受者處理。如果找不到備援接受者,就返回nil,此時就會啟用“完整的訊息轉發機制”。

  • 完整的訊息轉發

    如果轉發演算法已經來到了這一步,那麼代表之前的所有轉發嘗試都失敗了,此時只能啟用完整的訊息轉發機制。完整的訊息轉發機制是這樣的:首先建立NSInvocation物件,把尚未處理的那條訊息有關的全部細節封裝於這個NSInvocation物件中。此物件中包含選擇子(selector)、目標(target)及引數。在觸發NSInvocation物件時,“訊息派發系統(message-dispatch system)”將親自觸發,把訊息派發給目標物件。此步驟中會呼叫下面這個方法來轉發訊息:

      - (void)forwardInvocation:(NSInvocation *)invocation
    複製程式碼

    訊息派發系統觸發訊息前,會以某種方式改變訊息內容,包括但不限於額外追加一個引數、改變選擇子等。實現此方法時,如果發現呼叫操作不應該由本類處理,則需要沿著繼承體系,呼叫父類的同名方法,這樣一來,繼承體系中的每個類都有機會處理這個呼叫請求,直至rootClass,也就是NSObject類。如果最後呼叫了NSObject的類方法,那麼該方法還會繼而呼叫“doesNotRecognizeSelector:”以丟擲異常,此異常表明選擇子最終也未能得到處理。訊息轉發到此結束。

訊息轉發流程圖

3.3 舉例

  • main.m

      #import <Foundation/Foundation.h>
      #import "Test.h"
    
      int main(int argc, const char * argv[]) {
      	Test *test = [[Test alloc] init];
      	[test instanceMethod];
      	return 0;
      }
    複製程式碼
  • Test.m

      #import <Foundation/Foundation.h>
    
      @interface Test : NSObject
    
      - (void)instanceMethod;
    
      @end
    
    
      #import "Test.h"
      #import "Test2.h"
      #import <objc/runtime.h>
    
      @implementation Test
      /*
      * 被動態新增的例項方法實現
      */
      void instanceMethod(id self, SEL _cmd) {
      	NSLog(@"收到訊息會執行此處的函式實現...");
      }
    
      /*
      * 1.第一道防線:動態方法解析
      */
      + (BOOL)resolveInstanceMethod:(SEL)sel {
      	// return NO;
      	if (sel == @selector(instanceMethod)) {
      		class_addMethod(self, sel, (IMP)instanceMethod, "v@:");
      		return YES;
      	}
      	return [super resolveInstanceMethod:sel];
      }
    
      /*
      * 2.第二道防線:備援接收者
      */
      - (id)forwardingTargetForSelector:(SEL)aSelector {
      	// return nil;
      	/* 返回轉發的物件例項 */
      	if (aSelector == @selector(instanceMethod)) {
      		return [[Test2 alloc] init];
      	}
      	return nil;
      }
    
      /*
      * 3.第三道防線:完整的訊息轉發
      */
      - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
      	/* 為指定的方法手動生成簽名 */
      	NSString *selName = NSStringFromSelector(aSelector);
      	if ([selName isEqualToString:@"instanceMethod"]) {
      		return [NSMethodSignature signatureWithObjCTypes:"v@:"];
      	}
      	return [super methodSignatureForSelector:aSelector];
      }
      - (void)forwardInvocation:(NSInvocation *)anInvocation {
      	/* 如果另一個物件可以相應該訊息,則將訊息轉發給他 */
      	SEL sel = [anInvocation selector];
      	Test2 *test2 = [[Test2 alloc] init];
      	if ([test2 respondsToSelector:sel]) {
      		[anInvocation invokeWithTarget:test2];
      	}
      }
    
      @end
    複製程式碼
  • Test2.m

      #import <Foundation/Foundation.h>
    
      @interface Test2 : NSObject
    
      - (void)instanceMethod;
    
      @end
    
    
      #import "Test2.h"
    
      @implementation Test2
    
      - (void)instanceMethod {
      	NSLog(@"訊息轉發到這...");
      }
    
      @end
    複製程式碼

4.參考

相關文章