0x1、引言
EventBus(事件总线)
,跟之前写的 Handler 一样,老生常谈,教程早已烂大街,面试官偶尔会让你:
官方仓库 greenrobot/EventBus 看下Commit记录,最早可追溯到2012.7,作者大大已经好几年没有大的代码更新了,可能是库已趋于稳定 + 有其他更好的替代品(LiveData、Rx)。
写 把书读薄 | 《设计模式之美》设计模式与范式(行为型-观察者模式) 时,顺带过了一下它的大体源码,了解了初始化、订阅、取消订阅、发送普通事件、发送粘性事件的 大概调用流程和原理。但总感觉并没完全参透,想再仔细地摸索下,遂有此文,记录自己摸索着实现EventBus的过程。在此之前,我们先过一过没有EventBus前是如何处理数据传递问题的。
0x2、套娃 → 最原始的页面数据传递
最原始的页面数据传递
可以把Android页面笼统地分为两类:Activity
和 Fragment
,数据传递还包括 数据回传,常见的三种传递如下:
① 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
:
定义了两个类:ReceiverRecord
和 BroadcastRecord
,看下构造方法和 getInstance()
方法:
跟下注册广播的方法 registerReceiver()
:
有点懵?没事,先看下定义mReceivers和mActions的定义:
接着看下取消广播注册的方法 unregisterReceiver()
:
再看下发送广播的方法 sendBroadcast()
:
就是根据Action信息,生成了一个广播记录列表(Action,接收者列表),然后利用handler发起一个广播类型的空信息。回到构造方法处:
判断是广播类型的信息,走 executePendingBroadcasts()
:
还有一个立即执行的方法:sendBroadcastSync()
以上就是本地广播的实现逻辑,通过这种方式解耦:
- 要传出数据的页面:直接发广播,不用管谁接收;
- 要接收数据的页面,注册广播,收到广播自动执行对应回调;
这种玩法妥妥滴是 观察者模式
,虽然不是常规实现方式。核心是:自行维护广播(被观察者)和接收者(观察者)的集合,配合Handler完成事件分发,可以的~
但!还是不够轻量,数据传递的依赖系统的 BroadcastReceiver
,里面糅合了很多跟我们业务无关的东西,违反了 迪米特原则。
So,基于观察者模式的思想,借鉴 LocalBroadcast,来探索着实现一个更轻量级的广播~