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。
简单点说:被观察者还得知道观察者具体的类型,这又耦合了...
这个问题先放一放,等下会解决,这里思考另一个问题:
既然暂时没法有脑遍历了,那广播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() } }
运行效果一致,而且又想到了一个好玩的东西,还记得上面②那里写的:观察者不一定要关注被观察者的所有行为,利用注解,我们可以对不同的 广播类型 进行区分,观察者按需关注对应广播~