Objective-C 消息转发 与NSObject中forwardInvocation消息重定向

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




NSObject是大多数系统 api的基类,今天打开头文件看了一下,原来它还有很多现在都还不了解的地方。今天简单介绍一下它的forwardInvocation功能。


在obj-c中我们可以向一个实例发送消息,相当于c/c++ java中的方法调用,只不过在这儿是说发送消息,实例收到消息后会进行一些处理。比如我们想调用一个方法,便向这个实例发送一个消息,实例收到消息后,如果能respondsToSelector,那么就会调用相应的方法。如果不能respond一般情况下会crash。今天要的,就是不让它crash。


首先说一下向一个实例发送一个消息后,系统是处理的流程:

1. 发送消息如:[self startwork] 

2. 系统会check是否能response这个消息

3. 如果能response则调用相应方法,不能则抛出异常


在第二步中,系统是如何check实例是否能response消息呢?如果实例本身就有相应的response,那么就会相应之,如果没有系统就会发出methodSignatureForSelector消息,寻问它这个消息是否有效?有效就返回对应的方法地址之类的,无效则返回nil。如果是nil就会crash, 如果不是nil接着发送forwardInvocation消息。

所以我们在重写methodSignatureForSelector的时候就人工让其返回有效实例。  文字说不清,还是用代码说明

我们定义了这样一个类

  1. @interface TargetProxy : NSProxy {  
  2.     id realObject1;  
  3.     id realObject2;  
  4. }  
  5.    
  6. - (id)initWithTarget1:(id)t1 target2:(id)t2;  
  7.    
  8. @end  

实现:
  1. @implementation TargetProxy  
  2.    
  3. - (id)initWithTarget1:(id)t1 target2:(id)t2 {  
  4.     realObject1 = [t1 retain];  
  5.     realObject2 = [t2 retain];  
  6.     return self;  
  7. }  
  8.    
  9. - (void)dealloc {  
  10.     [realObject1 release];  
  11.     [realObject2 release];  
  12.     [super dealloc];  
  13. }  
  14.    
  15. // The compiler knows the types at the call site but unfortunately doesn't  
  16. // leave them around for us to use, so we must poke around and find the types  
  17. // so that the invocation can be initialized from the stack frame.  
  18.    
  19. // Here, we ask the two real objects, realObject1 first, for their method  
  20. // signatures, since we'll be forwarding the message to one or the other  
  21. // of them in -forwardInvocation:.  If realObject1 returns a non-nil  
  22. // method signature, we use that, so in effect it has priority.  
  23. - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {  
  24.     NSMethodSignature *sig;  
  25.     sig = [realObject1 methodSignatureForSelector:aSelector];  
  26.     if (sig) return sig;  
  27.     sig = [realObject2 methodSignatureForSelector:aSelector];  
  28.     return sig;  
  29. }  
  30.    
  31. // Invoke the invocation on whichever real object had a signature for it.  
  32. - (void)forwardInvocation:(NSInvocation *)invocation {  
  33.     id target = [realObject1 methodSignatureForSelector:[invocation selector]] ? realObject1 : realObject2;  
  34.     [invocation invokeWithTarget:target];  
  35. }  
  36.    
  37. // Override some of NSProxy's implementations to forward them...  
  38. - (BOOL)respondsToSelector:(SEL)aSelector {  
  39.     if ([realObject1 respondsToSelector:aSelector]) return YES;  
  40.     if ([realObject2 respondsToSelector:aSelector]) return YES;  
  41.     return NO;  
  42. }  
  43.    
  44. @end  

现在我们还用这个类,注意向它发送的消息:
  1. // Create a proxy to wrap the real objects.  This is rather  
  2.     // artificial for the purposes of this example -- you'd rarely  
  3.     // have a single proxy covering two objects.  But it is possible.  
  4.     id proxy = [[TargetProxy alloc] initWithTarget1:string target2:array];  
  5.    
  6.     // Note that we can't use appendFormat:, because vararg methods  
  7.     // cannot be forwarded!  
  8.     [proxy appendString:@"This "];  
  9.     [proxy appendString:@"is "];  
  10.     [proxy addObject:string];  
  11.     [proxy appendString:@"a "];  
  12.     [proxy appendString:@"test!"];  
  13.    
  14.     NSLog(@"count should be 1, it is: %d", [proxy count]);  
  15.       
  16.     if ([[proxy objectAtIndex:0] isEqualToString:@"This is a test!"]) {  
  17.         NSLog(@"Appending successful.");  
  18.     } else {  
  19.         NSLog(@"Appending failed, got: '%@'", proxy);  
  20.     }  
运行的结果是:

count should be 1, it is:  1

Appending successful.

TargetProxy声明中是没有appendString与addObject消息的,在这儿却可以正常发送,不crash,原因就是发送消息的时候,如果原本类没有这个消息响应的时候,转向询问methodSignatureForSelector,接着在forwardInvocation将消息重定向。 上面也说了多参数的消息是不能重定向的。这我还没测过。



reference:

https://developer.apple.com/library/mac/#documentation/Cocoa/Conceptual/ObjCRuntimeGuide/Articles/ocrtForwarding.html#//apple_ref/doc/uid/TP40008048-CH105-SW2

http://developer.apple.com/library/mac/#samplecode/ForwardInvocation/Listings/main_m.html#//apple_ref/doc/uid/DTS40008833-main_m-DontLinkElementID_4

http://cocoawithlove.com/2008/03/construct-nsinvocation-for-any-message.html
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值