首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Android 框架学习3:从 EventBus 中学到的精华

Android 框架学习3:从 EventBus 中学到的精华

作者头像
张拭心 shixinzhang
发布2018-01-05 17:03:38
6630
发布2018-01-05 17:03:38
举报

关联文章:

学习的目的是为了超越,经过前面对 EventBus 3.0 的学习,我们已经对它相当熟悉了,现在来总结下,从这个框架里我们可以学到些什么。

首先看看 EventBus 解决了什么问题。

EventBus 解决了什么问题

在日常开发中,回调的使用场景非常多,比如按钮的点击事件,网络的请求结果等等,它表示的是对某一将来可能发生事件的监听,具体使用步骤为以下 3 步:

  1. 创建一个回调接口,在接口中定义监听到事件发生时要进行的操作
  2. 需要监听的地方创建一个回调的具体实现,然后传递给事件触发者
  3. 事件触发者持有回调接口的引用,在事件发生时,调用回调接口的具体实现

非常简单的 3 步就实现了对未来事件的监听。

如果对某一个事件有多个监听,就需要在事件触发者里创建监听者列表,然后在事件发生时挨个通知注册过的监听者。这就是“观察者模式”。

观察者模式又称“发布-订阅模式”,它用于一个被观察者持有多个观察者对象的引用,当被观察者状态发生改变时,通知所有观察者进行更新。是一种一对多的依赖关系。

不熟悉的同学欢迎查看我的 观察者模式 : 一支穿云箭,千军万马来相见 了解观察者模式。

在观察者模式中,订阅者需要实现同样的接口,也就是只能监听同样的事件。如果想要监听不同的事件就需要创建不同的接口,在事件多了以后难免有些繁琐。

最好有一种方法,订阅者实现一个接口就可以监听不同事件,哦不,干脆不实现接口,只创建事件发生时要进行的操作就好了。

EventBus 所代表的思想,就是一种解决方案。

EventBus 的思想

这里写图片描述
这里写图片描述

在 EventBus 中,我们无需实现接口,只要在订阅者中创建监听不同事件的方法,然后使用注解标识。

EventBus 会在编译时和运行时(取决于你是否添加索引)通过处理注解和反射的方式拿到订阅方法和所在的类,然后将订阅者、订阅方法、订阅的事件分别保存在两个属性中。

在有发送者发送事件时,EventBus 根据事件去前面保存的属性里找到订阅者和订阅方法,然后以反射的方式调用它。

EventBus 的编译时注解

源码分析 EventBus 3.0 如何实现事件总线 中我们了解到,在编译时 EventBus 的注解处理器会读取注解,然后生成索引文件。

之前 EventBus 是纯反射,经常有人说性能差什么的,这下好了,编译时注解可以加快很多查找效率。

除了 ButterKnife 使用编译时注解生成重复代码外,EventBus 对编译时注解的使用为我们提供了新的思路:

  • 将运行时需要进行的查找工作转移到编译时
  • 使用哈希表保存查找到的信息
  • 生成的类实现约定的接口,便于运行时调用

关于编译时注解如何使用可以查看这篇: 使用编译时注解简单实现类似 ButterKnife 的效果

EventBus 用到的设计模式

EventBus 作为比较成熟的框架,还是使用了很多设计模式的,这里复习一下。

①单例模式

static volatile EventBus defaultInstance;
public static EventBus getDefault() {
    if (defaultInstance == null) {
        synchronized (EventBus.class) {
            if (defaultInstance == null) {
                defaultInstance = new EventBus();
            }
        }
    }
    return defaultInstance;
}

经典的 double-check 单例,使用 volatile 修饰避免重排序,完美。

②Builder 模式

public EventBus() {
    this(DEFAULT_BUILDER);
}

EventBus(EventBusBuilder builder) {
    subscriptionsByEventType = new HashMap<>();
    typesBySubscriber = new HashMap<>();
    stickyEvents = new ConcurrentHashMap<>();
    mainThreadPoster = new HandlerPoster(this, Looper.getMainLooper(), 10);
    backgroundPoster = new BackgroundPoster(this);
    asyncPoster = new AsyncPoster(this);
    indexCount = builder.subscriberInfoIndexes != null ? builder.subscriberInfoIndexes.size() : 0;
    subscriberMethodFinder = new SubscriberMethodFinder(builder.subscriberInfoIndexes,
            builder.strictMethodVerification, builder.ignoreGeneratedIndex);
    logSubscriberExceptions = builder.logSubscriberExceptions;
    logNoSubscriberMessages = builder.logNoSubscriberMessages;
    sendSubscriberExceptionEvent = builder.sendSubscriberExceptionEvent;
    sendNoSubscriberEvent = builder.sendNoSubscriberEvent;
    throwSubscriberException = builder.throwSubscriberException;
    eventInheritance = builder.eventInheritance;
    executorService = builder.executorService;
}

类中的字段太多时,每次都调用 setXXX() 太累了,不如提供一个 Builder,提供些默认值,剩下的就让用户自己设置吧。

③外观模式

EventBus 将不同事件的保存、分发封装在内部,向外部提供了简单的注册、发送、解除注册方法。

把散落在多处的重复操作收到一个入口,这不就是外观模式么。

④策略模式

private final HandlerPoster mainThreadPoster;
private final BackgroundPoster backgroundPoster;
private final AsyncPoster asyncPoster;

EventBus 的几种消息 Poster 虽然没有严格的符合策略模式,但思想其实是一样的。都是完成相同的任务,只不过具体实现方式不同。

其实完全可以制定一个统一 Poster 接口,方法为 enqueue(),然后 EventBus 持有 Poster 的三个引用,每个引用的实现不同。

⑤观察者模式

这个就不用多说了,EventBus 事件订阅、发布的过程就是观察者模式的改进版。

想了解设计模式的文章,可以查看: 设计模式专栏

值得学习的细节

如果现在让我去写一个 EventBus ,恐怕是难以胜任的。

即使整体思想(事件注册、收集、发送、解除注册)有了,分成不同的模块,具体怎么实现,想想就头疼哦。

来看看 EventBus 有什么值得学习的细节:

  • 提供默认 Builder 和默认实例
  • 选用合适的线程池
    • Executors.newCachedThreadPool() 适合并发执行大量短期的小任务
  • 使用 ThreadLocal 实现事件队列的线程无关性
  • 并发控制
    • 数据有可能在操作的同时添加,使用 CopyOnWriteArrayListsynchronized
  • 职责分离
    • 查找类中的订阅方法有专门的类 SubscriberMethodFinder
    • 保存类与其中的订阅方法关系有专门的类 Subscription
    • 不同线程发送事件也有专门的类
  • 使用 map 缓存一些可能会重复使用的信息
    • 事件与其的父类和接口的映射 Map<Class<?>, List<Class<?>>> eventTypesCache = new HashMap<>()
    • 事件与对应的订阅者关联列表 Map<Class<?>, CopyOnWriteArrayList<Subscription>> subscriptionsByEventType
    • 订阅者与订阅的事件关联列表 Map<Object, List<Class<?>>> typesBySubscriber
    • 保存粘性事件 Map<Class<?>, Object> stickyEvents
  • 如果需要创建大量相同类型的对象,考虑使用对象池,对象使用完毕回收,只要把引用其他对象置为 null 即可
    • FindState[] FIND_STATE_POOL = new FindState[POOL_SIZE]
    • List<PendingPost> pendingPostPool = new ArrayList<PendingPost>()
  • 将重复传递的一些参数封到一个对象里传递
    • PostingThreadState
  • 创建了自己的事件队列 PendingPostQueue
    • 双向链表
    • 生产者-消费者模型, 出队 wait - 入队 nofityAll

不足之处

①EventBus 虽然提供了编译时生成的索引,使用起来却不方便,不能直接给 EventBus.getDefault() 添加索引,必须自己创建一个 EventBusBuilder 实例,然后才能添加:

EventBus mInstance = EventBus.builder()
        .addIndex(new MyEventBusIndex())
        .build();

②使用 EventBus 进行解耦其实是把双刃剑,大量的业务逻辑散布在各处,有点类似 C 语言里的 goto 的弊端:

  1. 在程序比较简单时是比较灵活,但是当程序比较复杂时很容易造成程序流程的混乱
  2. 非编写本人,其他人看程序不那么容易理解
  3. 调试程序的过程也会变得很困难。

摘自:https://zhidao.baidu.com/question/142824323.html

③Poster 没有创建基类/接口

其实也不是什么大问题,就是感觉创建统一接口会更优美啊 0.0

感悟

EventBus 还是挺优秀的框架,主要是它出现的比较早,思想比较优秀。

在写这篇文章之前我去总结了几篇 EventBus 源码中设计的知识点,比如线程池、阻塞队列、编译时注解。

基础很重要啊,基础不好怎么可能写出这样的代码呢?!还是要再努力点才行啊。

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2017-07-06 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • EventBus 解决了什么问题
  • EventBus 的思想
  • EventBus 的编译时注解
  • EventBus 用到的设计模式
  • 值得学习的细节
  • 不足之处
  • 感悟
相关产品与服务
数据库
云数据库为企业提供了完善的关系型数据库、非关系型数据库、分析型数据库和数据库生态工具。您可以通过产品选择和组合搭建,轻松实现高可靠、高可用性、高性能等数据库需求。云数据库服务也可大幅减少您的运维工作量,更专注于业务发展,让企业一站式享受数据上云及分布式架构的技术红利!
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档