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,来探索着实现一个更轻量级的广播~
0x5、见招拆招 → 手写一个更轻量级的广播
① 观察者模式常规写法 → 先写个雏形
不了解观察者模式的可以看:把书读薄 | 《设计模式之美》设计模式与范式(行为型-观察者模式),直接开敲:
// 传递数据类
data class Entity(
val key: String,
var value: Any
)
// 更新回调接口
interface IUpdate {
fun updateData(entity: Entity)
}
// 观察者抽象类
abstract class Observer: IUpdate
// 被观察者
object Subject {
private val observerList = arrayListOf<Observer>()
fun register(observer: Observer) {
this.observerList.add(observer)
}
fun unregister(observer: Observer) {
this.observerList.remove(observer)
}
fun postMessage(entity: Entity) {
observerList.forEach { it.updateData(entity) }
}
}
简单如斯,接着可以写个简单的实例验证下,A → B → C → D,D发送消息,ABC接收消息(页面一样~):
class ATestActivity : AppCompatActivity() {
// 观察者回调
val mObserver: Observer = object : Observer() {
override fun updateData(entity: Entity) {
tv_content.text = entity.value.toString()
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_index)
tv_title.text = "页面A"
bt_test.setOnClickListener {
startActivity(Intent(this, BTestActivity::class.java))
}
// 注册事件
Subject.register(mObserver)
}
override fun onDestroy() {
super.onDestroy()
// 取消事件注册
Subject.unregister(mObserver)
}
}
class DTestActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_index)
bt_test.setOnClickListener {
// 发送事件
Subject.postMessage(Entity("back_data", "页面D的返回数据~"))
finish()
}
}
}
运行效果如下:
D发送了广播,然后ABC都收到,且进行了页面更新,可以,雏形有了,开始着手优化。
② 只关注想关注的广播 → 数据结构优化
被观察者Subject的postMessage()直接对列表里的所有观察者进行了遍历,有点过于粗暴了,毕竟 观察者不一定要关注被观察者的所有行为
,以这个为切入点,引入key,同时优化下存储的数据结构:
object Subject {
private val observerMap = hashMapOf<String, ArrayList<Observer>>()
fun register(key: String, observer: Observer) {
val observerList = observerMap[key]
if (observerList.isNullOrEmpty()) {
observerMap[key] = arrayListOf()
}
observerMap[key]!!.add(observer)
}
fun unregister(key: String, observer: Observer) {
if (observerMap[key].isNullOrEmpty()) return
observerMap[key]!!.remove(observer)
}
fun postMessage(key: String, entity: Entity) {
if (observerMap[key].isNullOrEmpty()) return
observerMap[key]!!.forEach { it.updateData(entity) }
}
}
通过不同的key对订阅者进行区分,减少了无效遍历,但是也带来了一个问题,注册、解注册、发送广播都要传多一个key。
Subject.register("back_data", mObserver)
Subject.unregister("back_data", mObserver)
Subject.postMessage("back_data", Entity("back_data", "页面D的返回数据~"))
嗯,传两个参数看着不是很优雅,发送广播可以把Key整到Entity里,注册和解注册可以搞到Observer类中,改动代码如下:
// 抽象观察者
abstract class Observer: IUpdate {
abstract val key: String
}
// 观察者回调
val mObserver: Observer = object : Observer() {
override val key = "back_data"
override fun updateData(entity: Entity) {
tv_content.text = entity.value.toString()
}
}
// 被观察者
object Subject {
private val observerMap = hashMapOf<String, ArrayList<Observer>>()
fun register(observer: Observer) {
val observerList = observerMap[observer.key]
if (observerList.isNullOrEmpty()) {
observerMap[observer.key] = arrayListOf()
}
observerMap[observer.key]!!.add(observer)
}
fun unregister(observer: Observer) {
if (observerMap[observer.key].isNullOrEmpty()) return
observerMap[observer.key]!!.remove(observer)
}
fun postMessage(entity: Entity) {
if (observerMap[entity.key].isNullOrEmpty()) return
observerMap[entity.key]!!.forEach { it.updateData(entity) }
}
}
③ FBI WARNING:警惕内存泄漏的风险
上面的代码,看上去好像没啥问题,是吧?但...真的没问题吗?
上面我们偷懒用的匿名内部类,它存在这样的问题:
匿名内部类会持有外部类的引用,此处的外部类是Activity,如果忘记解绑(移除集合),会导致onDestory()后,Subject中的集合依旧持有Activity引用。当Subject遍历执行到此回调时,BOOM!内存泄漏就来了~~
验证方法很简单,build.gradle依赖下LeakCanary:
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.7'
然后页面故意漏掉某个Observer的解绑,然后此页面finish()掉后,在另一个页面发起一个事件,尝试几次后会发现:
所以,切记解绑 !!!一种比较无脑的解绑方式(笨,但是稳健~):
在页面基类里定义一个集合,把每个Observer实例都加入其中,在onDestory()中遍历取消注册
示例如下:
protected val mObserverList = arrayListOf<Observer>()
// 直接将观察者加入列表
mObserverList.add(object : Observer() {
override val key = "other_data"
override fun updateData(entity: Entity) {
tv_content.text = entity.value.toString()
}
})
// 遍历列表注册
mObserverList.forEach { Subject.register(it) }
override fun onDestroy() {
super.onDestroy()
// 遍历取消事件注册
mObserverList.forEach { Subject.unregister(it) }
}
④ 谁是卧底 → 谁才是真正的观察者
知道要规避内存泄漏风险后,继续优化,在使用过程中不难发现这样的问题:
一个观察者可能对观察者的多种行为进行观察,行为有多少种,就要实例化多少个Observer
em...好像有点不对劲,这TM是把行为作为了观察者啊,观察者应该包裹各种行为的回调,明显 页面才是观察者
,简单,页面直接实现IUpdate接口,重写更新数据的方法。
class ATestActivity : AppCompatActivity(), IUpdate {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_index)
tv_title.text = "页面A"
bt_test.setOnClickListener {
startActivity(Intent(this, BTestActivity::class.java))
}
Subject.register(this)
}
override fun updateData(entity: Entity) {
when(entity.key) {
"back_data" -> tv_content.text = entity.value.toString()
}
}
override fun onDestroy() {
super.onDestroy()
Subject.unregister(this)
}
}
// 变回原样的被观察者
object Subject {
private val observerList = arrayListOf<IUpdate>()
fun register(observer: IUpdate) {
observerList.add(observer)
}
fun unregister(observer: IUpdate) {
observerList.remove(observer)
}
fun postMessage(entity: Entity) {
observerList.forEach { it.updateData(entity) }
}
}
可以是可以,但postMessage()又变回之前的无脑遍历状态了,因为页面传这个Key有点麻烦,毕竟实现的IUpdate接口。如果在页面中定义额外的key属性,在Subject里拿到观察者还得做下类型强转拿Key。
简单点说:被观察者还得知道观察者具体的类型,这TM的又耦合了...
这个问题先放一放,等下会解决,这里思考另一个问题:
既然暂时没法有脑遍历了,那广播Entity里的Key还有必要吗?
没有,直接定义不同的广播类型,观察者直接判断类型执行对应操作就好了,改动后的代码:
interface IUpdate {
fun updateData(any: Any)
}
object Subject {
private val observerList = arrayListOf<IUpdate>()
fun register(observer: IUpdate) {
observerList.add(observer)
}
fun unregister(observer: IUpdate) {
observerList.remove(observer)
}
fun postMessage(entity: Any) {
observerList.forEach { it.updateData(entity) }
}
}
// 传递数据
data class DataEntity(var data: String)
// 刷新页面
object RefreshEntity
// 测试页面
class ATestActivity : AppCompatActivity(), IUpdate {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_index)
tv_title.text = "页面A"
bt_test.setOnClickListener {
startActivity(Intent(this, BTestActivity::class.java))
}
Subject.register(this)
}
// 对应不同的广播执行不同的处理
override fun updateData(any: Any) {
when (any) {
is DataEntity -> tv_content.text = any.data
is RefreshEntity -> Toast.makeText(this, "收到更新广播", Toast.LENGTH_SHORT).show()
}
}
override fun onDestroy() {
super.onDestroy()
Subject.unregister(this)
}
}
class BTestActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_index)
tv_title.text = "页面B"
bt_test.setOnClickListener {
Subject.postMessage(DataEntity("回传数据"))
Subject.postMessage(RefreshEntity)
finish()
}
}
}
看到这里,有用过EventBus的童鞋,肯定会哔一句:这TM不就是EventBus吗?
像,但还不是,你用EventBus,Activity和Fragment需要实现接口吗?所以接下来想办法让观察者不用实现接口。
⑤ 巧用反射 → 少写一个接口
假设我们约定成俗,只要类中定义了 updateData(any: Any) 方法,我们就把它当做观察者的回调方法。
所以要做的就是获取观察者类所有的方法,遍历匹配 方法名和参数个数,符合的方法就是回调方法,用 反射
实现一波。修改后的被观察者:
object Subject {
private val observerMap = hashMapOf<Any, Method?>()
fun register(any: Any) {
var method: Method? = null
try {
// 反射获得此方法
method = any.javaClass.getDeclaredMethod("updateData", Any::class.java)
observerMap[any] = method
} catch (e: NoSuchMethodException) {
e.printStackTrace()
}
}
fun unregister(any: Any) {
observerMap[any] = null
observerMap.remove(any)
}
fun postMessage(entity: Any) {
observerMap.forEach { (key, value) ->
value?.invoke(key, entity)
}
}
}
把观察者类:实现IUpdate接口和updateData(any: Any)前override标识干掉,运行验证一波,效果一致。
⑥ 巧用运行时注解 → 规避方法名写错
上面通过反射省去了实现一个接口,但也带来了隐患,方法不能自动生成,只能手敲或复制粘贴,就有可能出现函数名拼写错误的问题,毕竟人是容易犯错的。
能不能显式地告诉编译器:这函数就是观察者的回调方法,你不用理他叫啥?
还真可以,利用 注解
就可以实现,有些朋友可能对注解有些陌生(没用过),没关系,简单过一波~
Java里都见过 @Override 吧,在重写方法时要加上此注解,否则编译器直接爆红,无法编译。这里的@Override就是注解,用于告知编译器正在重写一个方法,这样,当父类方法被删除或修改时,编译器会提示错误信息。
注解可用来修饰类、方法、参数等,有下述三种使用场景:
- 编译器提示信息:给编译器用来发现错误,或清除不必要的警告;
- 编译时生成代码:利用编译器外的工具(如kapt)根据注解信息自动生成代码;
- 运行时处理:在运行时根据注解,通过反射获得具体信息,然后做一些操作;
限于篇幅就不讲解注解相关的姿势了,可自行搜索资料学习,或参见 《Kotlin实用教程 | 0x9 - 注解与反射》,Java 和 Kotlin 的注解规则有点不一样哈~
此处应用注解正是第三种场景,先定义一个注解类:
// 与Java用 @interface 声明注解不同,kotlin使用 annotation class 进行声明
@Target(AnnotationTarget.FUNCTION) // 修饰函数
@Retention(AnnotationRetention.RUNTIME) // 函数的保留存活时间
annotation class Subscribe
接着获取订阅者类所有的方法,判断修饰符及是否包含Subscribe注解,然后加入集合:
// 修改后的被观察者
object Subject {
private const val BRIDGE = 0x40
private const val SYNTHETIC = 0x1000
private const val MODIFIERS_IGNORE = Modifier.ABSTRACT or Modifier.STATIC or BRIDGE or SYNTHETIC
private val observerMap = hashMapOf<Any, Method?>()
fun register(any: Any) {
try {
val methods = any.javaClass.declaredMethods
methods.forEach {
val modifiers = it.modifiers
// 判断方法修饰符是否为public,不是Static、ABSTRACT等修饰符
if (modifiers and Modifier.PUBLIC != 0 && modifiers and MODIFIERS_IGNORE == 0) {
// 获取参数列表
val parameterTypes = it.parameterTypes
// 判断参数是否只有一个
if (parameterTypes.size == 1) {
// 获取SubScribe注解
val subscribeAnnotation = it?.getAnnotation(Subscribe::class.java)
// Subscribe注解不为空的,把回调方法加入集合
subscribeAnnotation?.let { _ -> observerMap[any] = it }
}
}
}
} catch (e: NoSuchMethodException) {
e.printStackTrace()
}
}
fun unregister(any: Any) {
observerMap[any] = null
observerMap.remove(any)
}
fun postMessage(any: Any) {
observerMap.forEach { (key, value) ->
value?.invoke(key, any)
}
}
}
// 为观察者回调方法添加@Subscribe注解,并修改为别的函数名
@Subscribe
fun onMainEvent(any: Any) {
when (any) {
is DataEntity -> tv_content.text = any.data
is RefreshEntity -> Toast.makeText(this, "收到更新广播", Toast.LENGTH_SHORT).show()
}
}
运行效果一致,而且又想到了一个好玩的东西,还记得上面②那里写的:观察者不一定要关注被观察者的所有行为,利用注解,我们可以对不同的 广播类型 进行区分,观察者按需关注对应广播~
⑦ 巧用运行时注解 → 只关注想关注的广播
三个要素:广播、观察者、回调方法,可以把他们都定义到一个数据类里,但遍历起来有些繁琐,直接空间换时间,加多个 map<广播类型, 观察者列表>。而同样的广播可以发送多次,用 Class类类型 表示,而观察者直接用 普通类型 表示。
考虑到:java反射方法的调用要传入对象实例 + 广播类型与Method一一对应,创建两个数据类:
class SubscriberMethod(var method: Method?, var eventType: Class<*>?)
class Subscription(var subscriber: Any?, var subscriberMethod: SubscriberMethod?)
然后是一些判断、遍历逻辑,以及取消注册时的置空,比较简单,修改后的代码如下:
object Subject {
private const val BRIDGE = 0x40
private const val SYNTHETIC = 0x1000
private const val MODIFIERS_IGNORE = Modifier.ABSTRACT or Modifier.STATIC or BRIDGE or SYNTHETIC
// key:广播类型,value:观察者列表
private val typeMap = hashMapOf<Class<*>, ArrayList<Any>>()
// key: 观察者,value:观察者数据集
private val observerMap = hashMapOf<Any, ArrayList<Subscription>>()
fun register(any: Any) {
try {
val methods = any.javaClass.declaredMethods
methods.forEach {
val modifiers = it.modifiers
if (modifiers and Modifier.PUBLIC != 0 && modifiers and MODIFIERS_IGNORE == 0) {
val parameterTypes = it.parameterTypes
if (parameterTypes.size == 1) {
val subscribeAnnotation = it?.getAnnotation(Subscribe::class.java)
subscribeAnnotation?.let { _ ->
// 判断观察者对应的列表是否为空,空新建并添加Subscription
if (observerMap[any] == null) observerMap[any] = arrayListOf()
observerMap[any]!!.add(Subscription(any, SubscriberMethod(it, parameterTypes[0])))
// 判断事件对应的列表是否为空,空新建并添加观察者实例
if (typeMap[parameterTypes[0]] == null) typeMap[parameterTypes[0]] = arrayListOf()
typeMap[parameterTypes[0]]!!.add(any)
}
}
}
}
} catch (e: NoSuchMethodException) {
e.printStackTrace()
}
}
fun unregister(any: Any) {
// 强引用,解绑时要置空
any.javaClass.let { cls ->
observerMap[cls]?.forEach {
it.subscriber = null
it.subscriberMethod?.method = null
it.subscriberMethod?.eventType = null
}
observerMap.remove(cls)
typeMap.values.forEach {
if (cls in it) {
it.clear()
it.remove(any.javaClass)
}
}
}
}
fun postMessage(any: Any) {
any.javaClass.let { cls ->
if (cls in typeMap.keys) {
typeMap[cls]!!.forEach {
observerMap[it]?.forEach { subscription ->
// 判断如果是否为要处理的Event
if (subscription.subscriberMethod!!.eventType!!.isInstance(any)) {
subscription.subscriberMethod!!.method!!.invoke(it, any)
}
}
}
}
}
}
}
运行效果一致,这里我们采用反射的方式对整个观察者进行了扫描,可以是可以,但当观察者很多时,需耗费较多时间,会带来性能上的影响。有没有办法,在 运行前(编译时) 就拿到需要反射的订阅函数信息,而不用等到运行时再去遍历获取?有,那就是 编译时注解 ~
⑧ 巧用编译时注解 → 省去编译时扫描整个类
自定义一个注解处理器 → 解析 @Subscribe 类型的注解 → 生成一个包含订阅广播信息的java文件 → 广播库对这个Java类做解析获取到订阅的广播信息。
1) 自定义注解处理器
新建一个 New Module
→ Java or Kotlin Library
→ 项目名和类名都为:LBProcessor
没有合到一个项目另外建库的原因:注解处理器只在编译时运行,没必要打包到一起 ~
接着 LBProcessor类
继承 AbstractProcessor类
,定义一个用于信息打印的Messager实例,重写init()和process()方法:
@SupportedAnnotationTypes("com.example.test.temp.Subscribe") // 支持解释哪些注解类
@SupportedSourceVersion(SourceVersion.RELEASE_8) // 要支持的jdk版本
class LBProcessor: AbstractProcessor() {
private var messager: Messager? = null
override fun init(processingEnv: ProcessingEnvironment?) {
super.init(processingEnv)
messager = processingEnv?.messager
messager?.printMessage(Diagnostic.Kind.WARNING, "LBProcessor init")
}
override fun process(annotations: MutableSet<out TypeElement>?, roundEnv: RoundEnvironment?): Boolean {
messager!!.printMessage(Diagnostic.Kind.WARNING, "LBProcessor process")
return true
}
}
接着指定下注解处理器,按照下述路径依次创建文件夹:
在main目录下新建文件:\resources\META-INF\services\
新建文件:
javax.annotation.processing.Processor
文件内容为注解处理类的完整类名:
com.coderpig.lbprocessor.LBProcessor
也可以用AutoService 自动生成此文件,接着在app层级的build.gradle添加下述依赖:
kapt project(":LBProcessor")
implementation project(':LBProcessor')
sync后,终端键入 gradlew build
看看效果:
Tips:AbstractProcessor的父类Processer会在编译阶段初始化,对当前模块内的代码进行一次扫描,获取对应注解,然后调用process方法,根据这些注解类来做后续操作,发生在source -> complier过程:
2) 通过注解拿到类、函数信息
一些注解相关API的使用,直接给出代码:
@SupportedAnnotationTypes("com.example.test.temp.Subscribe") // 支持解释哪些注解类
@SupportedSourceVersion(SourceVersion.RELEASE_8) // 要支持的jdk版本
class LBProcessor : AbstractProcessor() {
private var messager: Messager? = null
private var methodsByClass = hashMapOf<TypeElement, ArrayList<ExecutableElement>>()
override fun init(processingEnv: ProcessingEnvironment?) {
super.init(processingEnv)
messager = processingEnv?.messager
messager?.printMessage(Diagnostic.Kind.WARNING, "LBProcessor init")
}
override fun process(
annotations: MutableSet<out TypeElement>,
roundEnv: RoundEnvironment
): Boolean {
annotations.forEach {
val elements = roundEnv.getElementsAnnotatedWith(it)
elements.forEach { element ->
if (element is ExecutableElement) {
// 获取类实例
val classElement = element.enclosingElement as TypeElement
if(methodsByClass[classElement] == null) {
methodsByClass[classElement] = arrayListOf()
}
methodsByClass[classElement]!!.add(element)
}
}
}
return true
}
}
注解处理器代码下断点进入调试模式(调试方法可看:我想调试下build.gradle),可以看到处理完后的列表:
啧啧啧,信息能拿到了,接着设计Java类~
3) 自动生成Java类的设计
从上面我们获到并能转移的信息:订阅者类的Class、方法名、广播类型的Class
需要设计传递这三者的数据结构,包两层(java实现,kt外部调可能有问题):
// 方法名 + 广播类型
public class SubscriberMethodGen {
private String methodName;
private Class<?> eventType;
public SubscriberMethodGen(String methodName, Class<?> eventType) {
this.methodName = methodName;
this.eventType = eventType;
}
public String getMethodName() {
return methodName;
}
public Class<?> getEventType() {
return eventType;
}
}
// 订阅者类型 + 订阅方法数组
public class SubscriberInfoGen {
private Class<?> subscriberClass;
private SubscriberMethodGen[] subscriberMethodGens;
public SubscriberInfoGen(Class<?> subscriberClass, SubscriberMethodGen[] subscriberMethodGens) {
this.subscriberClass = subscriberClass;
this.subscriberMethodGens = subscriberMethodGens;
}
public Class<?> getSubscriberClass() {
return subscriberClass;
}
public SubscriberMethodGen[] getSubscriberMethodGens() {
return subscriberMethodGens;
}
}
接着是设计我们的生成类结构:
public class SubscriberGen {
private static final Map<Class<?>, SubscriberInfoGen> SUBSCRIBE_INFO_MAP;
static {
SUBSCRIBE_INFO_MAP = new HashMap<>();
putSubscribe(new SubscriberInfoGen(com.example.test.ATestActivity.class, new SubscriberMethodGen[]{
new SubscriberMethodGen("onXXXEvent", DataEntity.class),
new SubscriberMethodGen("onYYYEvent", RefreshEntity.class)
}));
}
private static void putSubscribe(SubscriberInfoGen info) {
SUBSCRIBE_INFO_MAP.put(info.getSubscriberClass(), info);
}
}
static静态代码块,类加载时就完成初始化,剩下生成Java时要做的操作就是:有几个订阅者塞几个putSubscribe()方法。
接着再定义一个获取订阅者方法的方法,因为这里的方法参数只是方法名,我们需要的是 Method对象,遍历反射一波,生成SubscriberMethod数组:
public SubscriberMethod[] getSubscriberMethod(Class<?> subscriberClass) {
SubscriberInfoGen gen = SUBSCRIBE_INFO_MAP.get(subscriberClass);
if (gen != null) {
SubscriberMethodGen[] methodGens = SUBSCRIBE_INFO_MAP.get(subscriberClass).getSubscriberMethodGens();
SubscriberMethod[] methods = new SubscriberMethod[methodGens.length];
for (int i = 0; i < methodGens.length; i++) {
try {
Method method = subscriberClass.getDeclaredMethod(methodGens[i].getMethodName(), methodGens[i].getEventType());
methods[i] = new SubscriberMethod(method, methodGens[i].getEventType());
} catch (NoSuchMethodException e) {
e.printStackTrace();
}
}
return methods;
}
return null;
}
紧接着就是在我们的被订阅者类中试验能否解析成功了,改动内容如下:
private var byGen = true // 读取java文件形式的标记
private var subscriberGen: SubscriberGen? = null
init {
subscriberGen = SubscriberGen() // init代码块中完成初始化,避免重复创建
}
fun register(any: Any) {
if (byGen) {
val subscriberInfo = subscriberGen!!.getSubscriberMethod((any::class.java))
subscriberInfo?.forEach {
if (observerMap[any] == null) observerMap[any] = arrayListOf()
observerMap[any]!!.add(Subscription(any, SubscriberMethod(it.method, it.eventType)))
val eventTypeClass = it.eventType
if(eventTypeClass != null) {
if(typeMap[eventTypeClass] == null)
typeMap[eventTypeClass] = arrayListOf()
typeMap[eventTypeClass]!!.add(any)
}
}
} else {
...原先扫描类方法的代码
}
运行后测试一波,效果一致,可以,继续往下走 ~
4) 根据注解自动生成Java类
一般方案是土方法 BufferedWriter
一行行拼接代码然后输出文件,而这里生成的文件只有一个,可以上 square/javapoet 偷下懒。导下依赖:
implementation 'com.squareup:javapoet:1.13.0'
参考API文档,对着上述的生成类,照葫芦画瓢写出生成方法,如果API不熟悉挺难搞的,直接给出完整处理代码:
// 动态生成Java文件
private fun createLBFile(className: String?) {
val subscriberInfoGen = ClassName.get("com.example.test.temp", "SubscriberInfoGen")
val subscriberMethod = ClassName.get("com.example.test.temp", "SubscriberMethod")
val subscriberMethodGen = ClassName.get("com.example.test.temp", "SubscriberMethodGen")
val classClass = ClassName.bestGuess("Class<?>")
val subscriberMethodArrayClass = ClassName.bestGuess("SubscriberMethod[]")
val mapClass = ClassName.bestGuess("Map<Class<?>, SubscriberInfoGen>")
// 集合
val subscribeInfoMap = FieldSpec.builder(mapClass, "SUBSCRIBE_INFO_MAP")
.addModifiers(Modifier.PRIVATE, Modifier.FINAL, Modifier.STATIC).build()
// 为了导Map包和HashMap包引入的,没作用
val tempMap = FieldSpec.builder(Map::class.java, "tempMap")
.initializer("new \$T<String, String>()", HashMap::class.java)
.build()
// 静态代码块部分
val staticCode =
CodeBlock.builder().addStatement("SUBSCRIBE_INFO_MAP = new HashMap<>()").apply {
methodsByClass.forEach { (typeElement, arrayList) ->
add(
"putSubscribe(new SubscriberInfoGen(\$L.class, new SubscriberMethodGen[] {\n",
typeElement.qualifiedName
)
arrayList.forEachIndexed { index, it ->
add("new \$T(\"\$L\"", subscriberMethodGen, it.simpleName)
it.parameters.forEach { param -> add(",\$L.class", param.asType()) }
add(")")
if (index != arrayList.size - 1) add(",")
}
add("\n}));\n")
}
}.build()
// putSubscribe() 方法
val putSubscribe = MethodSpec.methodBuilder("putSubscribe")
.addModifiers(Modifier.PUBLIC, Modifier.STATIC)
.returns(Void.TYPE)
.addParameter(subscriberInfoGen, "info")
.addCode("SUBSCRIBE_INFO_MAP.put(info.getSubscriberClass(), info);")
.build()
// getSubscriberMethod() 方法
val getSubscriberMethod = MethodSpec.methodBuilder("getSubscriberMethod")
.addModifiers(Modifier.PUBLIC)
.returns(subscriberMethodArrayClass)
.addParameter(classClass, "subscriberClass")
.addStatement("SubscriberInfoGen gen = SUBSCRIBE_INFO_MAP.get(subscriberClass)")
.addCode("if (gen != null) { \n")
.addStatement("SubscriberMethodGen[] methodGens = SUBSCRIBE_INFO_MAP.get(subscriberClass).getSubscriberMethodGens()")
.addStatement(
"\$T[] methods = new SubscriberMethod[methodGens.length]",
subscriberMethod
)
.addCode("for (int i = 0; i < methodGens.length; i++) {\n")
.beginControlFlow("try")
.addStatement(
"\$T method = subscriberClass.getDeclaredMethod(methodGens[i].getMethodName(), methodGens[i].getEventType())",
Method::class.java
)
.addStatement("methods[i] = new SubscriberMethod(method, methodGens[i].getEventType())")
.nextControlFlow("catch (\$T e)", NoSuchMethodException::class.java)
.addStatement("e.printStackTrace()")
.endControlFlow()
.addCode("}\n return methods;\n}\n")
.addStatement("return null")
.build()
// 拼接生成最终类
val subscriberGen = TypeSpec.classBuilder("className")
.addModifiers(Modifier.PUBLIC, Modifier.FINAL)
.addField(tempMap)
.addField(subscribeInfoMap)
.addStaticBlock(staticCode)
.addMethod(putSubscribe)
.addMethod(getSubscriberMethod)
.build()
try {
val javaFile = JavaFile.builder("com.example.test", subscriberGen).build()
javaFile.writeTo(filer)
} catch (e: Exception) {
print(e.message)
}
}
// process() 方法处加上调用,后面这个Class名也可以通过扩展方式传入,如 processingEnv.options["lbClass"]:
if (methodsByClass.size > 0) createLBFile("SubscriberGen")
真心写到吐血,这段玩意凑了我2个小时...接着改动下页面B的代码,也注册下事件,然后build一下:
可以,虽然没有排版,但没语法错误,直接运行,试试康效果:
此处应有掌声,这么秀的东西,赶紧拿给小伙伴秀一下~
⑨ 利用Handler → 解决在子线程发广播引起的问题
小伙伴一开始还觉得你很6,玩耍了一会儿后跟你说,挂了,错误日志如下:
瞅下代码:
好家伙,怎么敢的啊,直接在子线程里发送广播?em...不过好像也正常,开主线程执行某项耗时操作,完成时发个广播。
所以在postMessage()时还需要对线程进行判断,主线程直接回调,子线程则需要利用Handler消息机制。
判断是否主线程很简单,直接:
if(Looper.getMainLooper() == Looper.myLooper())
不懂原理的可以自己抠源码,也可以看我写过的:《换个姿势,带着问题看Handler》
子线程用Handler消息机制,直接借(chao)鉴(xi)广播的思路,肝出代码:
private var mHandler: Handler? = null
private const val LIGHT_BROADCASTS = 666 // 广播标记
// 提供给异步广播临时用的...
private var mTempEntity: Any? = null
private var mTempSubscription: Subscription? = null
init {
subscriberGenDefault = SubscriberGen() // init代码块中完成初始化,避免重复创建
mHandler = object : Handler(Looper.getMainLooper()) {
override fun handleMessage(msg: Message) {
super.handleMessage(msg)
when (msg.what) {
LIGHT_BROADCASTS -> {
if (mTempEntity != null && mTempSubscription != null) {
mTempSubscription!!.subscriberMethod!!.method!!.invoke(
mTempSubscription!!.subscriber,
mTempEntity
)
// 用完重置
mTempEntity = null
mTempSubscription = null
}
}
else -> super.handleMessage(msg)
}
}
}
}
// 广播分发部分的代码
if (Looper.getMainLooper() == Looper.myLooper()) {
// 直接回调
subscription.subscriberMethod!!.method!!.invoke(it, any)
} else {
// 非主线程,存下用到的变量,发起一个消息
mHandler!!.sendMessage(mHandler!!.obtainMessage().apply {
mTempEntity = any
mTempSubscription = subscription
what = LIGHT_BROADCASTS
})
}
虽然不优雅,但问题解决了,细心的朋友可以看到我定义了两个临时变量,也是无奈之举:
Handler传对象,对应的类需要实现序列化接口,而且有大小限制...
这里更好的解法肯定是维护一个广播消息的队列,还可以细出更多的玩法来,限于篇幅,也就到这里了~
⑩ EventBus拾遗
Ctrl+R替换全文,把"广播"都换成"EventBus",你会发现读着还挺通顺,是的,本文挂着 "实现轻量级广播" 的狗头,把 "基本的EventBus" 给肝出来了,23333。
关键的技术细节都有涉猎到,还没做的有这些:
- 设计原则 + 面向对象特性 → 对类进行细化、拆分、重新设计,尽量可扩展等;
- synchronized + 并发容器(如CopyOnWriteArrayList、ConcurrentHashMap) → 并发处理,保证线程安全;
- 设计一个广播队列 → 区分不同类型的广播,判断执行不同的处理 (源码中的postToSubscription())
- 一些其他的细节 → 日志、错误处理等。
上面这些就属于经验积累范畴的东西了,多看库源码多实践,就说这么多吧,感谢😁~
参考文献: