换个姿势,更好地参透EventBus(上)

本文涉及的产品
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
云解析 DNS,旗舰版 1个月
全局流量管理 GTM,标准版 1个月
简介: EventBus(事件总线),跟之前写的 Handler 一样,老生常谈,教程早已烂大街

0x1、引言


EventBus(事件总线),跟之前写的 Handler 一样,老生常谈,教程早已烂大街,面试官偶尔会让你:


网络异常,图片无法展示
|


官方仓库 greenrobot/EventBus 看下Commit记录,最早可追溯到2012.7,作者大大已经好几年没有大的代码更新了,可能是库已趋于稳定 + 有其他更好的替代品(LiveData、Rx)。


把书读薄 | 《设计模式之美》设计模式与范式(行为型-观察者模式) 时,顺带过了一下它的大体源码,了解了初始化、订阅、取消订阅、发送普通事件、发送粘性事件的  大概调用流程和原理。但总感觉并没完全参透,想再仔细地摸索下,遂有此文,记录自己摸索着实现EventBus的过程。在此之前,我们先过一过没有EventBus前是如何处理数据传递问题的。


0x2、套娃 → 最原始的页面数据传递


最原始的页面数据传递

可以把Android页面笼统地分为两类:ActivityFragment,数据传递还包括 数据回传,常见的三种传递如下:


① Activity <=> Activity


简单场景:


/* ====== 单向传递 ====== */
// OriginActivity 给 TargetActivity传递数据
val intent = Intent(this, TargetActivity::class.java)
intent.putExtra("info", "Some Data")
startActivity(intent)
// TargetActivity 解析 OriginActivity 传递过来的数据
val data = getIntent().getStringExtra("info")
/* ====== 数据回传 ====== */
// 传递Intent实例的同时,传递请求码
startActivityForResult(intent, 0x123)
// OriginActivity重写此方法,在此解析回传数据,TargetActivity销毁时会回调此方法
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
    super.onActivityResult(requestCode, resultCode, data)
    if (requestCode == 0x123) {
        val backData = intent?.getStringExtra("back_info")
    }
}
// TargetActivity 在finish()前调用此方法设置回传数据:
setResult(0x123, Intent().putExtra("back_info", "Some Back Data"))
finish()


来点复杂点的场景,如这样的页面打开顺序:A → B → C → D,然后有下面这样的需求:


  • ① 在A里执行了一个耗时操作,执行完后,通知D做页面刷新:


解法:D采用singleTask模式,A要传参直接 startActivity(intent),D重写 onNewIntent(intent) 对传递过来的数据进行解析。


  • ② D回传数据给A


直接回传给A,A得用 startActivityForResult(intent),但用了它会导致 singleTask启动模式 失效,创建多一个新的D,只能用③里的套娃解法;


如果是D只能有一个的场景,就有些尴尬了...在说说另一个常见的场景:注册填写资料流程


  • ③ 假设A是登录页,BCD是填写资料页,D填完后点击完成,关闭DCB,同时把注册信息传递给A


解法:startActivityForResult(intent) 套娃,D把信息传给C,C传给B,B再传给A


  • ④ 再来个奇葩场景,D先传信息给B,让其 finish(),然后再调起一个新的B,此时的Activity栈:A → C → D → B


闲着无聊的童鞋可以向下怎么搞?真实的故事,不要怀疑产品经理的脑洞!接着到Activity和Fragment的数据传递。


② Activity <=> Fragment


// Fragment未添加到FragmentManger前
TargetFragment targetFragment = TargetFragment()
targetFragment.setArguments(Intent())
supportFragmentManager.beginTransaction().add(targetFragment, "TargetFragment").commit()
// 已添加到FragmentManager中
// ① 接口回调,Fragment实现接口,Activity调用对应回调传入数据;
// ② Fragment定义公共方法,Activity直接通过Fragment实例调用方法传入;
// 数据回传Activity
// ① 接口回调,Activity实现接口,Fragment调用对应回调回传;
// ② Activity定义公共方法,Fragment获取getActivity()获取Activity实例,类型强转,调用此方法传入;


上述是Fragment与 宿主Activity 的数据传递,与非宿主Activity的数据传递,则是在宿主Activity与非宿主Activity上再套一层,这样明显暴露了耦合的问题。而且 宿主Activity的任务太重了 ,既要处理Activity间的数据传递问题,又要处理子Fragment的数据传递问题。


③ Fragment <=> Fragment


同宿主Activity


  • 目标Fragment中定义设置数据的方法,发起Fragment调用 getActivity().getSupportFragmentManager().findFragmentByTag() + 强转 获取目标Fragment实例,然后调用设置数据的方法;


  • 接口回调 → 定义接口,Activity实现接口(定义更新目标Fragment的方法),传入发起Fragment,传入Fragment要传数据时调用此方法;


不同宿主Activity


发起Fragment → 宿主Activity → 目标宿主Activity → 目标Fragment


不难看出,使用上述这种数据传递机制,页面间的耦合太严重了,而且如果子Fragment有多个,或者一堆子Fragment嵌套,Activity摇身一变 超大类,谁顶得住啊,所以得想写法子来解耦。


0x3、临时应付 → 数据暂存 + 生命周期回调


即使用内存或硬盘,暂存要传递的数据,然后在Activity、Fragment对应的生命周期回调中去读取。简单示例如下:


// 数据暂存类
object DataTempUtils {
    private val tempDataSet = hashMapOf<String, Any>()
    fun getTempDataByKey(key: String) = tempDataSet[key]
    fun updateTempData(key: String, any: Any) {
        this.tempDataSet[key] = any
    }
}
// 解析回传数据的页面
override fun onResume() {
    super.onResume()
    val backData = DataTempUtils.getTempDataByKey("${this.javaClass.name}")
    Log.e("Test", backData.toString())
}
// 传递回传数据的页面
DataTempUtils.updateTempData("com.example.test.IndexActivity", "Some Back Data")


相比起原始的数据传递方式,简单了一些,要传的时候就写入,要读的时候就在生命周期函数回调里读,但存在下述问题:


  • key的问题:怎么生成一个唯一标识?谁负责管理?页面自己、还是另外写一个工具类?


  • 可能做了一些无效操作:每次都在生命周期回调处主动拉取,无论数据是否有更新都要执行;


  • 引入了额外的处理逻辑:比如要在onResume中判断是否第一次进入,数据有效性得判断等;


  • 可能会引入一些奇怪的BUG:比如有人在别的地方更新了此数据,但你不知道,导致拿到的数据一直错误等;


0x4、前车之鉴 → 本地广播


数据缓存的方法不太稳健,试试另一个方案 → Broadcast(广播) , Android四大组件之一,可用作进程内部通信,也可用于进程内部某些组件间的信息/数据传递。可以用,但直接用的话,太重了!怎么说?看下发起一个广播,经历的内部流程:


  • ① sendBroadcast 发起广播
  • ② 将广播信息告知system_server
  • ③ system_server查找到对应receivers
  • ④ 广播进入分发队列等待分发
  • ⑤ 调用App进程receiver的onReceiver()回调


就自己的APP内部使用,得经历两次Binder Call,而且发送的广播别人也能收到(存在被劫持风险),甚至可以伪造广播欺骗我们的Receiver。当然,可以通过配置权限进行规避:


  • 发送时:指定接受者必须具备的permission,或intent.setPackage()设置仅对某个程序生效;
  • 接收时:动态注册的指定发送者必须具备的permission,静态注册的设置android:exported="false";


针对上述问题,Android v4包引入了轻量级的本地广播 → LocalBroadcast,用法很简单:在Activity、Fragment中 动态注册 需要监听的广播并绑定,当发送了广播时,注册了此类型广播的接收者,就会回调对应的onReceiver()方法,还要注意页面销毁时取消广播注册!使用代码示例如下:


// ============ 动态注册广播 ============ 
// 实例化广播接收者实例(此处用匿名内部类偷懒,不然还得自己定义一个广播接收者类)
private var mReceiver = object : BroadcastReceiver() {
    override fun onReceive(context: Context?, intent: Intent?) {
        Log.e("Test", "收到返回数据")
    }
}
// 注册广播
LocalBroadcastManager.getInstance(this).registerReceiver(mReceiver, IntentFilter("index_action"))
// 销毁时要取消注册广播,防止内存泄漏
override fun onDestroy() {
    super.onDestroy()
    LocalBroadcastManager.getInstance(this).unregisterReceiver(mReceiver)
}
// ============ 发送广播 ============
LocalBroadcastManager.getInstance(this).sendBroadcast(Intent("index_action"))
// 注:androidx里,使用本地广播要另外添加下述依赖,不然会提示找不到类:
// implementation 'androidx.localbroadcastmanager:localbroadcastmanager:1.0.0'


调用方法简单,进一步解耦了,观摩下源码,了解具体的实现原理,先跟 LocalBroadcastManager:


网络异常,图片无法展示
|


定义了两个类:ReceiverRecordBroadcastRecord,看下构造方法和 getInstance() 方法:


网络异常,图片无法展示
|


跟下注册广播的方法 registerReceiver()


网络异常,图片无法展示
|


有点懵?没事,先看下定义mReceivers和mActions的定义:


网络异常,图片无法展示
|


接着看下取消广播注册的方法 unregisterReceiver()


网络异常,图片无法展示
|


再看下发送广播的方法 sendBroadcast()


网络异常,图片无法展示
|


就是根据Action信息,生成了一个广播记录列表(Action,接收者列表),然后利用handler发起一个广播类型的空信息。回到构造方法处:


网络异常,图片无法展示
|


判断是广播类型的信息,走 executePendingBroadcasts()


网络异常,图片无法展示
|


还有一个立即执行的方法:sendBroadcastSync()


网络异常,图片无法展示
|


以上就是本地广播的实现逻辑,通过这种方式解耦:


  • 要传出数据的页面:直接发广播,不用管谁接收;
  • 要接收数据的页面,注册广播,收到广播自动执行对应回调;


这种玩法妥妥滴是 观察者模式,虽然不是常规实现方式。核心是:自行维护广播(被观察者)和接收者(观察者)的集合,配合Handler完成事件分发,可以的~


但!还是不够轻量,数据传递的依赖系统的 BroadcastReceiver,里面糅合了很多跟我们业务无关的东西,违反了 迪米特原则


So,基于观察者模式的思想,借鉴 LocalBroadcast,来探索着实现一个更轻量级的广播~

相关文章
|
Java 程序员 开发者
只用一行代码,你能玩出什么花样?
只用一行代码,你能玩出什么花样?
100 1
|
设计模式 安全 Java
别再写满屏的爆爆爆炸类了,试试装饰器模式,这才是优雅的方式!!(2)
别再写满屏的爆爆爆炸类了,试试装饰器模式,这才是优雅的方式!!(2)
146 0
别再写满屏的爆爆爆炸类了,试试装饰器模式,这才是优雅的方式!!(2)
|
设计模式
别再写满屏的爆爆爆炸类了,试试装饰器模式,这才是优雅的方式!!(1)
别再写满屏的爆爆爆炸类了,试试装饰器模式,这才是优雅的方式!!(1)
160 0
别再写满屏的爆爆爆炸类了,试试装饰器模式,这才是优雅的方式!!(1)
|
JavaScript
每日一题:解释下什么是事件代理?应用场景?
每日一题:解释下什么是事件代理?应用场景?
131 0
|
JavaScript
手写代码:实现一个EventBus
EventBus,事件总线。总线一词来自于《计算机组成原理》中的”系统总线“,是指用于连接多个部件的信息传输线,各部件共享的传输介质。我们通常把事件总线也成为自定义事件,一般包含`on`、`once`、`emit`、`off`等方法。在Vue2中想要实现EventBus比较简单,直接暴露出一个`new Vue()`实例即可,以此为思路,我们应该如何自定义实现EventBus呢?
530 0
手写代码:实现一个EventBus
|
安全 Java API
换个姿势,更好地参透EventBus(下)
EventBus(事件总线),跟之前写的 Handler 一样,老生常谈,教程早已烂大街
129 0
|
设计模式 存储 Java
换个姿势,更好地参透EventBus(中)
EventBus(事件总线),跟之前写的 Handler 一样,老生常谈,教程早已烂大街
111 0
|
程序员 Android开发
牛逼!终于有人能把Android事件分发机制讲明白了
在Android开发中,事件分发机制是一块Android比较重要的知识体系,了解并熟悉整套的分发机制有助于更好的分析各种点击滑动失效问题,更好去扩展控件的事件功能和开发自定义控件,同时事件分发机制也是Android面试必问考点之一,如果你能把下面的一些事件分发图当场画出来肯定加分不少。废话不多说,总结一句:事件分发机制很重要。
牛逼!终于有人能把Android事件分发机制讲明白了
|
JavaScript 前端开发
【重温基础】20.事件
【重温基础】20.事件
127 0
|
缓存 移动开发 前端开发
从 SWR 开始 — 一窥现代请求 hooks 设计模型
本文将以 swr 为例子,讲述现在最热门的 useRequest、swr 和 react-query 三个请求 hooks 的新机制,以及新机制后 class Component 和 hooks 在设计上的区别。
从 SWR 开始 — 一窥现代请求 hooks 设计模型