Objective-C 消息轉發

Objective-C 消息轉發

Posted on August 14, 2012 by xuguoxing

一.消息轉發流程

當向Objective-C對象發送一個消息,但runtime在當前類及父類中找不到此selector對應的方法時,消息轉發(message forwarding)流程開始啓動。

  1. 動態方法解析(Dynamic Method Resolution或Lazy method resolution)
    向當前類(Class)發送resolveInstanceMethod:(對於類方法則爲resolveClassMethod:)消息,如果返回YES,則系統認爲請求的方法已經加入到了,則會重新發送消息。
  2. 快速轉發路徑(Fast forwarding path)
    若果當前target實現了forwardingTargetForSelector:方法,則調用此方法。如果此方法返回除nil和self的其他對象,則向返回對象重新發送消息。
  3. 慢速轉發路徑(Normal forwarding path)
    首先runtime發送methodSignatureForSelector:消息查看Selector對應的方法簽名,即參數與返回值的類型信息。如果有方法簽名返回,runtime則根據方法簽名創建描述該消息的NSInvocation,向當前對象發送forwardInvocation:消息,以創建的NSInvocation對象作爲參數;若methodSignatureForSelector:無方法簽名返回,則向當前對象發送doesNotRecognizeSelector:消息,程序拋出異常退出。

    Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[MessageInterceptor test]: unrecognized selector sent to instance 0x9589830'

二.動態解析(Lazy Resolution)

runtime發送消息的流程即查找該消息對應的方法或IMP,然後跳轉至對應的IMP。有時候我們不想事先在類中設置好方法,而想在運行時動態的在類中插入IMP。這種方法是真正的快速”轉發”,因爲一旦對應的方法被添加到類中,後續的方法調用就是正常的消息發送流程。此方法的缺點是不夠靈活,你必須有此方法的實現(IMP),這意味這你必須事先預測此方法的參數和返回值類型。

@dynamic屬性是使用動態解析的一個例子,@dynamic告訴編譯器該屬性對應的getter或setter方法會在運行時提供,所以編譯器不會出現warning; 然後實現resolveInstanceMethod:方法在運行時將屬性相關的方法加入到Class中。

respondsToSelector:instancesRespondToSelector:方法被調用時,若該方法在類中未實現,動態方法解析器也會被調用,這時可向類中增加IMP,並返回YES,則對應的respondsToSelector:的方法也返回YES。

三.快速轉發(Fast Forwarding)

runtime然後會檢查你是否想將此消息不做改動的轉發給另外一個對象,這是比較常見的消息轉發情形,可以用較小的消耗完成。
快速轉發技術可以用來實現僞多繼承,你只需編寫如下代碼

- (id)forwardingTargetForSelector:(SEL)sel { return _otherObject; }

這樣做會將任何位置的消息都轉發給_otherObject對象,儘管當前對象與_otherObject對象是包含關係,但從外界看來當前對象和_otherObject像是同一個對象。
僞多繼承與真正的多繼承的區別在於,真正的多繼承是將多個類的功能組合到一個對象中,而消息轉發實現的僞多繼承,對應的功能仍然分佈在多個對象中,但是將多個對象的區別對消息發送者透明。

四.慢速轉發(Normal Forwarding)

以上兩者方式是對消息轉發的優化,如果你不使用上述兩種方式,則會進入完整的消息轉發流程。這會創建一個NSInvocation對象來完全包含發送的消息,其中包括target,selector,所有的參數,返回值。

在runtime構建NSInvocation之前首先需要一個NSMethodSignature,所以它通過-methodSignatureForSelector:方法請求。一旦NSInvocation創建完成,runtime就會調用forwardInvocation:方法,在此方法內你可以使用參數中的invocation做任何事情。無限可能…
舉個例子,如果你想對一個NSArray中的所有對象調用同一個方法,而又不想一直寫循環代碼時,想直接操作NSArray時,可這樣處理:

@implementation NSArray (ForwardingIteration)

    - (NSMethodSignature *)methodSignatureForSelector:(SEL)sel
    {
        NSMethodSignature *sig = [super methodSignatureForSelector:sel];
        if(!sig)
        {
            for(id obj in self)
                if((sig = [obj methodSignatureForSelector:sel]))
                    break;
        }
        return sig;
    }

    - (void)forwardInvocation:(NSInvocation *)inv
    {
        for(id obj in self)
            [inv invokeWithTarget:obj];
    }

    @end

然後就可以這樣使用

[(NSWindow *)windowsArray setHidesOnDeactivate:YES];

不過不建議這樣使用,因爲若NSArray實現了此方法,就不會進入轉發流程。實現這種功能的一種比較好的方法是使用NSProxy。

五.方法聲明

雖然上述機制可以轉發當前類中沒有實現的方法,但發送消息時仍然需要知道每個消息的方法簽名,否則就會有編譯器告警。可以通過category來聲明轉發消息的方法。

六.使用消息轉發在子類中處理Delegate消息

當繼承一個具有delgate的類,而又需要在子類中處理某些delegate消息,而又不影響對正常Delegate消息的調用時,需要如何處理呢?
一種方法是將子類對象設爲自身的delegate,而將外部設置的delegate存儲到另一個參數中。在子類中實現所有的delegate方法,處理子類中需要處理的delegate消息,而將子類中不處理的delegate消息再發送到外部delegate。這種方法的缺點在於實現繁瑣,在子類中需要實現所有delegate方法,儘管大部分delegate消息又直接轉給了外部delegate處理。
另一種比較優雅的方式是使用消息轉發,創建一個proxy類,將proxy類設置爲父類的delegate,在proxy中分別將消息轉發給子類或外部Delegate。
比如,創建一個UISCrollView的子類可使用如下代碼
MessageInterceptor.h

@interface MessageInterceptor : NSObject {
    id receiver;
    id middleMan;
}
@property (nonatomic, assign) id receiver;
@property (nonatomic, assign) id middleMan;
@end

MessageInterceptor.m

@implementation MessageInterceptor
@synthesize receiver;
@synthesize middleMan;

- (id)forwardingTargetForSelector:(SEL)aSelector {
    if ([middleMan respondsToSelector:aSelector]) { return middleMan; }
    if ([receiver respondsToSelector:aSelector]) { return receiver; }
    return [super forwardingTargetForSelector:aSelector];
}

- (BOOL)respondsToSelector:(SEL)aSelector {
    if ([middleMan respondsToSelector:aSelector]) { return YES; }
    if ([receiver respondsToSelector:aSelector]) { return YES; }
    return [super respondsToSelector:aSelector];
}

@end

MyScrollView.h

#import "MessageInterceptor.h"

@interface MyScrollView : UIScrollView {
    MessageInterceptor * delegate_interceptor;
    //...
}

//...

@end

MyScrollView.m

@implementation MyScrollView

- (id)delegate { return delegate_interceptor.receiver; }

- (void)setDelegate:(id)newDelegate {
    [super setDelegate:nil];
    [delegate_interceptor setReceiver:newDelegate];
    [super setDelegate:(id)delegate_interceptor];
}

- (id)init* {
    //...
    delegate_interceptor = [[MessageInterceptor alloc] init];
    [delegate_interceptor setMiddleMan:self];
    [super setDelegate:(id)delegate_interceptor];
    //...
}

- (void)dealloc {
    //...
    [delegate_interceptor release];
    //...
}

// delegate method override:
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
    // 1. your custom code goes here
    // 2. forward to the delegate as usual
    if ([self.delegate respondsToSelector:@selector(scrollViewDidScroll:)]) {
        [self.delegate scrollViewDidScroll:scrollView];
    }
}

@end

MessageInterceptor對象會自動將將子類中實現的delegate消息轉發給子類,而將其他所有delegate消息轉發給外部設置的delegate對象。

在MessageInterceptor中除了實現forwardingTargetForSelector:方法外,還實現了respondsToSelector:方法,因爲UIScrollView在發送delegate消息之前會首先使用respondsToSelector:判斷delegate是否實現了該方法,而轉發的消息對respondsToSelector:也應返回YES。

參考:
Friday Q&A 2009-03-27: Objective-C Message Forwarding
Objective-C Runtime Programming Guide – Dynamic Method Resolution
Objective-C Runtime Programming Guide – Message Forwarding
Intercept obj-c delegate messages within a subclass
Hacking Block Support Into UIMenuItem
NSObject Class Reference
NSObject Protocol Reference
NSInvocation Class Reference
NSMethodSignature Class Reference

This entry was posted in IOS and tagged message forwardingobjective-C. Bookmark the permalink.

Leave a Reply

Your email address will not be published.

Name

Email

Website

Comment

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>

Protected by WP Anti Spam
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章