框架设计模式-⑤多代理模式

[toc]

前言

1、什么是多代理

引文①、环信SDK

用过环信SDK的同学应该对多代理不陌生了,请看下面代码:

1
2
3
4
5
6
7
8
9
/*
@method
@brief 注册一个监听对象到监听列表中
@discussion 把监听对象添加到监听列表中准备接收相应的事件
@param delegate 需要注册的监听对象
@param queue 通知监听对象时的线程
@result
*/
- (void)addDelegate:(id<EMChatManagerDelegate>)delegate delegateQueue:(dispatch_queue_t)queue;

平时我们写得比较多的代理: @property (nonatomic,weak) id<EMChatManagerDelegate>delegate; 写了上面属性后系统会默认生成set方法: - (void)setDelegate:(id<EMChatManagerDelegate>)delegate;通过对两个接口的比较就不难看出:单代理只能设置一个,而多代理可以设置多个,准确来说应该是多代理可以添加多个

以上摘自:iOS 实现多代理的方法及实例代码

引文②、XMPP

XMPP以及类似IM框架里通常会有这种需求:打开多个聊天窗口,和多个人聊天。然鹅框架底层消息转发管理器却只有一个。通常是这两个窗口都要收到消息回调,然后取自己有用的消息。。。
大概就这么个意思,我两年前用了下,具体也解释不清楚,欢迎指点交流,反正就是有一个需求需要多代理回调,这种IM框架通常有这种方法
[xmppRoom addDelegate:self delegateQueue:dispatch_get_main_queue()];

以上摘自:iOS 多代理的实现

引文③、类似IM库,当接受到消息,在几个不同地方做回调,比如改变消息数,显示小红点等

参考文章:iOS实现多重代理及应用场景

2、为什么不用NSNotificationCenter

系统不是已经有通知中心NSNotificationCenter了吗?为什么还要自己实现一个呢?

举个例子,现在我们有一个模块需要抛一个通知出来,通知其它模块用户名改变了,我们来看代码大致是怎么写的

1
2
3
4
5
6
7
8
9
10
11
12
13
// 发通知一方
NSString *const kUserNameChangedNotification = @"UserNameChangedNotification";

NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter];
[notificationCenter postNotificationName:UserNameChangedNotification object:nil
userInfo:@{@"oldName":@"zhangsan","newName":"lisi"}];

// 接收通知的一方可以是
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(UserNameChanged1:)
name:kUserNameChangedNotification object:nil];
// 也可以是
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(UserNameChanged2:)
name:kUserNameChangedNotification object:nil];

从例子中可以看到有的缺点:
1.对于接收同一个事件的通知,不同的人可能会用不同的方法名来执行(例子中是UserNameChanged1UserNameChanged1),无法统一。
2.对于多参数支持不方便。

所以本文我们的优化的是统一接收方的执行方法,并为该方法提供明确的参数。

CJProtocolCenter

CJProtocolCenter.m

项目背景举例:

背景描述如下:

vc中所做的某个操作(指一个操作),想要发送给多个人,让他们接收到信息后,自己处理。

我们假设接收者为

1
2
3
4
DelegateReceivedViewModel1 *delegateReceiver1 = [[DelegateReceivedViewModel1 alloc] init];
DelegateReceivedViewModel2 *delegateReceiver2 = [[DelegateReceivedViewModel2 alloc] init];
self.delegateReceiver1 = delegateReceiver1;
self.delegateReceiver2 = delegateReceiver2;

一、正常操作

发送状态变化的信息如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// OneToManyDelegateNormalViewController1.h
#import "CJUIKitBaseHomeViewController.h"
#import "TSDelegate.h"

@interface OneToManyDelegateNormalViewController1 : CJUIKitBaseHomeViewController {

}
@property (nonatomic, weak) id<TSDelegate> delegate1; // 提供给外部设置
@property (nonatomic, weak) id<TSDelegate> delegate2; // 提供给外部设置

@end


// OneToManyDelegateNormalViewController1.m
loginModule.actionBlock = ^{
[self.delegate1 delegate_didUpdateLoginState:YES];
[self.delegate2 delegate_didUpdateLoginState:YES];
}

②设置代理的地方会是如下:

1
2
3
4
OneToManyDelegateNormalViewController1 *viewController = [[OneToManyDelegateNormalViewController1 alloc] init];
viewController.delegate1 = delegateReceiver1;
viewController.delegate2 = delegateReceiver2;
[self.navigationController pushViewController:viewController animated:YES];

二、优化delegate

依赖注入

依赖注入:把做的事情交给某人。至于依赖的某人有什么能力,依靠外部决定。

参考文档:

注入可以通过对象的初始化(或者可以说构造器)或者通过特性(setter),即构造器注入和setter方法注入。

OneToManyDelegateNormalMediator21

1
2
3
4
5
6
7
8
9
10
11
OneToManyDelegateNormalMediator21 *delegateMediator

// 方法1:构造器注入
- (instancetype)initWithDelegate1:(Delegate1 *)delegate1
delegate2:(Delegate2 *)delegate2;
@property (nonatomic, weak, readonly) Delegate1 *delegate1;
@property (nonatomic, weak, readonly) Delegate2 *delegate2;

// 方法2:Setter 注入
@property (nonatomic, weak) id<Delegate1> delegate1;
@property (nonatomic, weak) id<Delegate2> delegate2;

参考的其他文档:

EXTConcreteProtocol 虽然没有直接叫做依赖注入,而是叫做混合协议,但是充分使用了 OC 动态语言的特性,不侵入项目,高度自动化,框架十分轻量,使用非常简单。

1、优化思路一:从vc中抽离delegate到Mediator

为了优化代码,我们从vc中抽离delegate,则此时

①设置代理的地方会是如下:

1
2
3
4
5
6
7
OneToManyDelegateNormalMediator21 *delegateMediator = [[OneToManyDelegateNormalMediator21 alloc] init];
delegateMediator.delegate1 = self.delegateReceiver1;
delegateMediator.delegate2 = self.delegateReceiver2;

OneToManyDelegateNormalViewController21 *viewController = [[OneToManyDelegateNormalViewController21 alloc] init];
viewController.delegateMediator = delegateMediator;
[self.navigationController pushViewController:viewController animated:YES];

②发送状态变化的信息如下:

1
2
3
4
5
6
// OneToManyDelegateNormalViewController21.m
loginModule.actionBlock = ^{
//[self.delegate1 delegate_didUpdateLoginState:YES];
//[self.delegate2 delegate_didUpdateLoginState:YES];
[self.delegateMediator delegate_didUpdateLoginState:YES];
};

③此时delegateMediator中的delegate_didUpdateLoginState:方法如下:

1
2
3
4
5
6
7
8
9
10
#pragma mark - TSDelegate
- (void)delegate_didUpdateLoginState:(BOOL)loginState {
if (self.delegate1 && [self.delegate1 respondsToSelector:@selector(delegate_didUpdateLoginState:)]) {
[self.delegate1 delegate_didUpdateLoginState:YES];
}

if (self.delegate2 && [self.delegate2 respondsToSelector:@selector(delegate_didUpdateLoginState:)]) {
[self.delegate2 delegate_didUpdateLoginState:YES];
}
}

2、优化思路二:在Mediator中管理delegate数组

为了减少设置代理的变量,我们使用delegate数组,则此时

①设置代理的地方会是如下:

1
2
3
4
5
6
7
OneToManyDelegateArrayMediator31 *delegateMediator = [[OneToManyDelegateArrayMediator31 alloc] init];
[delegateMediator addDelegate:self.delegateReceiver1];
[delegateMediator addDelegate:self.delegateReceiver2];

OneToManyDelegateArrayViewController31 *viewController = [[OneToManyDelegateArrayViewController31 alloc] init];
viewController.delegateMediator = delegateMediator;
[self.navigationController pushViewController:viewController animated:YES];

②发送状态变化的信息如下:不变

③此时delegateMediator中的delegate_didUpdateLoginState:方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//  OneToManyDelegateArrayMediator31.m
_delegates = [NSPointerArray weakObjectsPointerArray];

- (void)addDelegate:(id)delegate {
[_delegates addPointer:(__bridge void*)delegate];
}

#pragma mark - TSDelegate
- (void)delegate_didUpdateLoginState:(BOOL)loginState {
for (NSUInteger i = 0; i < self.delegates.count; i++) {
id delegate = (__bridge id)[self.delegates pointerAtIndex:i];
if (delegate && [delegate respondsToSelector:@selector(delegate_didUpdateLoginState:)]) {
[delegate delegate_didUpdateLoginState:YES];
}
}
}

三、优化状态变化的发送方法

1、未优化时候:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// vc.m
loginModule.actionBlock = ^{
if (self.delegateMediator && [self.delegateMediator respondsToSelector:@selector(delegate_didUpdateLoginState:)]) {
[self.delegateMediator delegate_didUpdateLoginState:YES];
}
};

// Mediator.m
#pragma mark - TSDelegate
- (void)delegate_didUpdateLoginState:(BOOL)loginState {
for (NSUInteger i = 0; i < self.delegates.count; i++) {
id delegate = (__bridge id)[self.delegates pointerAtIndex:i];
if (delegate && [delegate respondsToSelector:@selector(delegate_didUpdateLoginState:)]) {
[delegate delegate_didUpdateLoginState:YES];
}
}
}

为了能够在Mediator中减少我们要实现每个代理的每个方法,我们进行如下优化:

2、增加个发送的封装方法,将协议要执行的方法改为参数等传进去

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// vc.m
loginModule.actionBlock = ^{
[self.delegateMediator broadcastProtocol:@protocol(TSDelegate)
selector:@selector(delegate_didUpdateLoginState:)
responder:^(id<TSDelegate> delegateReceiver) {
[delegateReceiver delegate_didUpdateLoginState:YES];
}];
};

// Mediator.m
- (void)broadcastProtocol:(Protocol * _Nonnull)protocol selector:(SEL _Nullable)selector responder:(void (^_Nonnull)(id _Nonnull delegateReceiver))block {
NSAssert(protocol, @"Protocol is nil.");
NSAssert(block, @"Block is nil.");

for (NSUInteger i = 0; i < self.delegates.count; i++) {
id delegateReceiver = (__bridge id)[self.delegates pointerAtIndex:i];
if (!selector || [delegateReceiver respondsToSelector:selector]) {
block(delegateReceiver);
}
}
}

3、如果你不用上述的封装方法,你还可以利用消息转发

对Mediator对象调用delegate_didUpdateLoginState方法时,因为含delegates的Mediator对象并没有实现协议中的方法,如delegate_didUpdateLoginState,所以,我们只能要么补充实现,要么不实现的话,就利用消息转发,将协议中的方法转发到自己delegate链中的对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// vc.m
loginModule.actionBlock = ^{
if (self.delegateMediator && [self.delegateMediator respondsToSelector:@selector(delegate_didUpdateLoginState:)]) {
[self.delegateMediator delegate_didUpdateLoginState:YES];
}
};

// Mediator.m
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
NSMethodSignature *signature = [super methodSignatureForSelector:aSelector];
if (signature) {
return signature;
}
[_delegates addPointer:nil]; //不加上这句的话,直接调用compact,并不能清除 _delegates 数组中的 nil 对象。
[_delegates compact]; //注意 [_delegates compact],这个方法可以帮助你去掉数组里面的野指针,避免你在快速遍历的时候拿到一个指向不存在对象的地址

for (id delegate in _delegates) {
if (!delegate) {
continue;
}
signature = [delegate methodSignatureForSelector:aSelector];
if (signature) {
break;
}
}
return signature;
}

- (void)forwardInvocation:(NSInvocation *)anInvocation {
SEL selector = [anInvocation selector];
BOOL responded = NO;
for (id delegate in _delegates) {
if (delegate && [delegate respondsToSelector:selector]) {
[anInvocation invokeWithTarget:delegate];
responded = YES;
}
}
if (!responded) {
[self doesNotRecognizeSelector:selector];
}
}

此方式的问题:由于[self.delegateMediator delegate_didUpdateLoginState:YES];,所以delegateMediator需要在@interface支持,同样的因为这个支持,我们需要在.m文件中去实现它里面的方法,否则会有警告。除非说协议里的那些方法全部都是设置为了@optional

结论:基于此方式的问题,所以为了减少对其他的约束,我们这里最后采用2的优化方式。

四、发现潜在问题

1、如果一个类同时依赖两个protocal,且刚好两个protocal中有同名方法怎么办?

举例:在vc.m中想要告诉大家去实现协议1中的showMessage方法,但是如果不说明是协议1中的方法,可能有个协议2也有同样的方法,导致执行的方法错了。

分析:这种情况是可能出现的,但是这种情况出现的时候,不会被引用到同一个监听者,不然分不清delegateReceiver中现在执行的是哪一个。

如原本名字不一样,我们区分得开。

1
2
3
4
5
6
7
8
9
10
// viewModel.m
#pragma mark - TSUserDelegate
- (void)userDelegate_didUpdateLoginState:(BOOL)loginState {
NSLog(@"vm收到userDelegate:登录状态发生变化,您已%@", loginState ? @"登录" : @"登出");
}

#pragma mark - TSMessageDelegate
- (void)messageDelegate_didUpdateMessageState:(BOOL)messageState {
NSLog(@"vm收到messageDelegate:信息状态发生变化");
}

我们直接在上述原本实现的基础上看问题,修改后为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// vc.m
loginModule.actionBlock = ^{
[self.delegateMediator broadcastProtocol:@protocol(TSDelegate)
selector:@selector(delegate_didUpdateLoginState:)
responder:^(id<TSDelegate> delegateReceiver) {
[delegateReceiver delegate_didUpdateLoginState:YES];
}];
};

// Mediator.m
- (void)broadcastProtocol:(Protocol * _Nonnull)protocol selector:(SEL _Nullable)selector responder:(void (^_Nonnull)(id _Nonnull delegateReceiver))block {
NSAssert(protocol, @"Protocol is nil.");
NSAssert(block, @"Block is nil.");

for (NSUInteger i = 0; i < self.delegates.count; i++) {
id delegateReceiver = (__bridge id)[self.delegates pointerAtIndex:i];

if (![delegateReceiver conformsToProtocol:protocol]) { // 处理多个protocal中有同名方法,通过传进来的protocol参数,我们在判断接收者有我们想要的方法前,先找到有实现我们想要协议的那个接收者
continue;
}

if (!selector || [delegateReceiver respondsToSelector:selector]) {
block(delegateReceiver);
}
}
}

同理:添加delegate的时候,也

1
2
3
if (![delegateReceiver conformsToProtocol:protocol]) {  // 处理多个protocal中有同名方法,通过传进来的protocol参数,我们在判断接收者有我们想要的方法前,先找到有实现我们想要协议的那个接收者
// continue or return or ...;
}

2、如何保证delegates操作的数据安全

场景:B页面发送一个信号,B页面的viewModel会去执行,当然还有其他地方也有可能执行。

那么当B发送信号出去,还在遍历执行的时候,突然某个从B页面返回A页面,B页面释放,B的viewModel也跟着释放,即这个信号的其中一个接收者B的viewModel被释放了,那么当执行的时候相当于给nil发送信息,不会崩溃。

1
2
3
4
5
6
7
8
9
10
11
for (NSUInteger i = 0; i < self.delegates.count; i++) {
id delegateReceiver = (__bridge id)[self.delegates pointerAtIndex:i];

if (![delegateReceiver conformsToProtocol:protocol]) { // 处理多个protocal中有同名方法,通过传进来的protocol参数,我们在判断接收者有我们想要的方法前,先找到有实现我们想要协议的那个接收者
continue;
}

if (!selector || [delegateReceiver respondsToSelector:selector]) {
block(delegateReceiver);
}
}

需要注意数据安全的地方,应该是避免重复添加和重复删除

所以,我们使用@synchronized来保护。

1
2
3
@synchronized(xxx) {

}

五、delegateMediator升级为中心单例

发送时候的核心代码,大概如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// vc.m
loginModule.actionBlock = ^{
[[TSNotificationCenter defaultCenter] broadcastProtocol:@protocol(TSDelegate)
selector:@selector(delegate_didUpdateLoginState:)
responder:^(id<TSDelegate> delegateReceiver) {
[delegateReceiver delegate_didUpdateLoginState:YES];
}];
};


// 该发送方法在`TSNotificationCenter.m`中的代码如下
- (void)broadcastProtocol:(Protocol * _Nonnull)protocol selector:(SEL _Nullable)selector responder:(void (^_Nonnull)(id _Nonnull delegateReceiver))block {
NSAssert(protocol, @"Protocol is nil.");
NSAssert(block, @"Block is nil.");

for (NSUInteger i = 0; i < self.delegates.count; i++) {
id delegateReceiver = (__bridge id)[self.delegates pointerAtIndex:i];

if (![delegateReceiver conformsToProtocol:protocol]) { // 处理多个protocal中有同名方法,通过传进来的protocol参数,我们在判断接收者有我们想要的方法前,先找到有实现我们想要协议的那个接收者
continue;
}

if (!selector || [delegateReceiver respondsToSelector:selector]) {
block(delegateReceiver);
}
}
}

接收时候的核心代码,大概如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// receiver.m
[[TSNotificationCenter defaultCenter] addDelegate:self.diffDelegateReceiver
forProtocol:@protocol(TSUserDelegate)];
[[TSNotificationCenter defaultCenter] addDelegate:self.diffDelegateReceiver
forProtocol:@protocol(TSMessageDelegate)];

// 该添加方法在`TSNotificationCenter.m`中的代码如下
- (void)addDelegate:(id __nonnull)listener forProtocol:(Protocol * _Nonnull )protocol {
NSAssert(listener, @"listener is nil");
NSAssert(protocol, @"Protocol is nil");
#ifdef DEBUG
NSAssert([listener conformsToProtocol:protocol], @"This listener does not conform to the protocol");
#endif

@synchronized(protocol) {
[self.hashTable addObject:listener];
}
}

说说上面我们应该用什么来管理遵守协议的类(NSHashTable)?

答:因为delegate本身为了避免循环应用,所以其是弱引用对象。而要保存弱引用对象,我们不能够用NSArray,因为NSArray是强引用,而应该用NSHashTable。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 方法1:NSPointerArray
_delegates = [NSPointerArray weakObjectsPointerArray];
- (void)addDelegate:(id)delegate {
[_delegates addPointer:(__bridge void*)delegate];
}

// 方法2:NSHashTable
_hashTable = [NSHashTable weakObjectsHashTable];
- (void)addDelegate:(id)delegate {
[_hashTable addObject:delegate];
}


@property (nonatomic, strong, readonly) NSMutableSet *listenerLists; // 保存多个NSHashTable,即其是数组的数组,里面的元素是 NSHashTable *listeners

这边用了NSHashTable来存储遵守协议的类,NSHashTable和NSSet类似,但又有所不同,总的来说有这几个特点:

NSHashTable中的元素可以通过Hashable协议来判断是否相等.

NSHashTable中的元素如果是弱引用,对象销毁后会被移除,可以避免循环引用.

附一:block 或 delegate 等弱引用对象用什么保存

如果你需要保存一堆block,并且希望它们能够响应特定的事件或操作,你通常会使用数组来存储这些block。然而,由于block在Objective-C中是对象,并且默认情况下它们会被强引用,你需要考虑循环引用的问题,特别是当block内部捕获了它们的捕获环境(例如,捕获了它们的创建者对象的强引用)时。

以下是几种处理block存储的方法:

  1. 使用__weakweak修饰符:如果你的block捕获了它们的创建者对象的引用,你可以在block内部使用__weak(在Objective-C中)或weak(在Swift中)来避免强引用循环。
  2. 使用NSHashTableNSPointerArray:如果你需要存储block的弱引用,可以使用NSHashTableNSPointerArray,并设置它们的pointerFunctions属性以使用NSPointerFunctionsWeakMemory,这样它们就会存储block的弱引用。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@property (nonatomic, strong) NSPointerArray *blocks;

- (void)addBlock:(void (^)(void))block {
if (!self.blocks) {
self.blocks = [NSPointerArray weakObjectsPointerArray];
}
[self.blocks addPointer:(__bridge void *)(block ? block : [NSNull null])];
}

- (void)executeBlocks {
for (NSUInteger i = 0; i < self.blocks.count; i++) {
void *pointer = [self.blocks pointerAtIndex:i];
if (pointer != [NSNull null]) {
void (^block)(void) = (__bridge void (^)(void))pointer;
block();
}
}
}

附:不常见集合NSHashTable和NSMapTable

不常见集合NSHashTable和NSMapTable

5框架设计模式-⑦组件化.md

END

其他参考文章:

iOS 实现多代理的方法及实例代码