换个姿势,更好地参透EventBus

3,928 阅读7分钟

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


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 ModuleJava 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())
  • 一些其他的细节 → 日志、错误处理等。

上面这些就属于经验积累范畴的东西了,多看库源码多实践,就说这么多吧,感谢😁~


参考文献