换个姿势,更好地参透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())
  • 一些其他的细节 → 日志、错误处理等。


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


参考文献:



相关文章
|
2月前
|
Android开发 开发者 Kotlin
告别AsyncTask:一招教你用Kotlin协程重构Android应用,流畅度飙升的秘密武器
【9月更文挑战第13天】随着Android应用复杂度的增加,有效管理异步任务成为关键。Kotlin协程提供了一种优雅的并发操作处理方式,使异步编程更简单直观。本文通过具体示例介绍如何使用Kotlin协程优化Android应用性能,包括网络数据加载和UI更新。首先需在`build.gradle`中添加coroutines依赖。接着,通过定义挂起函数执行网络请求,并在`ViewModel`中使用`viewModelScope`启动协程,结合`Dispatchers.Main`更新UI,避免内存泄漏。使用协程不仅简化代码,还提升了程序健壮性。
76 1
|
3月前
|
开发者 C# Android开发
明白吗?Xamarin与Native的终极对决:究竟哪种开发方式更适合您的项目需求,让我们一探究竟!
【8月更文挑战第31天】随着移动应用开发的普及,开发者面临多种技术选择。本文对比了跨平台解决方案Xamarin与原生开发方式的优势与劣势。Xamarin使用C#进行跨平台开发,代码复用率高,可大幅降低开发成本;但因基于抽象层,可能影响性能。原生开发则充分利用平台特性,提供最佳用户体验,但需维护多套代码库,增加工作量。开发者应根据项目需求、团队技能和预算综合考量,选择最适合的开发方式。
110 0
|
6月前
|
存储 Web App开发 运维
发布、部署,傻傻分不清楚?从概念到实际场景,再到工具应用,一篇文章让你彻底搞清楚
部署和发布是软件工程中经常互换使用的两个术语,甚至感觉是等价的。然而,它们是不同的! • 部署是将软件从一个受控环境转移到另一个受控环境,它的目的是将软件从开发状态转化为生产状态,使得软件可以为用户提供服务。 • 发布是将软件推向用户的过程,应用程序需要多次更新、安全补丁和代码更改,跨平台和环境部署需要对版本进行适当的管理,有一定的计划性和管控因素。
1480 1
|
Web App开发 JavaScript 前端开发
这次终于搞清楚移动端开发了(三)
这次终于搞清楚移动端开发了(三)
|
编解码 前端开发 JavaScript
这次终于搞清楚移动端开发了(一)
这次终于搞清楚移动端开发了(一)
|
编解码 前端开发 UED
这次终于搞清楚移动端开发了(二)
这次终于搞清楚移动端开发了(二)
|
设计模式 缓存 Android开发
换个姿势,更好地参透EventBus(上)
EventBus(事件总线),跟之前写的 Handler 一样,老生常谈,教程早已烂大街
247 1
|
移动开发 开发框架 前端开发
一文搞懂ReactNative生命周期的进化
一文搞懂ReactNative生命周期的进化
598 0
一文搞懂ReactNative生命周期的进化
|
XML 架构师 Java
一文把Java反射说的明明白白,清清楚楚,记得点赞关注,距离架构师的小目标又进一步
今天有时间没加班回家来好好写一篇文章,反射是Java里比较高级的概念了,一般在书的后半部分。反射也是写框架的必备技能,反射很重要,现在仍然记得刚毕业的一两年一直没有搞懂反射是什么。今天就讲讲反射,希望这篇文章能帮有同样疑惑的你解开疑团,废话不多说,让我们开始吧。
188 0
一文把Java反射说的明明白白,清清楚楚,记得点赞关注,距离架构师的小目标又进一步
|
设计模式 存储 Java
换个姿势,更好地参透EventBus(中)
EventBus(事件总线),跟之前写的 Handler 一样,老生常谈,教程早已烂大街
109 0