Objective-C 消息轉發
一.消息轉發流程
當向Objective-C對象發送一個消息,但runtime在當前類及父類中找不到此selector對應的方法時,消息轉發(message forwarding)流程開始啓動。
-
動態方法解析(Dynamic Method Resolution或Lazy method resolution)
向當前類(Class)發送resolveInstanceMethod:(對於類方法則爲resolveClassMethod:)消息,如果返回YES,則系統認爲請求的方法已經加入到了,則會重新發送消息。 -
快速轉發路徑(Fast forwarding path)
若果當前target實現了forwardingTargetForSelector:方法,則調用此方法。如果此方法返回除nil和self的其他對象,則向返回對象重新發送消息。 -
慢速轉發路徑(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