随着公司业务不断地迭代,数据层和UI层不断地下沉,被业务层进行包装,导致数据层想要跟UI层进行通信要经过一层层的带向上抛事件转发给对应的UI层。在重构过程中,我们希望设计一种通信方式,能直接连通数据层和UI层,而又不影响当前的业务层,在本次重构中,我们采取了事件总线的方式来解决这个问题。
事件总线
事件总线是对发布-订阅模式的一种实现。它是一种集中式事件处理机制,允许不同的组件之间进行彼此通信而又不需要相互依赖,达到一种解耦的目的。
需求分析
设计之初,我们简单的分析了一下我们想要的功能:
-
1.我们希望事件支持强弱类型的区分。因为造成此次重构的主要原因即为业务越来越复杂,业务层级随业务复杂度不断提升。我们希望一个事件具有一个强类型来代表某一个业务类型,一个弱类型枚举代表指定业务中某个特定事件。这样不同的业务事件实现了分类管理,逻辑更加清晰且后期维护方便,同时能更大程度上减少强类型事件的存在。
-
2.我们希望当A广播给B一个事件,B处理完事件后,应该存在反馈机制,来告诉A我已将处理完事件了。
-
3.我们希望尽可能的简化对外接口,可以实现随订阅者释放自动移除订阅关系的功能,从而更大程度的减少学习成本和避免野指针奔溃的问题。
基本结构
基于以上需求,老司机实现了一套发布者-订阅者模式的事件总线,基本结构如下:
基础结构基本流程就是订阅者在DWEventBus上进行订阅,订阅一个事件。发布者通过DWEventBus发布一个事件后Bus自动续找对应的订阅者后进行回调。
代码实现
首先我们考虑到既然要实现随Target释放解除订阅关系,我们很自然的想到应该让订阅者Subscriber与Target建立某种关系,使其生命周期与Target相同,并且当Bus发布一个事件后,Subscriber应该做出响应。故Bus同时应于Subscriber建立持有关系。
此处我们考虑到不同总线间应该相对独立,互不干扰,所以我设计成Target对每一个Bus维护一个Subscriber。当订阅消息时,Bus持有Target对应自己的Subscriber。基本代码如下:
+(instancetype)subscriberWithTaget:(id)target bus:(DWEventBus *)bus {
DWEventSubscriber * sub = objc_getAssociatedObject(target, [bus.uid UTF8String]);
if (!sub) {
sub = [DWEventSubscriber new];
sub.target = target;
sub.bus = bus;
sub.proxy = [DWEventProxy proxyWithTarget:sub];
objc_setAssociatedObject(target, [bus.uid UTF8String], sub, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
return sub;
}
-(void)registEvent:(__kindof DWEvent *)event onSubscriber:(DWEventSubscriber *)sub entity:(DWEventEntity *)entity {
///在bus上注册
NSMutableSet * eventSet = self.subscribersMap[event.eventName];
if (!eventSet) {
eventSet = [NSMutableSet set];
[self.subscribersMap setValue:eventSet forKey:event.eventName];
}
[eventSet addObject:sub.proxy];
///Something else...
}
与Target进行关联是想让Subscriber的生命周期与相同。故Bus持有Subscriber不能直接进行强持有,添加Proxy代理层
作为转发。
当Subscriber随着Target释放时,我们应该移除Bus上Subscriber对应的订阅。我们可以通过Subscriber自身对应的所有主事件类型去移除Bus中对应主类型Subscriber集合中的自身。
///移除bus中所有包含此sub的项
-(void)disposeHanlder {
[self.eventsMap enumerateKeysAndObjectsUsingBlock:^(NSString * key, id _Nonnull obj, BOOL * _Nonnull stop) {
NSMutableSet * subs = [self.bus.subscribersMap valueForKey:key];
[subs removeObject:self.proxy];
}];
}
-(void)dealloc {
[self disposeHanlder];
}
至此我们解决了自动移除订阅关系,但是对应的事件分发还没有完成。当消息分发给Subscriber后,应由Subscriber根据事件的强弱类型进行分发。故注册订阅时,在Subscriber上同时应该注册详细的事件关系。
-(void)registEvent:(__kindof DWEvent *)event onSubscriber:(DWEventSubscriber *)sub entity:(DWEventEntity *)entity {
///Do something else...
///在subscriber上注册
///取出一级map
NSMutableDictionary * subTypeMD = [sub.eventsMap valueForKey:event.eventName];
if (!subTypeMD) {
subTypeMD = [NSMutableDictionary dictionary];
[sub.eventsMap setValue:subTypeMD forKey:event.eventName];
}
///取出二级set
NSMutableSet * entitys = [subTypeMD valueForKey:@(event.subType).stringValue];
if (!entitys) {
entitys = [NSMutableSet set];
[subTypeMD setValue:entitys forKey:@(event.subType).stringValue];
}
///添加观察者
[entitys addObject:entity];
}
这样,一个entity负责处理一个事件订阅关系,当Bus发送事件后,Subscriber按照事件类型转发给对应的entity即可。
-(void)receiveEvent:(__kindof DWEvent *)event {
dispatch_semaphore_wait(self.sema, DISPATCH_TIME_FOREVER);
NSDictionary * subType = [self.eventsMap valueForKey:event.eventName];
NSSet * entitys = [subType valueForKey:@(event.subType).stringValue];
[entitys enumerateObjectsUsingBlock:^(DWEventEntity * _Nonnull obj, BOOL * _Nonnull stop) {
[obj receiveEvent:event target:self.target];
}];
dispatch_semaphore_signal(self.sema);
}
至此我们就完成了事件的订阅和发布。事件移除只要按照对应的层级关系移除Bus及Subscriber上对应的entity就好,此处不做赘述。
基于需求我们大致粗略的完成了一个事件总线,借助他我们就可以完成代码结构建的解耦及消息互通。其实需求分析过后,思路都是很顺其自然的。大家多想多思考就都可以分析得到。
DWEventBus
DWEventBus即是本次重构我设计的一个事件总线。他大概具备以下功能:
- 发布-订阅模式
- 联合事件
- 指定发布和订阅回调所在队列
- 订阅方执行完毕的反馈
具体可以去我的GitHub看一下,如果使用过程中有什么问题大家可以随时给我提Issue或者给我留言。
相关参考资料:
网友评论