自己写一个 KEventBus(四)—— 注解处理器

2021-03-29  本文已影响0人  Vic_wkx

一、为什么需要注解处理器

在 KEventBus 的 register 函数中,我们用到了反射技术查看 obj 的所有函数,从所有函数中找出带有 @Subscribe 注解的函数,然后将其添加到观察者中。

众所周知,反射技术是比较耗时的,用反射技术来解析注解是一个很消耗性能的操作,所以 Java 为我们提供了注解处理器(Annonation Processor),Kotlin 语言有专门的注解处理器 kapt(Kotlin annotation processing tool)。它可以帮助我们在编译期处理所有带注解的元素。我们可以从中解析出需要的信息,并将这些需要的信息保存起来。在程序运行的时候,就可以直接使用这些信息了。这样就避免了在运行期间通过耗时的反射来查询所有的函数这一步骤。

二、如何使用注解处理器解析出的信息

具体的保存方式是生成一个 KEventBusIndex 类,比如,对于这样一个类:

class MainActivity : AppCompatActivity() {

    //...
    @Subscribe
    fun onStringEvent(message: String) {
        
    }
}

我们需要生成一个这样的对象:

class KEventBusIndex() {
    val methodsByClass = mutableMapOf<Class<*>, MutableList<SubscriberMethodInfo>>()
    
    init {
        if (methodsByClass[MainActivity::class.javaObjectType] == null) methodsByClass[MainActivity::class.javaObjectType] = mutableListOf()
        methodsByClass[MainActivity::class.javaObjectType]!!.add(SubscriberMethodInfo("onStringEvent", String::class.javaObjectType, ThreadMode.MAIN, false))
    }
}

其中,methodsByClass 保存了每一个类中,所有带有 @Subscribe 注解的函数的信息。SubscriberMethodInfo 类就是用于存储函数信息的:

data class SubscriberMethodInfo(val methodName: String, val eventType: Class<*>, val threadMode: ThreadMode, val sticky: Boolean)

有了 KEventBusIndex 这一个辅助类,我们就可以在 register 时,通过这个类中的 methodsByClass,很快找到所有的观察者:

private val index = KEventBusIndex()
fun register(obj: Any) {
    index.methodsByClass[obj.javaClass]?.forEach {
        if (it.eventType !in subscriptionsByEventType) {
            subscriptionsByEventType[it.eventType] = mutableListOf()
        }
        val subscriberMethod = SubscriberMethod(obj, obj.javaClass.getDeclaredMethod(it.methodName, it.eventType), it.threadMode)
        subscriptionsByEventType[it.eventType]!!.add(subscriberMethod)
        if (it.sticky) {
            if (stickyEvents[it.eventType] != null) {
                subscriberMethod.method(obj, stickyEvents[it.eventType])
            }
        }
    }
}
fun unregister(obj: Any) {
    index.methodsByClass[obj.javaClass]?.forEach {
        if (it.eventType in subscriptionsByEventType) {
            subscriptionsByEventType[it.eventType]?.remove(SubscriberMethod(obj, obj.javaClass.getDeclaredMethod(it.methodName, it.eventType), it.threadMode))
        }
    }
}

这种方式可以大幅提升 EventBus 的性能。

可以看到,我们由 SubscriberMethodInfo 生成 SubscriberMethod 的过程中,还是用到了一次反射:

obj.javaClass.getDeclaredMethod(it.methodName, it.eventType)

这里通过反射技术,从函数的名字和参数信息,拿到具体的函数。这个反射过程是无法避免的,因为我们只有在 register 时,才能拿到这个具体的观察者,这时才能把这个具体的观察者的 @Subscribe 函数绑定到被观察的 Event 上。

注解处理器只是帮助我们省去了 反射遍历所有函数,找到所有带 @Subscribe 注解的函数 的过程。

上述代码的这一行是无法编译通过的:

private val index = KEventBusIndex()

这是因为 KEventBusIndex 类是在编译后才生成的,我们不能直接用构造函数 new 出它的实例。所以我们需要定义一个接口,在 KEventBus 类中,先使用此接口来调用其中的方法。具体的 KEventBusIndex 类,可以由客户端在运行时传入,也可以用反射技术拿到。

SubscriberInfoIndex 接口:

interface SubscriberInfoIndex {
    val methodsByClass: Map<Class<*>, MutableList<SubscriberMethodInfo>>
}

KEventBus 中,将 index 对应修改为接口类型:

lateinit var index: SubscriberInfoIndex

这样就可以编译通过了。接下来只剩一个问题,如何生成这个 KEventBusIndex 类。

三、如何生成 KEventBusIndex 类

这里涉及的都是注解处理器的基本使用方式,与 EventBus 本身的关系不大。

注解处理器不能在 Android Library 中使用,只能用到 Java/Kotlin Library 中,keventbus 所在的 module 如果是 Android Library 的话,需要先改成 Java/Kotlin Library。
修改 build.gradle 文件,编辑如下:

plugins {
    id 'java-library'
    id 'kotlin'
}

dependencies {
    compileOnly 'com.google.android:android:4.1.1.4'
    compileOnly "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
}

然后删除这个 module 下的 androidTest/ 和 test/ 文件夹即可。

当然,在创建 keventbus module 时,也可以直接创建 Java/Kotlin library,就可以省去这一步操作。

然后,在 src/ 目录下新建 resources/META-INF/services 文件夹,在其中新建 javax.annotation.processing.Processor 文件,编辑文件内容:

com.library.keventbus.KEventBusProcessor

这里的内容是自定义的注解处理器的路径,这一步是固定步骤,目的就是让 Library 找到我们自定义的注解处理器。由于这个固定步骤实在是枯燥乏味,所以 github 上有很多的框架可以帮我们生成这个文件,比如著名的 AutoService。本文采用手打的方式,加深读者理解。

KEventBusProcessor 类用来读取所有带 @Subscribe 注解的元素,并通过这些元素生成 KEventBusIndex.kt 文件。

KEventBusProcessor 类:

class KEventBusProcessor : AbstractProcessor() {
    override fun getSupportedAnnotationTypes(): MutableSet<String> {
        return mutableSetOf(Subscribe::class.java.canonicalName)
    }

    override fun process(annotations: MutableSet<out TypeElement>?, env: RoundEnvironment?): Boolean {
        annotations?.forEach { annotation ->
            val generatedDir = processingEnv.options["kapt.kotlin.generated"]
            val filePath = "$generatedDir/com/example/eventbus"
            val file = File(filePath, "KEventBusIndex.kt")
            file.parentFile.mkdirs()
            val writer = BufferedWriter(FileWriter(file))
            writer.use {
                it.write(
                    """
                package com.example.eventbus
                
                import com.library.keventbus.SubscriberInfoIndex
                import com.library.keventbus.SubscriberMethodInfo
                import com.library.keventbus.ThreadMode
                
                class KEventBusIndex() : SubscriberInfoIndex {
                    override val methodsByClass = mutableMapOf<Class<*>, MutableList<SubscriberMethodInfo>>()
                    
                    init {
                
            """.trimIndent()
                )
                env?.getElementsAnnotatedWith(annotation)?.forEach { element ->
                    val clazz = element.enclosingElement
                    val method = element as ExecutableElement
                    val subscribe = method.getAnnotation(Subscribe::class.java)
                    val obj = "$clazz::class.javaObjectType"
                    val methodName = method.simpleName
                    val eventType = "${method.parameters.first().asType()}::class.javaObjectType"
                    val threadMode = "ThreadMode.${subscribe.threadMode}"
                    val sticky = subscribe.sticky
                    it.write("\t\tif (methodsByClass[$obj] == null) methodsByClass[$obj] = mutableListOf()\n")
                    it.write("\t\tmethodsByClass[$obj]!!.add(SubscriberMethodInfo(\"$methodName\", $eventType, $threadMode, $sticky))\n")
                }
                it.write(
                    """
                    }
                }
                """.trimIndent()
                )
            }
        }
        return false
    }
}

在这个类中,通过 getSupportedAnnotationTypes 函数指定需要处理的注解类型,这里我们传入了 Subscribe 类的全路径。除了通过重写函数指定注解类型外,还可以通过为这个类添加注解指定,比如这个例子中,添加注解@SupportedAnnotationTypes("com.library.keventbus.Subscribe")效果是一样的。

在 process 函数中处理带有这个注解的所有元素。

kapt 生成文件时的默认路径是 processingEnv.options["kapt.kotlin.generated"],它的值是 /build/generated/source/kaptKotlin/debug,我们将 KEventBusIndex.kt 生成到这个路径下的 /com/example/eventbus 文件夹中。

解析注解元素的过程就是一些 api 的调用,没什么技术含量。

最终,这个注解处理器生成的文件类似这样:

package com.example.eventbus

import com.library.keventbus.SubscriberInfoIndex
import com.library.keventbus.SubscriberMethodInfo
import com.library.keventbus.ThreadMode

class KEventBusIndex() : SubscriberInfoIndex {
    override val methodsByClass = mutableMapOf<Class<*>, MutableList<SubscriberMethodInfo>>()
    
    init {
        if (methodsByClass[com.example.eventbus.MainActivity::class.javaObjectType] == null) methodsByClass[com.example.eventbus.MainActivity::class.javaObjectType] = mutableListOf()
        methodsByClass[com.example.eventbus.MainActivity::class.javaObjectType]!!.add(SubscriberMethodInfo("onStringEvent", java.lang.String::class.javaObjectType, ThreadMode.MAIN, false))
        if (methodsByClass[com.example.eventbus.MainActivity::class.javaObjectType] == null) methodsByClass[com.example.eventbus.MainActivity::class.javaObjectType] = mutableListOf()
        methodsByClass[com.example.eventbus.MainActivity::class.javaObjectType]!!.add(SubscriberMethodInfo("onAsyncEvent", com.example.eventbus.AsyncEvent::class.javaObjectType, ThreadMode.ASYNC, false))
        if (methodsByClass[com.example.eventbus.MainActivity::class.javaObjectType] == null) methodsByClass[com.example.eventbus.MainActivity::class.javaObjectType] = mutableListOf()
        methodsByClass[com.example.eventbus.MainActivity::class.javaObjectType]!!.add(SubscriberMethodInfo("onStickyEvent", com.example.eventbus.StickyEvent::class.javaObjectType, ThreadMode.MAIN, true))
    }
}

到这里,我们的注解处理器基本写完了。使用时,只需要将生成的 KEventBusIndex 类传给 KEventBus 的 index 参数就可以了。但前文说到,我们也可以用反射的方式为 index 赋值,类似这样:

private val index: SubscriberInfoIndex by lazy { Class.forName("com.example.eventbus.KEventBusIndex").newInstance() as SubscriberInfoIndex }

这样修改之后,我们就无需传入生成的 KEventBusIndex 类了。

但著名的 EventBus 框架中,并没有将 EventBusIndex 类的生成路径写死,而是将其作为可配置项交给用户完成。目的是让 EventBus 在多个 Module 中使用时,避免 Index 类重名。

所以想要使用 EventBus 的 Index 功能,必须调用 addIndex 函数将生成的 Index 类传入:

EventBus.builder().addIndex(new MyEventBusIndex()).installDefaultEventBus()

这也让 EventBus 变得更加灵活。

本系列文章没有完全模仿 EventBus,只是通过一步步手写 EventBus 的方式讲解其原理。将 index 设置为可配置的方式也非常简单,感兴趣的读者可以自行实现,或参考 EventBus 的源码。

源码已上传 github:https://github.com/wkxjc/KEventBus

上一篇 下一篇

猜你喜欢

热点阅读