iOS 消息轉發機制

EugZFTC 8年前發布 | 9K 次閱讀 iOS開發 Objective-C開發

來自: http://my.oschina.net/Jacedy/blog/625343?fromerr=P8gZMxVs

在Objective-C中,使用對象進行方法調用是一個消息發送的過程(Objective-C采用“動態綁定機制”,所以所要調用的方法直到運行期才能確定)。

方法在調用時,系統會查看這個對象能否接收這個消息(查看這個類有沒有這個方法,或者有沒有實現這個方法。),如果不能并且只在不能的情況下,就會調用下面這幾個方法,給你“補救”的機會,你可以先理解為幾套防止程序crash的備選方案,我們就是利用這幾個方案進行消息轉發,注意一點,前一套方案實現后一套方法就不會執行。如果這幾套方案你都沒有做處理,那么程序就會報錯crash。

OC的運行時在程序崩潰前提供了三次拯救程序的機會:

方案一:

+ (BOOL)resolveInstanceMethod:(SEL)sel
+ (BOOL)resolveClassMethod:(SEL)sel

方案二:

- (id)forwardingTargetForSelector:(SEL)aSelector

方案三:

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector;
- (void)forwardInvocation:(NSInvocation *)anInvocation;

上圖顯示了消息轉發的具體流程,接收者在每一步中均有機會處理消息。步驟越往后處理消息的代價越大。首先,會調用

+ (BOOL)resolveInstanceMethod:(SEL)sel 。若方法返回YES,則表示可以處理該消息。在這個過程,可以動態地給消息增加方法。

// Person.m

// 不自動生成getter和setter方法
@dynamic name; 

+ (BOOL)resolveInstanceMethod:(SEL)sel
{
    if (sel == @selector(name)) {
        // BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types)
        class_addMethod(self, sel, (IMP)GetterName, "@@:");
        return YES;
    }
    if (sel == @selector(setName:)) {
        class_addMethod(self, sel, (IMP)SetterName, "v@:@");
        return YES;
    }

    return [super resolveInstanceMethod:sel];
}

// (用于類方法)
//+ (BOOL)resolveClassMethod:(SEL)sel
//{
//    NSLog(@"resolveClassMethod called %@", NSStringFromSelector(sel));
//    
//    return [super resolveClassMethod:sel];
//}

id GetterName(id self, SEL cmd)
{
    NSLog(@"%@, %s", [self class], sel_getName(cmd));

    return @"Getter called";
}

void SetterName(id self, SEL cmd, NSString *value)
{
    NSLog(@"%@, %s, %@", [self class], sel_getName(cmd), value);

    NSLog(@"SetterName called"
);

簽名符號含義:

*          代表  char * 
char BOOL  代表  c
:          代表  SEL 
^type      代表  type *
@          代表  NSObject * 或 id
^@         代表  NSError ** 
#          代表  NSObject 
v          代表  void
// main.m
/* 現在在main.m中給Person發送setName:和name消息,由于Person中未實現這兩個方法,就會經消息轉發調用GetterName和SetterName方法
*/

Person *person = [[Person alloc] init];

[person setName:@"Jake"];

NSLog(@"%@", [person name]);
// 輸出結果:

Person, setName:, Jake
SetterName called
Person, name
Getter called

若方法返回NO,則進行消息轉發的第二步,查找是否有其它的接收者。對應的處理函數是:

- (id)forwardingTargetForSelector:(SEL)aSelector 。可以通過該函數返回一個可以處理該消息的對象。

現在新建一個類Child,在Child中實現一個eat方法,在Person類中定義eat方法但不實現它。

// Child.m

- (void)eat
{
    NSLog(@"Child method eat called");
}

然后在Person類中實現 forwardingTargetForSelector:方法:

// Person.m
// 當調用Person中的eat方法時,由于Person中并未實現該方法,就會經下面的方法將消息轉發給可以處理eat方法的對象

- (id)forwardingTargetForSelector:(SEL)aSelector
{
    NSString *selStr = NSStringFromSelector(aSelector);

    if ([selStr isEqualToString:@"eat"]) {
        return [[Child alloc] init];        // 這里返回Child類對象,讓Child去處理eat消息
    }

    return [super forwardingTargetForSelector:aSelector];
}
// main.m

[person eat];
// 輸出結果:

Child method eat called

通過此方案,我們可以用“組合”來模擬出“多重繼承”的某些特性。在一個對象內部,可能還有一系列其他對象,該對象可以經由此方法將能夠處理某選擇子的相關內部對象返回,這樣的話,在外界看來好像是該對象親自處理了這些消息。

偽多繼承與真正的多繼承的區別在于,真正的多繼承是將多個類的功能組合到一個對象中,而消息轉發實現的偽多繼承,對應的功能仍然分布在多個對象中,但是將多個對象的區別對消息發送者透明。

若第二步返回nil,則進入消息轉發的第三步。調用

- (void)forwardInvocation:(NSInvocation *)anInvocation 。這個方法實現得很簡單。只需要改變調用目標,使消息在新目標上得以調用即可。不過,如果采用這種方式,實現的效果與第二步的消息轉發是一致的。所以比較有用的實現方式是:先以某種方式改變消息內容,比如追加另外一個參數,或者改換選擇子,等等。

// Person.m

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
     NSString *sel = NSStringFromSelector(aSelector);
    // 判斷要轉發的SEL
    if ([sel isEqualToString:@"sleep"]) {
        // 為轉發的方法手動生成簽名
        return [NSMethodSignature signatureWithObjCTypes:"v@:"];
    }

    return [super methodSignatureForSelector:aSelector]; 
}

- (void)forwardInvocation:(NSInvocation *)anInvocation
{
    SEL selector = [anInvocation selector];
    // 新建需要轉發消息的對象
    Child *child = [[Child alloc] init];
    if ([child respondsToSelector:selector]) {
        // 喚醒這個方法
        [anInvocation invokeWithTarget:child];
    }
}
// Child.h

#import <Foundation/Foundation.h>

@interface Child : NSObject

- (void)eat;

- (void)sleep;

@end
// Child.m

- (void)sleep
{
    NSLog(@"Child method sleep called");
}
// 輸出結果:

Child method sleep called

有時候服務器很煩不靠譜,老是不經意間返回null,可以重寫NSNull的消息轉發方法, 讓他能處理這些異常的方法,達到解決問題的目的。

 本文由用戶 EugZFTC 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!