Objective-C Runtime (二):方法與訊息轉發

雲本尊發表於2018-05-28

Objective-C Runtime (二):方法與訊息轉發

方法基礎資料型別

SEL(objc_selector)

Objc.h
/// An opaque type that represents a method selector.
typedef struct objc_selector *SEL;
複製程式碼

SEL是什麼了? 以下是官方說明:

A method selector is a C string that has been registered (or “mapped“) with the Objective-C runtime. Selectors generated by the compiler are automatically mapped by the runtime when the class is loaded.

SEL其實是Objective-C在編譯時,根據每一個方法的名字、引數序列,生成一個唯一的整型標識(Int型別的地址),這個標識就是SEL。如下程式碼所示:

SEL sel = @selector(age);
NSLog(@"%p", sel);
複製程式碼

輸出為:

2018-05-25 13:15:30.816955+0800 OC_Object_Analysis[18514:3240643] 0x7fffb3819130
複製程式碼

當我們多次執行,列印的結果永遠是0x7fffb3819130。 從上面我們可以分析出:

  1. 只要方法名相同,那麼方法的SEL就是一樣的。每一個方法都對應著一個SEL。所以,在Objective-C的用一個類中,不能存在2個同名的方法,即使引數型別不同也不行。(不能像C,C++, C#那樣的函式過載,就是函式名相同,引數不同)
  2. 不同的類可以擁有相同的selector,這個沒有問題。不同類的例項物件執行相同的selector時,會在各自的方法列表中去根據selector去尋找自己對應的IMP。

IMP

IMP實際上是一個函式指標,指向方法實現的首地址,其定義如下:

/// A pointer to the function of a method implementation. 
#if !OBJC_OLD_DISPATCH_PROTOTYPES
typedef void (*IMP)(void /* id, SEL, ... */ ); 
#else
typedef id _Nullable (*IMP)(id _Nonnull, SEL _Nonnull, ...); 
#endif
複製程式碼

當的得到IMP後,我們就獲得了執行這個方法程式碼的入口點,可以跳過Runtime的訊息傳遞機制,像呼叫普通的C語言函式一樣來使用這個函式指標,這樣省去了Runtime訊息傳遞過程中所做的一系列查詢操作,會比直接向物件傳送訊息高效一些。

方法呼叫

在Objective-C中,訊息是直到執行時才繫結到方法實現上。不像C語言,呼叫一個方法其實就是跳到記憶體中的某一點並開始執行一段程式碼。沒有任何動態的特性,因為這在編譯時就決定好了。

在Objective-C中,比如:[object foo]語法並不會立即執行 foo 這個方法的程式碼。它是在執行時給 object 傳送一條叫 foo 的訊息。這個訊息,也許會由 object 來處理,也許會被轉發給另一個物件,或者不予理睬假裝沒收到這個訊息。多條不同的訊息也可以對應同一個方法實現。這些都是在程式執行的時候決定的。

在編譯期間, Objective-C 中函式呼叫的語法都會被翻譯成一個 C 的函式呼叫 :objc_msgSend

NSMutableArray *mArr = [NSMutableArray array];
//以下兩個方法是等效的
[mArr addObject:@"1"];
((void (*)(id, SEL, id))(void *) objc_msgSend)(mArr, @selector(addObject:),@"2");
複製程式碼

那麼objc_msgSend做了什麼?我們以objc_msgSend(obj, foo)為例來說明:

  1. 首先,通過 obj 的 isa 指標找到它的 class ;
  2. 在class的cache裡找到foo,如果找到就去執行它的實現IMP;
  3. 如果在cache裡沒有找到找到foo,去class的method list 找 foo;
  4. 如果 class 中沒找到 foo,繼續往它的 superclass 中找 ;
  5. 一旦找到 foo 這個函式,把 foo 的 method_name 作為 key ,method_imp 作為 value 給存起來,在去執行它的實現IMP。

動態方法解析和轉發

當一個物件能夠接收物件時,就會走正常的呼叫流程。但如果一個物件無法接收指定訊息時,又會發生什麼事呢?比如:在上面的例子中,如果 foo 沒有找到會發生什麼?通常情況下,程式會在執行時掛掉並丟擲 unrecognized selector sent to … 的異常。但在異常丟擲前,Objective-C 的執行時會給你三次拯救程式的機會:

  1. Method resolution(動態方法解析)
  2. Fast forwarding(備用接收者)
  3. Normal forwarding(完整訊息轉發)

Objective-C Runtime (二):方法與訊息轉發

Method Resolution

首先,Objective-C 執行時會呼叫 +resolveInstanceMethod: 或者 +resolveClassMethod:,讓你有機會提供一個函式實現。如果你新增了函式並返回 YES, 那執行時系統就會重新啟動一次訊息傳送的過程。還是以 foo 為例,你可以這麼實現:

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    if (sel == @selector(foo)) {
        class_addMethod([self class], sel, (IMP)foo, "v@:");
        return YES;
    }
    
    return [super resolveInstanceMethod:sel];
}

void foo(id obj, SEL _cmd) {
    NSLog(@"foo Method ");
}
複製程式碼

如果 resolve 方法返回 NO ,執行時就會移到下一步:Fast forwarding。 @dynamic屬性的實現就是這種方案。

Fast forwarding

如果目標物件實現了 -forwardingTargetForSelector: ,並返回一個非nil的結果,則這個物件會作為訊息的新接收者,且訊息會被分發到這個物件。當然這個物件不能是self自身,否則就是出現無限迴圈。當然,如果我們沒有指定相應的物件來處理aSelector,則應該呼叫父類的實現來返回結果。

使用這個方法通常是在物件內部,可能還有一系列其它物件能處理該訊息,我們便可借這些物件來處理訊息並返回,這樣在物件外部看來,還是由該物件親自處理了這一訊息。如下程式碼所示:

#import "ViewController.h"
#import "objc/runtime.h"

@interface Person: NSObject

@end

@implementation Person

- (void)foo {
    NSLog(@"Doing foo");//Person的foo函式
}

@end

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    //執行foo函式
    [self performSelector:@selector(foo)];
}

- (id)forwardingTargetForSelector:(SEL)aSelector {
    if (aSelector == @selector(foo)) {
        return [Person new];//返回Person物件,讓Person物件接收這個訊息
    }
    
    return [super forwardingTargetForSelector:aSelector];
}

@end
複製程式碼

Normal forwarding

如果在上一步還不能處理未知訊息,執行時系統會在這一步給訊息接收者最後一次機會將訊息轉發給其它物件。

首先它會傳送**-methodSignatureForSelector:**訊息獲得函式的引數和返回值型別。如果 -methodSignatureForSelector: 返回nil,Runtime 則會發出 -doesNotRecognizeSelector: 訊息,程式這時也就掛掉了。如果返回了一個函式簽名,Runtime 就會建立一個 NSInvocation 物件併傳送 -forwardInvocation: 訊息給目標物件。

NSInvocation 實際上就是對一個訊息的描述,包括selector 以及引數等資訊。所以你可以在 -forwardInvocation: 裡修改傳進來的 NSInvocation 物件,然後傳送 -invokeWithTarget: 訊息給它,傳進去一個新的目標。

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    NSMethodSignature *signature = [super methodSignatureForSelector:aSelector];

    if (!signature) {
        signature = [NSMethodSignature signatureWithObjCTypes:"v@:"];
    }
    
    return signature;
}

- (void)forwardInvocation:(NSInvocation *)anInvocation {
    SEL sel = anInvocation.selector;
    
    Person *p = [Person new];
    if([p respondsToSelector:sel]) {
        [anInvocation invokeWithTarget:p];
    }
    else {
        [self doesNotRecognizeSelector:sel];
    }
    
}
複製程式碼

NSObject的**forwardInvocation:方法實現只是簡單呼叫了doesNotRecognizeSelector:**方法,它不會轉發任何訊息。這樣,如果不在以上所述的三個步驟中處理未知訊息,則會引發一個異常。

從某種意義上來講,forwardInvocation:就像一個未知訊息的分發中心,將這些未知的訊息轉發給其它物件。或者也可以像一個運輸站一樣將所有未知訊息都傳送給同一個接收物件。這取決於具體的實現。

Cocoa 裡很多地方都利用到了訊息傳遞機制來對語言進行擴充套件,如Responder Chain。,而 Responder Chain 保證一個訊息轉發給合適的響應者。

參考:

  1. blog.csdn.net/fengsh998/a…
  2. Message forwarding
  3. The faster objc_msgSend
  4. Understanding objective-c runtime

相關文章