前言
在上篇文章 Android 注解系列之 EventBus3 原理(四)中我们讲解了 EventBus3 的内部原理,在该篇文章中我们将讲解 EventBus3 中的 “加速引擎“
—索引类。阅读该篇文章我们能够学到如下知识点。
- EventBus3 索引类出现的原因
- EventBus3 索引类的使用
- EventBus3 索引类生成的过程
- EventBus3 混淆注意事项
对 APT 技术不熟悉的小伙伴,可以查看文章 Android 注解系列之APT工具(三)
前景回顾
在 Android 注解系列之 EventBus3 原理(四)中,我们特别指出在 EventBus3 中优化了 SubscriberMethodFinder
获取类中包含 @Subscribe
注解的订阅方法的流程。使其能在 EventBus.register()
方法调用之前就能知道相关订阅事件的方法,这样就减少了程序在运行期间使用反射遍历获取方法所带来的时间消耗。优化点如下图中 红色虚线框
所示:
EventBus 作者 Markus Junginger 也给出了使用索引类前后 EventBus 的效率对比,如下图所示:
从上图中,我们可以使用索引类后,EventBus 的效率有着明显的提升,而效率提升的背后,正是使用了 APT
技术所创建的索引类
。那么接下来我们就来看一看 EventBus3 中是如何结合 APT
技术来进行优化的。
关键代码
阅读过 EventBus3 源码的小伙伴应该都知道,在 EventBus3 中获取类中包含 @Subscribe
注解的订阅方法有两种方式。
- 第一种:是直接在程序运行时反射获取
- 第二种:就是通过索引类。
而使用索引类的关键代码为 SubscriberMethodFinder
中的
getSubscriberInfo() 方法与 findUsingInfo() 方法 。
我们分别来看这两个方法。
findUsingInfo 方法
1 | private List<SubscriberMethod> findUsingInfo(Class<?> subscriberClass) { |
我们能从该方法中获得以下信息:
- EventBus3 中默认会调用 getSubscriberInfo() 方法去获取 subscriberInfo 对象信息。
- 如果 subscriberInfo 不为空,则会从该对象中获取
SubscriberMethod
数组。 - 如果 subscriberInfo 为空,那么会直接通过
反射
去获取SubscriberMethod
集合信息。
SubscriberMethod 类中含有
@Subscribe
注解的方法信息封装(优先级,是否粘性,线程模式,订阅的事件),以及当前方法的 Method 对象(java.lang.reflect
包下的对象)。
也就说 EventBus 是否通过反射获取信息,是由 getSubscriberInfo()方法来决定,那么我们查看该方法。
getSubscriberInfo 方法
1 | private SubscriberInfo getSubscriberInfo(FindState findState) { |
从代码逻辑中我们能得出,如果 subscriberInfoIndexes
集合不为空的话,那么就会从 SubscriberInfoIndex(索引类)
中去获取 SubscriberInfo
对象信息。该方法的逻辑并不复杂,唯一的疑惑就是这个 SubscriberInfoIndex(索引类) 对象是从何而来的呢?
聪明的小伙伴们已经想到了。对!!!就是通过 APT 技术自动生成的类。那么我们怎么使用 EventBus3 中的索引类?以及 EventBus3 中是如何生成的索引类的呢? 不急不急,我们一个一个的解决问题。我们先来看看如何使用索引类。
EventBus中索引类的使用
如果需要使用 EventBus3 中的索引类,我们可以在 App 的 build.gradle
中添加如下配置:
1 | android { |
如果有小伙伴不熟悉 gradle 配置,可以查看 AnnotationProcessorOptions
在上述配置中,我们需要注意如下几点:
- 如果你不使用索引类,那么就没有必要设置
annotationProcessorOptions
参数中的值。也没有必要引入 EventBus 的注解处理器。 - 如果要使用索引类,并且也引入了 EventBus 的注解处理器(eventbus-annotation-processor),但却没有设置 arguments 的话,编译时就会报错:
No option eventBusIndex passed to annotation processor
。 - 索引类的生成,需要我们对代码重新编译。编译成功后,其该类对应路径为
\ProjectName\app\build\generated\source\apt\你设置的包名
。
当我们的索引类生成后,我们还需要在初始化 EventBus 时应用我们生成的索引类,代码如下所示:
1 | EventBus.builder().addIndex(new EventBusIndex()).installDefaultEventBus(); |
之所以要配置索引类,是因为我们需要将我们生成的索引类添加到 subscriberInfoIndexes
集合中,这样我们才能从之前讲解的 getSubscriberInfo()
找到我们配置的索引类。addIndex()
代码如下所示:
1 | public EventBusBuilder addIndex(SubscriberInfoIndex index) { |
索引类实际使用分析
如果你已经配置好了索引类,那么我们看下面的例子,这里我配置的索引类为 EventBusIndex
对应包名为: 'com.eventbus.project'
。我在 EventBusDemo.java 中声明了如下方法:
1 | public class EventBusDemo { |
自动生成的索引类,如下所示:
1 | public class EventBusIndex implements SubscriberInfoIndex { |
在生成的索引类中我们可以看出:
- 生成的索引类中,维护了一个 key 为 订阅对象 value 为
SimpleSubscriberInfo
的 HashMap。 SimpleSubscriberInfo
类中维护了当前订阅者的 class 对象与SubscriberMethodInfo[] 数组
。- HashMap 中的数据添加是放到静态代码块中执行的。
SubscriberMethodInfo 类中含有
@Subscribe
注解的方法信息封装(优先级,是否粘性,线程模式,订阅的事件),以及当前方法的名称
。
到现在,我们已经知道了我们索引类中的内容,那么现在在回到 findUsingInfo()
方法:
1 | private List<SubscriberMethod> findUsingInfo(Class<?> subscriberClass) { |
当 subscriberInfo
不为空时,会通过 getSubscriberMethods()
方法,去获取索引类中 SubscriberMethod[]数组
信息。因为索引类使用的是 SimpleSubscriberInfo
类,我们查看该类中该方法的实现:
1 |
|
观察该代码,我们发现 SubscriberMethod 对象的创建是通过 createSubscriberMethod
方法创建的,我们继续跟踪。
1 | protected SubscriberMethod createSubscriberMethod(String methodName, Class<?> eventType, ThreadMode threadMode, |
从上述代码中,我们可以看出 SubscriberMethod
中的 Method
对象,其实是调用订阅者的 class 对象并使用 getDeclaredMethod()
方法找到的。
现在为止我们已经基本了解,索引类之所以相比传统的通过反射遍历去获取订阅方法效率要更高。是因为在自动生成的索引类中,已经包含了相关订阅者中的订阅方法的名称及注解信息,那么当 EventBus 注册订阅者时,就可以直接通过方法名称
拿到 Method 对象。这样就减少了通过遍历寻找方法的时间。
索引类的生成
那现在我们继续学习 EventBus3 中是如何创建索引类的。索引类的创建是通过 APT
技术,如果你不了解这门技术,你可能需要查看文章 Android 注解系列之APT工具(三)
APT(Annotation Processing Tool)
是 javac 中提供的一种编译时扫描和处理注解的工具,它会对源代码文件进行检查,并找出其中的注解,然后根据用户自定义的注解处理方法进行额外的处理。APT工具不仅能解析注解,还能根据注解生成其他的源文件,最终将生成的新的源文件与原来的源文件共同编译(注意:APT并不能对源文件进行修改操作,只能生成新的文件,例如在已有的类中添加方法
)
使用APT技术需要创建自己的注解处理器,在 EventBus 中也创建了自己的注解处理器,从其源代码中我们就可以看出。
那下面,我们就直接查看源码:
以下的代码,都出至于 EventBusAnnotationProcessor
查看 EventBusAnnotationProcessor 中的 process()
方法:
process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv)
:注解处理器实际处理方法,一般要求子类实现该抽象方法,你可以在在这里写你的扫描与处理注解的代码,以及生成 Java 文件。其中参数 RoundEnvironment ,可以让你查询出包含特定注解的被注解元素.
1 | "org.greenrobot.eventbus.Subscribe") ( |
该方法中主要逻辑为三个逻辑:
- 步骤1:读取我们之前在 APP 中的 build.gradle 设置的索引类对应的包名与类名。
- 步骤2:读取源文件中的包含
@Subscribe
注解的方法。并将订阅者与订阅方法进行记录在methodsByClass
Map 集合中。 - 步骤3:根据读取的索引类设置,通过
createInfoIndexFile()
方法开始创建索引类文件。
因为声明了
@SupportedAnnotationTypes("org.greenrobot.eventbus.Subscribe")
在注解处理器上,那么 APT 只会处理包含该注解的文件。
我们接下来看看步骤2中的方法 collectSubscribers()
方法:
1 | private void collectSubscribers(Set<? extends TypeElement> annotations, RoundEnvironment env, Messager messager) { |
在注解处理过程中,我们需要扫描所有的Java源文件,源代码的每一个部分都是一个特定类型的
Element
,也就是说 Element 代表源文件中的元素,例如包、类、字段、方法等。
在上述方法中,annotations
为扫描到包含 @Subscribe
注解 的 Element
集合。其中 ExecutableElement 表示类或接口的方法、构造函数或初始化器(静态或实例),因为我们可以通过 getEnclosingElement()方法,拿到当前 ExecutableElement
的最近的父 Element,那么我们就能获得当前的类的 element 对象了。那么通过该方法,我们就能知道所有订阅者与其对应的订阅方法了。
我们继续跟踪查看索引类文件的创建:
1 | private void createInfoIndexFile(String index) { |
在该方法中,通过 processingEnv.getFiler().createSourceFile(index)
拿到我们需要创建的索引类文件对象,然后通过文件IO流向该文件中输入索引类中需要的内容。在该方法中,最为主要的就是 writeIndexLines()
方法了。查看该方法:
1 | private void writeIndexLines(BufferedWriter writer, String myPackage) throws IOException { |
在该方法中,会从 methodsByClass
Map 中遍历获取我们之前的订阅者,然后获取其所有的订阅方法,并书写模板方法。其中关构造 SubscriberMethodInfo
代码的关键方法为 writeCreateSubscriberMethods()
,跟踪该方法:
1 | private void writeCreateSubscriberMethods(BufferedWriter writer, List<ExecutableElement> methods, |
在该方法中,会获取订阅方法的参数信息,并构建 SubscriberMethodInfo
信息。这里就不对该方法进行详细的介绍了,大家可以根据代码中的注释进行理解。
混淆相关
在使用 EventBus3 的时候,如果你的项目采用了混淆,需要注意 keep 以下类及方法。官方中已经给出了详细的 keep 规则,如下所示:
1 | -keepattributes *Annotation* |
为什么不能混淆注解
android在打包的时候,应用程序会进行代码优化,优化的过程就把注解给去掉了。为了在程序运行期间读取到注解信息,所以我们需要保存注解信息不被混淆。
为什么不能混淆包含 @Subscribe 注解的方法
因为当我们在使用索引类时,获取相关订阅的方法是通过方法名称
获取的,那么当代码被混淆过后,订阅者的方法名称将会发生改变,比如原来订阅方法名称为onMessageEvent,混淆后有可能改为a,或b方法。这个时候是找不到相关的订阅者的方法的 ,就会抛出 Could not find subscriber method in + subscriberClass + Maybe a missing ProGuard rule?
的异常,所以在混淆的时候我们需要保留订阅者所有包含 @Subscribe
注解的方法。
为什么不能混淆枚举类中的静态变量
如果我们没有在混淆规则中添加如下语句:
1 | -keep public enum org.greenrobot.eventbus.ThreadMode { public static *; } |
在运行程序的时候,会报java.lang.NoSuchFieldError: No static field POSTING
。原因是因为在 SubscriberMethodFinder
的 findUsingReflection
方法中,在调用 Method.getAnnotation()
时获取 ThreadMode
这个 enum
失败了。
我们都知道当我们声明枚举类时,编译器会为我们的枚举,自动生成一个继承java.lang.Enum
的 final
类。如下所示:
1 | //使用命令 javap ThreadMode.class |
也就是说,我们在枚举中声明的元素,其实最后对应的是类中的静态公有的常量。
那么在结合在没有添加混淆规则时,程序所提示的错误信息。我们可以确定当我们在注解中包含枚举类型
的注解元素时且设置了默认值时。该默认值是通过枚举类的 class 对象.getField(String name) 去获取的。因为只有该方法才会抛出该异常。getField()
代码如下所示:
1 | public Field getField(String name) |
那么也就说如果不添加上述的 keep 规则,就会导致我们编译器自动生成的静态常量名发生变化,又因为注解中的默认枚举值,是通过 getField(String name)
获得的。所以就会出现找不到字段的情况。
其实在很多情况下,我们需要添加 keep 规则,常常是因为代码中是直接拿混淆前的方法名称或字段名称去直接寻找混淆后的方法与字段名称,我们只要在项目中注意这些情况,添加相应的 keep 规则,就可以避免因为代码被混淆而产生的异常啦。
最后
EventBus3 中的索引类及其相关内容到这里就讲完啦!我相应大家已经了解了索引类在性能优化上的重要作用。希望大家在后续使用EventBus3时,一定要使用索引类呦。在接下来的一段时间内,我可能不会继续更新博客啦,因为作者我要去学习 flutter 去啦~ 没有办法,总要保持前进呢。优秀的人还在努力,更何况自己并不聪明呢。哎~伤心