美文网首页iOS开发攻城狮的集散地iOS面试知识点iOS开发之常用技术点
老司机出品———疯狂造轮子之事件总线的设计思路

老司机出品———疯狂造轮子之事件总线的设计思路

作者: 老司机Wicky | 来源:发表于2018-10-23 21:29 被阅读16次
    事件总线的设计思路

    随着公司业务不断地迭代,数据层和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或者给我留言。

    相关参考资料:

    实现一个优雅的iOS消息总线

    相关文章

      网友评论

      • HiKdn:请教下,我们什么时候才需要去用这个订阅呢?是不是在一对多的情况下才使用。。一对一的时候尽可能不用是不是比较好?
        老司机Wicky:@HiKdn 我是这样想的,EventBus可以解决各业务模块、功能模块之间的耦合。与EventBus的引用关系无法避免,但之前可以让对ABC等多个模块的引用降低成为EventBus的一个引用。
        HiKdn:@老司机Wicky 不好意思,容我钻个牛角尖。那么分层是为了解耦的话,那么各层都引入这个订阅中心,算不算也是一种耦合?多谢您的解答
        老司机Wicky:我们遇到的场景是,我们UI层,功能层,中间层,逻辑层,数据层嵌套层级较深。每次事件要逐级上抛,过于复杂。想在两端的层级直接建立关系,相互之间又不想建立引用关系。就可以使用这种方案。

      本文标题:老司机出品———疯狂造轮子之事件总线的设计思路

      本文链接:https://www.haomeiwen.com/subject/wwlzzftx.html