OC 如何实现多代理模式
为什么要使用多代理模式
标题虽然是如何实现多代理模式,但是知道为什么需要实现多代理模式同样重要。
众所周知,OC的常用的消息传递方式有很多种,各有各的好处,在不同的场景选择不同实现方式。如:
-
代理 1对1,高耦合
-
通知 1对多,松耦合
-
block
-
KVO
...
不同的实现方式有不同的应用场景,也有各自的优缺点。普通的代理模式只能应用与1对1的场景,针对1对多的场景只能被迫选择使用通知。
但是通知也有自己的缺点:
-
在编译期间不会检查通知是否能够被观察者正确的处理;
-
在释放通知的观察者时,需要在通知中心移除观察者;
-
在调试的时候,通知传递的过程很难控制和跟踪;
-
发送通知和接收通知时需要提前知道通知名称,如果通知名称不一致,会出现不同步的情况;
-
通知发出后,不能从观察者获得任何的反馈信息。对于需要返回值的场景没有办法处理。
如果代理模式能支持多个响应对象,那么就不会再有以上的问题。
如何实现多代理模式
单代理模式
一个最普通的代理模式如下:
代理协议ReportDelegate:
@protocol ReportDelegate <NSObject>
//上交报告
- (void)report;
@end
类ComandA,发送命令者
#import "ReportDelegate.h"
@interface ComandA : NSObject
@property (weak, nonatomic) id <ReportDelegate> delegate;
/**
发送上交报告的命令
*/
- (void)sendOrder;
@end
@implementation ComandA
- (void)sendOrder
{
if(self.delegate && [self.delegate respondsToSelector:@selector(report)])
{
[self.delegate report];
}
}
@end
类ExecutorB,执行命令者
#import "ReportDelegate.h"
@interface ExecutorB : NSObject <ReportDelegate>
@end
@implementation ExecutorB
- (void)report
{
NSLog(@"我要上交报告");
}
@end
现在一个ComandA
对象A可以命令一个ExecutorB
对象B上交报告。因为ComandA
只定义了一个单成员@property (weak, nonatomic) id <ReportDelegate> delegate;
。
最初的多代理模式
如果现在将delegate
变为id <ReportDelegate> delegate
的数组delegates
。在sendOrder
方法中遍历delegates
数组去调用每个delegate
执行代理方法不就好了。类似下面:
@interface ComandA : NSObject
@property (strong, nonatomic) NSPointerArray *delegates;
/**
发送上交报告的命令
*/
- (void)sendOrder;
@end
@implementation ComandA2
- (void)sendOrder
{
for (NSUInteger i = 0; i < self.delegates.count; i += 1) {
id delegate = (__bridge id)[self.delegates pointerAtIndex:i];
if(delegate && [delegate respondsToSelector:@selector(report)])
{
[delegate report];
}
}
}
@end
多代理就这样实现了,现在一个ComandA
对象A可以命令多个ExecutorB
对象B上交报告,只要提前将多个ExecutorB
对象加入到delegates
数组中即可。 之所以选择NSPointerArray
,是因为NSPointerArray
不增加成员的引用计数,相当于弱引用,在释放一个delegate
前,就算不将其从delegates
数组中移除也不会有问题。
一切看起来非常完美,对的,只是看起来非常完美。再深入的思考或实践一下,就会发现这个方式运用起来多么麻烦,哪怕更多的优化也不可避免。有兴趣的可以下载这个。
pod 'MultiDelegateOC', '0.0.1'
代理协议中的每个方法都要主动遍历调用每个代理对象。我们自己新建的类还好,如果我们需要将第三方库的类变为多代理,想想那么多的代理方法需要改动。倘若第三方库的类新增了部分代理方法,我们也要相应的添加。
如果不想修改第三方库的代码,怎么办,难道要在外面再封装一层吗?想想以后的维护工作就让人头疼。
进阶的多代理
于是寻找进阶的多代理方式已不得不做,幸好万能的github有很多大牛,我们只需要站在他们的肩膀上就好了。
最初的多代理模式之所以有上述的问题,是因为我们让ComandA
直接管理delegates
数组,这样必然会对原有代码进行改动。
如果我们新建一个类MultiDelegateOC
代替ComandA
管理delegates
数组,只需要将ComandA
的@property (weak, nonatomic) id <ReportDelegate> delegate
设置为MultiDelegateOC
对象不就好了。
这样原来的ComandA
不需要任何改动就能继续使用多代理了。我们只需要在MultiDelegateOC
内部实现遍历调用就好了。
如果我们理解OC的方法执行——消息转发机制就很容易实现了。我们只需要截获MultiDelegateOC
的方法执行,将其变为遍历执行就可以了。
这里需要重写NSObject
的两个方法methodSignatureForSelector:
和forwardInvocation:
。
methodSignatureForSelector:
原型:
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
这个函数让重载方有机会抛出一个函数的签名,再由后面的forwardInvocation:去执行。如果当前类没有实现这个函数导致返回值为nil,程序就会crash--未实现的函数。
forwardInvocation:
原型:
- (void)forwardInvocation:(NSInvocation *)anInvocation
函数的真正执行者,在这个方法中,我们可以从NSInvocation对象中截获selector,参数,可以设置selector的调用者,真正的遍历delegates
数组去执行就完全没有问题了。
//重写respondsToSelector方法,让`ComandA`类真实判断。
- (BOOL)respondsToSelector:(SEL)selector
{
if ([super respondsToSelector:selector])
{
return YES;
}
for (id delegate in self.delegates)
{
if (delegate && [delegate respondsToSelector:selector])
{
return YES;
}
}
return NO;
}
//防止崩溃,生成函数签名
- (NSMethodSignature *)methodSignatureForSelector:(SEL)selector {
NSMethodSignature* signature = [super methodSignatureForSelector:selector];
if (signature)
{
return signature;
}
[self.delegates compact];
if (self.silentWhenEmpty && self.delegates.count == 0)
{
// return any method signature, it doesn't really matter
return [self methodSignatureForSelector:@selector(description)];
}
for (id delegate in self.delegates)
{
if (!delegate)
{
continue;
}
signature = [delegate methodSignatureForSelector:selector];
if (signature)
{
break;
}
}
return signature;
}
//遍历`delegates`数组调用代理方法
- (void)forwardInvocation:(NSInvocation *)invocation
{
SEL selector = [invocation selector];
BOOL responded = NO;
NSArray *copiedDelegates = [self.delegates copy];
for (id delegate in copiedDelegates)
{
if (delegate && [delegate respondsToSelector:selector])
{
[invocation invokeWithTarget:delegate];
responded = YES;
}
}
if (!responded && !self.silentWhenEmpty)
{
[self doesNotRecognizeSelector:selector];
}
}
一个进阶版的多代理模式就完成,现在我们只需要主动生成一个MultiDelegateOC
对象管理多代理就可以了。
有兴趣的可以下载这个。
pod 'MultiDelegateOC', '0.0.2'
不过现在还不是很完美,如果代理协议中有返回值的情况,我们并没有处理。再给- (void)forwardInvocation:
方法添点料就好了:
- (void)forwardInvocation:(NSInvocation *)invocation
{
SEL selector = [invocation selector];
BOOL responded = NO;
NSArray *copiedDelegates = [self.delegates copy];
void *returnValue = NULL;
for (id delegate in copiedDelegates)
{
if (delegate && [delegate respondsToSelector:selector])
{
[invocation invokeWithTarget:delegate];
if(invocation.methodSignature.methodReturnLength != 0)
{
void *value = nil;
[invocation getReturnValue:&value];
if(value)
{
returnValue = value;
}
}
responded = YES;
}
}
if(returnValue)
{
[invocation setReturnValue:&returnValue];
}
if (!responded && !self.silentWhenEmpty)
{
[self doesNotRecognizeSelector:selector];
}
}
如果多个代理对象都有返回值,最终返回将是最后加入的代理的返回值。当然NSPointerArray可以调整成员的顺序,你也可以自己设置判断条件来选择返回值。
总结
以上是我自己在实现多代理模式的历程,很多方法都是使用网络上大神的成熟经验,在不断的使用实践踩坑中,逐步完善出来。
包括最初的多代理模式,我也使用了很长时间,后来实在觉得太麻烦。逼不得已从网上找到第二种方式,觉得挺好用。
后来测试发现总会出现莫名其妙的异常,一时之间找不到原因,不得已又切换到了第一种方式。
后来闲的时候灵光一闪,发现第二种方式有异常的情况都是在代理方法有返回值的情况下出现。知道问题原因,解决起来就简单多了。
现在我一直使用第二种代理模式,至今没有出现过问题。
多代理模式我一般是与单例配合使用。使用多代理的地方还不少,比如高德地图SDK。
Demo地址
Pod引用
pod 'MultiDelegateOC'
网友评论