换个姿势,更好地参透EventBus(下)

简介: EventBus(事件总线),跟之前写的 Handler 一样,老生常谈,教程早已烂大街

⑦ 巧用运行时注解 → 只关注想关注的广播


三个要素:广播、观察者、回调方法,可以把他们都定义到一个数据类里,但遍历起来有些繁琐,直接空间换时间,加多个 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())
  • 一些其他的细节 → 日志、错误处理等。


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


参考文献:



相关文章
|
3月前
|
存储 Web App开发 运维
发布、部署,傻傻分不清楚?从概念到实际场景,再到工具应用,一篇文章让你彻底搞清楚
部署和发布是软件工程中经常互换使用的两个术语,甚至感觉是等价的。然而,它们是不同的! • 部署是将软件从一个受控环境转移到另一个受控环境,它的目的是将软件从开发状态转化为生产状态,使得软件可以为用户提供服务。 • 发布是将软件推向用户的过程,应用程序需要多次更新、安全补丁和代码更改,跨平台和环境部署需要对版本进行适当的管理,有一定的计划性和管控因素。
172 1
|
12月前
|
人工智能 小程序
行动派:想到就做,无关乎与成功或失败,重在过程!
行动派:想到就做,无关乎与成功或失败,重在过程!
154 0
|
12月前
|
算法 Java
手把手一步一步教你使用Java开发一个大型街机动作闯关类游戏17敌人自动追踪(自动寻路)
手把手一步一步教你使用Java开发一个大型街机动作闯关类游戏17敌人自动追踪(自动寻路)
111 0
|
设计模式 SQL Java
有点狠有点猛,我用责任链模式重构了业务代码
文章开篇,抛出一个老生常谈的问题,学习设计模式有什么作用? 设计模式主要是为了应对代码的复杂性,让其满足开闭原则,提高代码的扩展性 另外,学习的设计模式 一定要在业务代码中落实,只有理论没有真正实施,是无法真正掌握并且灵活运用设计模式的 这篇文章主要说 责任链设计模式,认识此模式是在读 Mybatis 源码时, Interceptor 拦截器主要使用的就是责任链,当时读过后就留下了很深的印象(内心 OS:还能这样玩)
|
XML 架构师 Java
一文把Java反射说的明明白白,清清楚楚,记得点赞关注,距离架构师的小目标又进一步
今天有时间没加班回家来好好写一篇文章,反射是Java里比较高级的概念了,一般在书的后半部分。反射也是写框架的必备技能,反射很重要,现在仍然记得刚毕业的一两年一直没有搞懂反射是什么。今天就讲讲反射,希望这篇文章能帮有同样疑惑的你解开疑团,废话不多说,让我们开始吧。
156 0
一文把Java反射说的明明白白,清清楚楚,记得点赞关注,距离架构师的小目标又进一步
|
设计模式 缓存 Android开发
换个姿势,更好地参透EventBus(上)
EventBus(事件总线),跟之前写的 Handler 一样,老生常谈,教程早已烂大街
215 1
|
设计模式 存储 Java
换个姿势,更好地参透EventBus(中)
EventBus(事件总线),跟之前写的 Handler 一样,老生常谈,教程早已烂大街
90 0
|
Java Spring
9条消除if...else的锦囊妙计,助你写出更优雅的代码(下)
9条消除if...else的锦囊妙计,助你写出更优雅的代码(下)
|
程序员 Android开发
牛逼!终于有人能把Android事件分发机制讲明白了
在Android开发中,事件分发机制是一块Android比较重要的知识体系,了解并熟悉整套的分发机制有助于更好的分析各种点击滑动失效问题,更好去扩展控件的事件功能和开发自定义控件,同时事件分发机制也是Android面试必问考点之一,如果你能把下面的一些事件分发图当场画出来肯定加分不少。废话不多说,总结一句:事件分发机制很重要。
牛逼!终于有人能把Android事件分发机制讲明白了
|
设计模式 算法 Java
9条消除if...else的锦囊妙计,助你写出更优雅的代码
9条消除if...else的锦囊妙计,助你写出更优雅的代码