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

简介: 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,来探索着实现一个更轻量级的广播~

相关文章
|
域名解析 负载均衡 网络协议
【计算机网络】什么是区域传输?
【1月更文挑战第10天】【计算机网络】什么是区域传输?
|
Kubernetes 数据可视化 Docker
Kubeadm方式搭建k8s集群全流程-20230207
Kubeadm方式搭建k8s集群全流程-20230207
231 0
|
传感器 数据采集 供应链
港口智能化,我们这样做!
港口智能化,我们这样做!
554 0
港口智能化,我们这样做!
|
前端开发 JavaScript API
netty系列之:使用netty搭建websocket客户端
netty系列之:使用netty搭建websocket客户端
|
Android开发
Android Uri转File方法(适配android 10以上版本及android 10以下版本)
Android Uri转File方法(适配android 10以上版本及android 10以下版本)
1036 0
|
NoSQL Linux Redis
在CentOS上安装和配置Redis
在CentOS上安装和配置Redis
2888 3
|
存储 缓存 数据库
Android之SQLite数据库使用详解
Android之SQLite数据库使用详解
1151 0
|
SQL 存储 缓存
maxcompute的特点
【5月更文挑战第5天】maxcompute的特点
308 6
|
机器学习/深度学习 人工智能 自然语言处理
AI生产范式
【5月更文挑战第7天】AI生产范式
645 4
|
存储 缓存 定位技术
游戏服务器缓存系统如何设计
游戏服务器缓存系统如何设计