本文是投稿文章,作者:kingizz(博客)
一、OC的消息機制
那麼,怎樣實現delegate方法的動態轉發呢?這需要用到Objective-C的消息機制,我們都知道,在OC中,調用一個對象的方法,實際上是給對象發了一條消息,在編譯Objective-C函數調用的語法時,會被翻譯成一個C的函數調用:objc_msgSend(),例如:
[array insertObject:foo atIndex:2]; //會被翻譯成: objc_msgSend(array, @selector(insertObject:atIndex), foo, 2);
那麼,objc_msgSend又做了哪些事呢?,以[object foo]為例:
通過object的isa指針找到它的class
在class的method_list中找到foo
如果class中沒找到foo,則繼續往他的superclass中查找
一旦找到foo這個函數,就去執行對應的方法實現(IMP)
如果一直沒有找到foo,OC的runtime將繼續下面的步驟:
二、動態方法決議與消息轉發
在Objective-C中,如果向一個對象發送一條該對象無法處理的消息(對應selector不存在),會導致程序crash, 但是,在crash之前,oc的運行時系統會先經過以下兩個步驟:
Dynamic Method Resolution
Message Forwarding
1、Dynamic Method Resolution(動態方法決議)
首先,如果調用的方法是實例方法,OC的運行時會調用- (BOOL)resolveInstanceMethod:(SEL)sel,如果是類方法,則會調用+ (BOOL)resolveClassMethod:(SEL)sel 讓我們可以在程序運行時動態的為一個selector提供實現,如果我們添加了函數的實現,並返回YES,運行時系統會重啟一次消息的發送過程,調用動態添加的方法。例如,下面的例子:
+ (BOOL)resolveInstanceMethod:(SEL)sel{ if (sel == @selector(foo)) { class_addMethod([self class], sel, (IMP)dynamicMethodIMP, "V@:"); return YES; } return [super resolveInstanceMethod:sel]; } void dynamicMethodIMP(id self, SEL _cmd){ NSLog(@"%s", __PRETTY_FUNCTION__); }
class_addMethod 方法動態的添加新的方法與對應的實現,如果調用了[Foo foo],將會轉到動態添加的dynamicMethodIMP 方法中。Objective-C的方法本質上是一個至少包含兩個參數(id self, SEL _cmd)的C函數,這樣,當重啟消息發送時,就能在類中找到@selector(foo)了。而如果方法返回NO時,將會進入下一步:消息轉發(Message Forwarding)
2、Message Forwarding(消息轉發)
消息轉發分為兩步:
首先運行時系統會調用- (id)forwardingTargetForSelector:(SEL)aSelector方法,如果這個方法中返回的不是nil或者self,運行時系統將把消息發送給返回的那個對象
如果- (id)forwardingTargetForSelector:(SEL)aSelector返回的是nil或者self,運行時系統首先會調用- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector方法來獲得方法簽名,方法簽名記錄了方法的參數和返回值的信息,如果-methodSignatureForSelector 返回的是nil, 運行時系統會拋出unrecognized selector exception,程序到這裡就結束了。
整個流程可以用下面這張圖表示
三、實現多重代理
結合上面的流程分析,我麼可以發現,要實現多重代理的分發,我們需要讓Runtime系統運行到forwardInvocation這一步,並在該方法中將delegate方法分發到其他各個對象中去:
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{ NSMethodSignature *signature = [super methodSignatureForSelector:aSelector]; if (!signature) { for (id target in self.allDelegates) { if ((signature = [target methodSignatureForSelector:aSelector])) { break; } } } return signature; } - (void)forwardInvocation:(NSInvocation *)anInvocation{ for (id target in self.allDelegates) { if ([target respondsToSelector:anInvocation.selector]) { [anInvocation invokeWithTarget:target]; } } }
由於我們調用delegate的方法時,一般會先調用[delegate responseToSelector]方法,所以,我們還需要實現這個方法:
- (BOOL)respondsToSelector:(SEL)aSelector{ if ([super respondsToSelector:aSelector]) { return YES; } for (id target in self.allDelegates) { if ([target respondsToSelector:aSelector]) { return YES; } } return NO; } @end
然後我們來測試一下,新建一個ScrollDelegate類,實現UIScrollViewDelegate方法:
#import "ScrollDelegate.h" @implementation ScrollDelegate - (void)scrollViewDidScroll:(UIScrollView *)scrollView{ NSLog(@"%s", __PRETTY_FUNCTION__); } @end
然後再新建一個ViewController,也實現UIScrollViewDelegate方法,添加一個UIScrollView在controller的view中,然後設置scrollView的delegate為multipleProxy:
#import "ViewController.h" #import "MultipleDelegate.h" #import "ScrollDelegate.h" @interface ViewController ()@property (weak, nonatomic) IBOutlet UIScrollView *scrollView; @end @implementation ViewController{ MultipleDelegate *_multipleDelegate; } - (void)viewDidLoad { [super viewDidLoad]; self.scrollView.contentSize = CGSizeMake(self.view.bounds.size.width, self.view.bounds.size.height * 2); _multipleDelegate = [MultipleDelegate new]; //添加要處理delegate方法的對象 NSArray *array = @[self, [ScrollDelegate new]]; _multipleDelegate.allDelegates = array; self.scrollView.delegate = (id)_multipleDelegate; } - (void)scrollViewDidScroll:(UIScrollView *)scrollView{ NSLog(@"%s", __PRETTY_FUNCTION__); } @end
運行,滑動scrollView,看看控制台打印的信息:
2015-11-01 11:07:49.199 MultipleDelegateDemo[4732:498520] -[ViewController scrollViewDidScroll:] 2015-11-01 11:07:49.200 MultipleDelegateDemo[4732:498520] -[ScrollDelegate scrollViewDidScroll:] 2015-11-01 11:07:49.227 MultipleDelegateDemo[4732:498520] -[ViewController scrollViewDidScroll:] 2015-11-01 11:07:49.227 MultipleDelegateDemo[4732:498520] -[ScrollDelegate scrollViewDidScroll:]
很好,deegate方法已經被正確地轉發給了兩個對象了,看起來好像沒什麼不對,可是,細心的你一定會發現,這裡存在retain cycle:controller -> _multipleDelegate -> controller,那麼怎樣解決這個問題呢?
四、NSPointerArray防止循環引用
因為NSArray會對對象進行retain操作,導致循環引用的產生,所以我們可以用NSPointerArray來解決這個問題,但是需要注意對於其他的delegate對象也需要在controller中對其強引用, 最終MultipleDelegateProxy的實現:
#import "KIZMultipleDelegateProxy.h" @interface KIZMultipleDelegateProxy () @property (nonatomic, strong) NSPointerArray *weakRefTargets; @end @implementation KIZMultipleDelegateProxy - (void)setDelegateTargets:(NSArray *)delegateTargets{ self.weakRefTargets = [NSPointerArray weakObjectsPointerArray]; for (id delegate in delegateTargets) { [self.weakRefTargets addPointer:(__bridge void *)delegate]; } } - (BOOL)respondsToSelector:(SEL)aSelector{ if ([super respondsToSelector:aSelector]) { return YES; } for (id target in self.weakRefTargets) { if ([target respondsToSelector:aSelector]) { return YES; } } return NO; } - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{ NSMethodSignature *sig = [super methodSignatureForSelector:aSelector]; if (!sig) { for (id target in self.weakRefTargets) { if ((sig = [target methodSignatureForSelector:aSelector])) { break; } } } return sig; } //轉發方法調用給所有delegate - (void)forwardInvocation:(NSInvocation *)anInvocation{ for (id target in self.weakRefTargets) { if ([target respondsToSelector:anInvocation.selector]) { [anInvocation invokeWithTarget:target]; } } } @end
五、小記
利用這個多重代理動態轉發,我封裝了一些獨立的delegate實現的小功能,比如本文開頭提到的TableView頭部圖片拉伸效果,放在github上:https://github.com/zziking/KIZBehavior