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

目录
打赏
0
1
1
0
7
分享
相关文章
Android之SQLite数据库使用详解
Android之SQLite数据库使用详解
使用 MongoTemplate 对 MongoDB 进行 CRUD
使用 MongoTemplate 对 MongoDB 进行 CRUD
177 0
一次性完整学完搭建PWA项目
一次性完整学完搭建PWA项目
411 0
反编译app方法
如果你没有代码,那么可以反编译该app。 这里将用到2个工具,分别是dex2jar和jd-gui。你可以在这里下载目前为止的最新版本以及示例apk。 我们以工具包里的ContactManager.apk为例,简单介绍一下反编译的流程。
1184 0
如何在Java中使用Socket编程实现TCP连接?
在Java中,通过Socket编程实现TCP连接非常常见。以下演示了基本的TCP通信流程,可根据具体需求进行扩展。
367 0
Glide显示不出图片,监听报javax.net.ssl.SSLHandshakeException: java.security.cert.CertPathValidatorException:
Glide框架是当前比较流行的图片加载框架,使用起来也很简单,肯定有人在使用的时候加载不出图片的,情况有多种,下面讲一下加载不出来捕获到的Exception:javax.net.ssl.SSLHandshakeException: java.security.cert.CertPathValidatorException: 。出现这种情况基本上都是加载https出现的
1001 0
Glide显示不出图片,监听报javax.net.ssl.SSLHandshakeException: java.security.cert.CertPathValidatorException:
Frida - App逆向 JavaScript代码注入 常用语法介绍
Frida可以通过将JavaScript 脚本插入到APP的内存中来对APP的逻辑进行跟踪和监视乃至修改原程序的逻辑,实现逆向开发和分析人员想要实现的功能称之为HOOK(钩子 即通过钩子机制与钩子函数建立联系);
1230 0
Frida - App逆向 JavaScript代码注入 常用语法介绍

热门文章

最新文章