EventBus索引加速探究

网友投稿 223 2022-09-21

EventBus索引加速探究

EventBus在3.0后推出了索引加速(自己的AnnotationProcessor实现),来优化EventBus对 ​​@Subscribe​​方法的查找速度。

EventBus的官方说明中有这么一段:

译为:我们墙裂建议你使用 EventBus注解器和它的订阅者索引,这样会避免一些因为使用反射而导致的问题。

这说明EventBus本身不会自己使用注解器,需要我们添加才会使用。由于之前一直没有在项目中使用,突然发现有这么个说法,就写这篇来研究一下。(顺便回顾一下EventBus源码)

0. 为什么使用索引加速?

EventBus在源码上的实现目标是什么:“找到所有的订阅方法” 所以它的优化方向是什么:“以最快的速度找到所有的订阅方法”

这里先介绍一下3.0之前 EventBus寻找订阅方法的做法

3.0版本之前通过反射,拿到订阅者类的所有方法,找到​​​onEvent​​开头的订阅方法

就是通过 反射+遍历 的方式。这样的做法未免太过暴力,如果在方法特别多的类进行了注册,性能会不高。

GreenDao其实在很早就想过使用 反射+注解 的方式来进行方法搜寻,但是由于注解只是作为一个标识的方法,在没有别的帮助的情况下,还是得使用 遍历来拿到所有被注解的方法。 它就是变相版的“onEvent开头”的做法。 甚至性能更低(因为算上了注解时消耗的时间空间),这就导致 GreenDao团队迟迟不肯使用这个注解+反射的原因。

直到他们使用了 AnnotationProcessor

3.0版本提供了​​​EventBusAnnotationProcessor​​,它可以在编译时生成代码,避免了在运行时通过反射遍历所有方法的弊端。 在性能方面,超越之前任何版本

厉害归厉害,但是我之前一直认为这个 注解器是直接封装在 EventBus里面的,所以默认它会使用这种方法,直到我学习了AnnotationProcessor的用法…

1. findSubscriberMethods方法

// EventBus.java List findSubscriberMethods(Class subscriberClass) { List subscriberMethods = METHOD_CACHE.get(subscriberClass); if (subscriberMethods != null) { return subscriberMethods; } if (ignoreGeneratedIndex) { // 1 subscriberMethods = findUsingReflection(subscriberClass); // 2 } else { subscriberMethods = findUsingInfo(subscriberClass); // 3 } if (subscriberMethods.isEmpty()) { throw new EventBusException("Subscriber " + subscriberClass + " and its super classes have no public methods with the @Subscribe annotation"); } else { METHOD_CACHE.put(subscriberClass, subscriberMethods); return subscriberMethods; } }

第一次注册的订阅者会走到注释1处进行判断。​​​ignoreGeneratedIndex​​​ 这个字段的意思为 :是否忽略索引生成 可以看出,这个字段用来判断订阅者是否使用索引加速。默认为false。

如果不使用索引加速,就会调用注释2: ​​findUsingRelfection()​​​,它通过反射得到订阅者类及其父类里所有方法,找到的被 ​​@Subscribe​​注解的方法。和第0节中所讲的方式是一样的。

如果使用索引加速,就会调用注释3: ​​findUsingInfo()​​,通过注解器方式获取。来看下这个方法:

// SubscriberMethodFinder.java private List findUsingInfo(Class subscriberClass) { FindState findState = prepareFindState(); // 1 findState.initForSubscriber(subscriberClass); // 2 while (findState.clazz != null) { findState.subscriberInfo = getSubscriberInfo(findState); // 3 if (findState.subscriberInfo != null) { // 4 SubscriberMethod[] array = findState.subscriberInfo.getSubscriberMethods(); for (SubscriberMethod subscriberMethod : array) { if (findState.checkAdd(subscriberMethod.method, subscriberMethod.eventType)) { findState.subscriberMethods.add(subscriberMethod); } } } else { findUsingReflectionInSingleClass(findState); // 5 } findState.moveToSuperclass(); // 6 } return getMethodsAndRelease(findState); }

注释1:从 ​​FindState​​​池中获取一个 FindState对象,所以它的内容是空的。为什么使用 FindState池而不是 new一个出来,是因为EventBus是一个单例模式,一个进程只有一个EventBus,如果为了每个订阅者都要new一个FindState,那么其EventBus的内存空间就会有许许多多的FindState,为了优化性能,就创建 一个池子来循环利用。​​FindState​​的作用是用来描述 订阅者和订阅方法的关联信息。

注释2:将 订阅者对象 和注释1处获取到的FindState对象绑定,代码是这样的:

void initForSubscriber(Class subscriberClass) { this.subscriberClass = clazz = subscriberClass; skipSuperClasses = false; subscriberInfo = null; // 这里 subscriberInfo置null }

注释3:通过 ​​getSubscriberInfo(findState)​​来获取这个 订阅者的subscriberInfo

注释4: 如果subscriberInfo存在,就从 subscriberInfo中获取 @Subscribe方法

注释5:如果subscriberInfo不存在,就调用 ​​findUsingReflectionInSingleClass(findState)​​​ 反射+暴力的方法寻找,就是一开始的​​findUsingReflection()​​

显然,注释3的获取的东西,可以验证我们有没有开启 索引加速。我们来看看这个方法:

// SubscriberMethodFiner.java private SubscriberInfo getSubscriberInfo(FindState findState) { if (findState.subscriberInfo != null && findState.subscriberInfo.getSuperSubscriberInfo() != null) { // 1 SubscriberInfo superclassInfo = findState.subscriberInfo.getSuperSubscriberInfo(); if (findState.clazz == superclassInfo.getSubscriberClass()) { return superclassInfo; } } if (subscriberInfoIndexes != null) { // 2 for (SubscriberInfoIndex index : subscriberInfoIndexes) { SubscriberInfo info = index.getSubscriberInfo(findState.clazz); if (info != null) { return info; } } } return null; }

注释1: 如果 findState.subcriberInfo 不为null,且其内容也不为null,就把那些东西取出来。 在上面 FindState的初始化,我们看到了会把它的 subcriberInfo置为null,所以如果是第一次执行,注释1的条件是会判断会false。就会往注释2走。

注释2:判断 ​​subscriberInfoIndexes​​是否为空,如果不为空,则遍历所有,找到一个和 订阅者相匹配的 SubscriberInfo返回回去。

那​​subscriberInfoIndexes​​​是什么?怎么来的呢? 首先它是一个 ​​​List​​​类型,它是一个列表,里面放的是 “订阅者索引”。 它在 EventBus的构造方法中被创建:

EventBus(EventBusBuilder builder) { .... subscriberMethodFinder = new SubscriberMethodFinder(builder.subscriberInfoIndexes, builder.strictMethodVerification, builder.ignoreGeneratedIndex); .... }

可以看出它是来自于 ​​EventBusBuilder​​,而在EventBusBuilder中, 这个对象又是怎么产生的呢,我通过源码,发现它只有一处地方用来操作这个列表:

// EventBusBuidler.java /** Adds an index generated by EventBus' annotation preprocessor. */ public EventBusBuilder addIndex(SubscriberInfoIndex index) { if (subscriberInfoIndexes == null) { subscriberInfoIndexes = new ArrayList<>(); } subscriberInfoIndexes.add(index); return this; }

通过 ​​addIndex()​​​将 ​​SubscriberInfoIndex​​​ 往这个列表里面加。再往上找,没有了,addIndex()没有出现在任意一处被调用的地方。 也就是说,这个方法,是外部调用的。

它的英文注释我翻译一下:通过EventBus的注解处理器添加一个索引 。

这就说明了这个方法,是由注解器产生的Java文件调用。这也说明了,如果我们不使用注解处理器,EventBus寻找 ​​@Subcriber​​的做法,永远是 反射 + 遍历。

2. 通过EventBusAnnotationProcessor生成Java文件

​​EventBusAnnotationProcessor​​ 需要引入,示例代码如下:

// build.gradleandroid { defaultConfig { javaCompileOptions { annotationProcessorOptions { arguments = [ eventBusIndex : 'com.example.myapp.MyEventBusIndex' ] } } }} dependencies { def eventbus_version = '3.2.0' implementation "org.greenrobot:eventbus:$eventbus_version" annotationProcessor "org.greenrobot:eventbus-annotation-processor:$eventbus_version"}

然后在项目中,写入 ​​@Subscribe​​方法:

public class MainActivity extends AppCompatActivity { @Subscribe(threadMode = ThreadMode.MAIN) public void onMainActivityEvent(MainActivityEvent event){ } ...}

来看下文件内容:

public class MyEventBusIndex implements SubscriberInfoIndex { private static final Map, SubscriberInfo> SUBSCRIBER_INDEX; static { SUBSCRIBER_INDEX = new HashMap, SubscriberInfo>(); // 1 putIndex(new SimpleSubscriberInfo(MainActivity.class, true, new SubscriberMethodInfo[] { new SubscriberMethodInfo("onMainActivityEvent", MainActivityEvent.class, ThreadMode.MAIN), })); // 2 } private static void putIndex(SubscriberInfo info) { SUBSCRIBER_INDEX.put(info.getSubscriberClass(), info); } @Override public SubscriberInfo getSubscriberInfo(Class subscriberClass) { // 3 SubscriberInfo info = SUBSCRIBER_INDEX.get(subscriberClass); if (info != null) { return info; } else { return null; } }}

注释1: 在静态代码块中, 创建一个 Map,key是所有的订阅者, value 是该订阅者所有的订阅方法

注释2:对每个订阅者,将其内部所有的 @Subscribe方法添加其 ​​SubscriberMethodInfo[]​​ 这个数组中,并封装到 SubscriberInfo中,put到注释1的Map中。

注释3:​​getSubscriberInfo()​​​用来拿到对应订阅者的信息,在第一节中的 ​​SubscriberMethodFiner.getSubscriberInfo()​​​里面,我们看到的正是调用了这个方法。 但是如果我们默认情况下使用 ​​​EventBus.getDefault().register()​​​是不会调用到这个方法的,因为默认方法并不会 赋值给​​subscriberInfoIndexes​​,所以我们要手动加入:

EventBus.builder().addIndex(new MyEventBusIndex()).installDefaultEventBus(); // 1EventBus.getDefault().register(this);

注释1:通过 ​​addIndex()​​​把生成出来的Java文件加入进去,它会给 ​​subscriberInfoIndexes​​赋值。这个方法只能使用一次,建议在Application的onCreate中使用。

到这里,索引的用法就结束了。通过使用索引,把搜索订阅方法的做法放在了编译时做,相比于运行时暴力搜索,性能相比可见一斑。官方给出了在Nexus5上性能图:

EventBus3.0在没有使用注解生成器的性能比2.x都低(我现在就是处在这个位置),但是使用索引之后,速度能快到究极独一档。所以没什么好说的,赶紧用了。

接下来我要分析一下EventBusAnnotationProcessor的源码,这部分不感兴趣的同学可以不看了。

3.EventBusAnnotationProcessor部分源码分析

3.1 process方法

AnnotationProcessor的关键地方都是在与 ​​process()​​​是怎么生成Java文件的,来看下它的 ​​process()​​:

// EventBusAnnotationProcessor.java public boolean process(Set annotations, RoundEnvironment env) { Messager messager = processingEnv.getMessager(); // 1 try { String index = processingEnv.getOptions().get(OPTION_EVENT_BUS_INDEX); // 2 ... verbose = Boolean.parseBoolean(processingEnv.getOptions().get(OPTION_VERBOSE)); // 3 int lastPeriod = index.lastIndexOf('.'); String indexPackage = lastPeriod != -1 ? index.substring(0, lastPeriod) : null; // 4 ... collectSubscribers(annotations, env, messager); // 5 checkForSubscribersToSkip(messager, indexPackage); // 6 if (!methodsByClass.isEmpty()) { createInfoIndexFile(index); // 7 } ... } ... }

​​process()​​ 仅留下比较关键的代码,其他非关键代码更多是容错相关。

注释1: 获取信使,它用来打印log,因为AnnotationProcessor组件是Java的,所以不能使用Android的Log工具来打印,它就起到这么一个作用。

注释2:​​OPTION_EVENT_BUS_INDEX​​​就是我们在 gradle文件中写的 : ​​[eventBusIndex : 'xxx']​​​的内容,通过注释2的方法,可以拿到当前项目EventBus的 ​​eventBusIndex​​。它的作用是用来创建索引文件。

注释3:拿到verbose值,它同样在 gralde中拿,但是我们导包时没有写出来,annotationProcessor的完整导包是:

{ annotationProcessorOptions { arguments = [ eventBusIndex : 'com.example.myapp.MyEventBusIndex' verbose : 'true'] } }

这个 verbose如果为true,那我们注释1的信使就会在 编译时打印编译这些代码的log,如果为false则不打印。所以这个字段并不重要,我们一般都会忽略。

注释4:根据注释2拿到的索引,生成一个包名,就是新生成的Java文件的所在包的包名。

注释5:粗略收集所有的订阅者信息,因为订阅者是带有 ​​@Subscribe​​注解的文件,所以 AnnotationProcessor能够扫描到。

注释6:检查注释5中的订阅者,如果他们不是public,或者说他们的内部包含的事件方法不是public,则去除掉这些订阅者。

注释7:创建索引文件

上面最终要的方法是注释5、注释7。

先来看下注释5。

3.2 collectSubscribers方法

// EventBusAnnotationProcessor.java private final ListMap methodsByClass = new ListMap<>(); private void collectSubscribers(Set annotations, RoundEnvironment env, Messager messager) { // 1 for (TypeElement annotation : annotations) { Set elements = env.getElementsAnnotatedWith(annotation); // 2 for (Element element : elements) { // 3 if (element instanceof ExecutableElement) { ExecutableElement method = (ExecutableElement) element; // 4 if (checkHasNoErrors(method, messager)) { TypeElement classElement = (TypeElement) method.getEnclosingElement(); // 5 methodsByClass.putElement(classElement, method);// 6 } } else { messager.printMessage(Diagnostic.Kind.ERROR, "@Subscribe is only valid for methods", element); } } } }

注释1: 遍历所有的 annotations,annotations是被AnnotationProcessor扫描项目扫出来的,这个Set只有一个元素,那就是 Subscribe。

注释2:拿到所有被 @Subscribe 标记的 元素(也就是方法)

注释3:遍历所有的 注释2中获取的元素

注释4:把 元素(Element) 转换成 方法(ExecutableElement)

注释5:​​getEnclosingElement()​​​可以拿到方法的外部类,比如说我们例子的 ​​onMainActivityEvent​​,它的外部信息,就是 MainAcitivty

注释6:把 订阅方法和其所在的类(订阅者)放到ListMap中。

​​collectSubscribers()​​这个方法最终可以整理出一个 Map,key是所有的订阅者,value是这个订阅者里面的订阅方法。

接下来就是把上面的信息写成一个文件。就是 ​​createInfoIndexFile()​​

3.3 createInfoIndexFile方法

它就是生成文件的方法:

private void createInfoIndexFile(String index) { BufferedWriter writer = null; try { JavaFileObject sourceFile = processingEnv.getFiler().createSourceFile(index); int period = index.lastIndexOf('.'); String myPackage = period > 0 ? index.substring(0, period) : null; String clazz = index.substring(period + 1); writer = new BufferedWriter(sourceFile.openWriter()); if (myPackage != null) { writer.write("package " + myPackage + ";\n\n"); } writer.write("import org.greenrobot.eventbus.meta.SimpleSubscriberInfo;\n"); writer.write("import org.greenrobot.eventbus.meta.SubscriberMethodInfo;\n"); writer.write("import org.greenrobot.eventbus.meta.SubscriberInfo;\n"); writer.write("import org.greenrobot.eventbus.meta.SubscriberInfoIndex;\n\n"); writer.write("import org.greenrobot.eventbus.ThreadMode;\n\n"); writer.write("import java.util.HashMap;\n"); writer.write("import java.util.Map;\n\n"); writer.write("/** This class is generated by EventBus, do not edit. */\n"); writer.write("public class " + clazz + " implements SubscriberInfoIndex {\n"); writer.write(" private static final Map, SubscriberInfo> SUBSCRIBER_INDEX;\n\n"); writer.write(" static {\n"); writer.write(" SUBSCRIBER_INDEX = new HashMap, SubscriberInfo>();\n\n"); writeIndexLines(writer, myPackage); // 1 writer.write(" }\n\n"); writer.write(" private static void putIndex(SubscriberInfo info) {\n"); writer.write(" SUBSCRIBER_INDEX.put(info.getSubscriberClass(), info);\n"); writer.write(" }\n\n"); writer.write(" @Override\n"); writer.write(" public SubscriberInfo getSubscriberInfo(Class subscriberClass) {\n"); writer.write(" SubscriberInfo info = SUBSCRIBER_INDEX.get(subscriberClass);\n"); writer.write(" if (info != null) {\n"); writer.write(" return info;\n"); writer.write(" } else {\n"); writer.write(" return null;\n"); writer.write(" }\n"); writer.write(" }\n"); writer.write("}\n"); } catch (IOException e) { throw new RuntimeException("Could not write source for " + index, e); } finally { if (writer != null) { try { writer.close(); } catch (IOException e) { //Silent } } } } private void writeIndexLines(BufferedWriter writer, String myPackage) throws IOException { for (TypeElement subscriberTypeElement : methodsByClass.keySet()) { // 2 if (classesToSkip.contains(subscriberTypeElement)) { continue; } String subscriberClass = getClassString(subscriberTypeElement, myPackage); if (isVisible(myPackage, subscriberTypeElement)) { writeLine(writer, 2, "putIndex(new SimpleSubscriberInfo(" + subscriberClass + ".class,", "true,", "new SubscriberMethodInfo[] {"); // 3 List methods = methodsByClass.get(subscriberTypeElement); writeCreateSubscriberMethods(writer, methods, "new SubscriberMethodInfo", myPackage); writer.write(" }));\n\n"); // 4 } else { writer.write(" // Subscriber not visible to index: " + subscriberClass + "\n"); } } }

上面这些没啥技术含量,​​createInfoIndexFile()​​首先写好了所有的格式,接着在static代码块中,调用了注释1的代码

注释1:调用​​writeIndexLines()​​写入 订阅者内容

注释2:遍历methodsByClass,这个是我们 3.2节中整理出来的订阅者Map,就是遍历所有的订阅者

注释3、4 将订阅者、订阅者信息写入到 BufferWriter中。

就这样,一个编译时产生的Java文件就被写出来了。它帮助了EventBus更好的提升了性能。关于AnnotationProcessor的用法我们也可以多借鉴使用,它更多是和 注解一起配合,提高程序效率。

版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。

上一篇:Jetpack学习之 Lifecycle
下一篇:到此一游|去探险吧,乡村骑行、采摘草药、住进塞尔维亚传统民居!
相关文章

 发表评论

暂时没有评论,来抢沙发吧~