换个姿势,更好地参透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,来探索着实现一个更轻量级的广播~

相关文章
|
2月前
|
Android开发 开发者 Kotlin
告别AsyncTask:一招教你用Kotlin协程重构Android应用,流畅度飙升的秘密武器
【9月更文挑战第13天】随着Android应用复杂度的增加,有效管理异步任务成为关键。Kotlin协程提供了一种优雅的并发操作处理方式,使异步编程更简单直观。本文通过具体示例介绍如何使用Kotlin协程优化Android应用性能,包括网络数据加载和UI更新。首先需在`build.gradle`中添加coroutines依赖。接着,通过定义挂起函数执行网络请求,并在`ViewModel`中使用`viewModelScope`启动协程,结合`Dispatchers.Main`更新UI,避免内存泄漏。使用协程不仅简化代码,还提升了程序健壮性。
76 1
|
3月前
|
开发者 C# Android开发
明白吗?Xamarin与Native的终极对决:究竟哪种开发方式更适合您的项目需求,让我们一探究竟!
【8月更文挑战第31天】随着移动应用开发的普及,开发者面临多种技术选择。本文对比了跨平台解决方案Xamarin与原生开发方式的优势与劣势。Xamarin使用C#进行跨平台开发,代码复用率高,可大幅降低开发成本;但因基于抽象层,可能影响性能。原生开发则充分利用平台特性,提供最佳用户体验,但需维护多套代码库,增加工作量。开发者应根据项目需求、团队技能和预算综合考量,选择最适合的开发方式。
110 0
|
6月前
|
存储 Web App开发 运维
发布、部署,傻傻分不清楚?从概念到实际场景,再到工具应用,一篇文章让你彻底搞清楚
部署和发布是软件工程中经常互换使用的两个术语,甚至感觉是等价的。然而,它们是不同的! • 部署是将软件从一个受控环境转移到另一个受控环境,它的目的是将软件从开发状态转化为生产状态,使得软件可以为用户提供服务。 • 发布是将软件推向用户的过程,应用程序需要多次更新、安全补丁和代码更改,跨平台和环境部署需要对版本进行适当的管理,有一定的计划性和管控因素。
1480 1
|
编解码 前端开发 JavaScript
这次终于搞清楚移动端开发了(一)
这次终于搞清楚移动端开发了(一)
|
Web App开发 JavaScript 前端开发
这次终于搞清楚移动端开发了(三)
这次终于搞清楚移动端开发了(三)
|
编解码 前端开发 UED
这次终于搞清楚移动端开发了(二)
这次终于搞清楚移动端开发了(二)
|
移动开发 开发框架 前端开发
一文搞懂ReactNative生命周期的进化
一文搞懂ReactNative生命周期的进化
598 0
一文搞懂ReactNative生命周期的进化
|
XML 架构师 Java
一文把Java反射说的明明白白,清清楚楚,记得点赞关注,距离架构师的小目标又进一步
今天有时间没加班回家来好好写一篇文章,反射是Java里比较高级的概念了,一般在书的后半部分。反射也是写框架的必备技能,反射很重要,现在仍然记得刚毕业的一两年一直没有搞懂反射是什么。今天就讲讲反射,希望这篇文章能帮有同样疑惑的你解开疑团,废话不多说,让我们开始吧。
188 0
一文把Java反射说的明明白白,清清楚楚,记得点赞关注,距离架构师的小目标又进一步
|
安全 Java API
换个姿势,更好地参透EventBus(下)
EventBus(事件总线),跟之前写的 Handler 一样,老生常谈,教程早已烂大街
122 0
|
设计模式 存储 Java
换个姿势,更好地参透EventBus(中)
EventBus(事件总线),跟之前写的 Handler 一样,老生常谈,教程早已烂大街
109 0