preface

In this article, we explained the inner workings of EventBus3. In this article, we will explain the “acceleration engine” in EventBus3, the index class. Reading the article, we can learn the following points.

  • The EventBus3 index class is present
  • Use of the EventBus3 index class
  • EventBus3 index class generation process
  • EventBus3 confuses caution

If you are not familiar with APT technology, you can check out the article Android- Annotations series APT tools (3).

Prospects for review

In Principle 4 of EventBus3 in the Android Annotations series, we specifically noted that in EventBus3 we optimized the process for SubscriberMethodFinder retrieving the Subscribe method that contains the @Subscribe annotation in the class. Making the method of the relevant subscription event known before the eventBus.register () method is called reduces the time consumed by the program using the reflection traversal fetch method at run time. The optimization points are shown in the red dotted box below:

EventBus author Markus Junginger also provides a comparison of EventBus efficiency before and after using index classes, as shown in the figure below:

In the figure above, we can see that the efficiency of EventBus has been significantly improved by using index classes. Behind this improvement is the index classes created by using APT technology. Let’s take a look at how APT optimization is implemented in EventBus3.

The key code

If you’ve read the source code for EventBus3, there are two ways to get the @subscribe annotation from a class in EventBus3.

  • The first is reflected directly while the program is running
  • The second is through index classes.

The key code to use the index class is the getSubscriberInfo() and findUsingInfo() methods in the SubscriberMethodFinder. Let’s look at these two methods separately.

FindUsingInfo method

    private List<SubscriberMethod> findUsingInfo(Class
        subscriberClass) {
        FindState findState = prepareFindState();
        findState.initForSubscriber(subscriberClass);
        while(findState.clazz ! =null) {
            //👇 key code to get SubscriberInfo from the index class
            findState.subscriberInfo = getSubscriberInfo(findState);
            // Method 1: Get the SubscriberMethod object from the subscriberInfo object if that object is not empty
            if(findState.subscriberInfo ! =null) {
                SubscriberMethod[] array = findState.subscriberInfo.getSubscriberMethods();
                for (SubscriberMethod subscriberMethod : array) {
                    if(findState.checkAdd(subscriberMethod.method, subscriberMethod.eventType)) { findState.subscriberMethods.add(subscriberMethod); }}}else {
                // Method 2: If subscriberInfo is empty, get it directly by reflection
                findUsingReflectionInSingleClass(findState);
            }
            findState.moveToSuperclass();
        }
        return getMethodsAndRelease(findState);
    }
Copy the code

We can get the following information from this method:

  • In EventBus3, the getSubscriberInfo() method is called by default to get the subscriberInfo object information.
  • If subscriberInfo is not empty, it is fetched from this objectSubscriberMethodThe array.
  • If subscriberInfo is empty, it goes through directlyreflectionTo obtainSubscriberMethodCollection information.

The SubscriberMethod class contains an encapsulation of Method information annotated @Subscribe (priority, stickiness, thread mode, subscribed event) and the Method object for the current Method (an object under the java.lang.Reflect package).

That is, it is the getSubscriberInfo () method that determines whether EventBus gets information by reflection, so we look at that method.

GetSubscriberInfo method

    private SubscriberInfo getSubscriberInfo(FindState findState) {
        if(findState.subscriberInfo ! =null&& findState.subscriberInfo.getSuperSubscriberInfo() ! =null) {
            SubscriberInfo superclassInfo = findState.subscriberInfo.getSuperSubscriberInfo();
            if (findState.clazz == superclassInfo.getSubscriberClass()) {
                returnsuperclassInfo; }}//👇 here is the key to optimization in EventBus3, the index class
        if(subscriberInfoIndexes ! =null) {
            for (SubscriberInfoIndex index : subscriberInfoIndexes) {
                SubscriberInfo info = index.getSubscriberInfo(findState.clazz);
                if(info ! =null) {
                    returninfo; }}}return null;
    }
Copy the code

From the code logic, we can conclude that if the subscriberInfoIndexes collection is not empty, the SubscriberInfo object information will be obtained from the SubscriberInfoIndex class. The logic of this method isn’t complicated, and the only puzzle is where the SubscriberInfoIndex object comes from.

Smart guys have figured it out. Right!!!!!! Is automatically generated through APT technology classes. So how do we use the index class in EventBus3? And how is the index class generated in EventBus3? No rush, no rush. We’ll take it one at a time. Let’s start by looking at how to use index classes.

Use of index classes in EventBus

If you want to use the index class in EventBus3, you can add the following configuration to your App’s build.gradle:

Android {defaultConfig {javaCompileOptions {annotationProcessorOptions {/ / project based on the actual situation, Arguments = [eventBusIndex: 'the eventbus. Project. Dependencies EventBusIndex']}}}} {implementation 'org. Greenrobot: eventbus: 3.1.1' / / into the annotation processor AnnotationProcessor 'org. Greenrobot: eventbus -- the annotation processor: 3.1.1}'Copy the code

If there is a friend not familiar with gradle configuration, can check AnnotationProcessorOptions

Note the following points during the configuration:

  • If you don’t use index classes, there is no need to set them upannotationProcessorOptionsThe value in the parameter. There is also no need to introduce EventBus’s annotation handler.
  • If you want to use the index class, and also introduced the EventBus annotation processors (EventBus – the annotation processor), but did not set the arguments, compile time would be an error:No option eventBusIndex passed to annotation processor.
  • The generation of the index class requires us to recompile the code. After the compilation is successful, the path of the class is\ProjectName\app\build\generated\source\apt\ The package name you set.

Once our index class is generated, we also need to apply our generated index class when initializing EventBus as follows:

 EventBus.builder().addIndex(new EventBusIndex()).installDefaultEventBus();
Copy the code

The index class is configured because we need to add the index class we generated to the subscriberInfoIndexes collection so that we can find our configured index class from the getSubscriberInfo () explained earlier. The addIndex() code looks like this:

 public EventBusBuilder addIndex(SubscriberInfoIndex index) {
        if (subscriberInfoIndexes == null) {
            subscriberInfoIndexes = new ArrayList<>();
        }
        //👇 Here add the index class to the subscriberInfoIndexes collection
        subscriberInfoIndexes.add(index);
        return this;
    }
Copy the code

Index classes actually use analysis

If you have already configured the index class, let’s look at the example below, where I configured the index class as EventBusIndex corresponding to the package named ‘com.eventbus.project’. I declare the following methods in EventBusDemo.java:

public class EventBusDemo { @Subscribe(threadMode = ThreadMode.MAIN) public void onMessageEventOne(MessageEvent event) { System. Out.println (" hello "); } @Subscribe(threadMode = ThreadMode.MAIN) public void onMessageEventTwo(MessageEvent event) { System. Out.println (" world "); }}Copy the code

Automatically generated index class, as shown below:

public class EventBusIndex implements SubscriberInfoIndex {
    private static finalMap<Class<? >, SubscriberInfo> SUBSCRIBER_INDEX;static {
        SUBSCRIBER_INDEX = newHashMap<Class<? >, SubscriberInfo>(); putIndex(new SimpleSubscriberInfo(EventBusDemo.class, true.new SubscriberMethodInfo[] {
            new SubscriberMethodInfo("onMessageEventOne", MessageEvent.class, ThreadMode.MAIN),
            new SubscriberMethodInfo("onMessageEventTwo", MessageEvent.class, ThreadMode.MAIN),
        }));

    }

    private static void putIndex(SubscriberInfo info) {
        SUBSCRIBER_INDEX.put(info.getSubscriberClass(), info);
    }

    @Override
    public SubscriberInfo getSubscriberInfo(Class
        subscriberClass) {
        SubscriberInfo info = SUBSCRIBER_INDEX.get(subscriberClass);
        if(info ! =null) {
            return info;
        } else {
            return null; }}}Copy the code

In the generated index class we can see:

  • In the generated index class, a key is maintained for the subscription object value isSimpleSubscriberInfoThe HashMap.
  • SimpleSubscriberInfoClass maintains the class object of the current subscriber andSubscriberMethodInfo [] array.
  • Adding data in a HashMap is done in static blocks of code.

The method information encapsulation (priority, stickiness, thread mode, subscribed event) with the @SUBSCRIBE annotation in the SubscriberMethodInfo class, as well as the name of the current method.

Now that we know what’s in our index class, let’s go back to the findUsingInfo() method:

    private List<SubscriberMethod> findUsingInfo(Class
        subscriberClass) {
        // Omit some code
        while(findState.clazz ! =null) {
            findState.subscriberInfo = getSubscriberInfo(findState);
            if(findState.subscriberInfo ! =null) {
                 //👇 key code to get the SubscriberMethod from the index class
                SubscriberMethod[] array = findState.subscriberInfo.getSubscriberMethods();
                for (SubscriberMethod subscriberMethod : array) {
                    if(findState.checkAdd(subscriberMethod.method, subscriberMethod.eventType)) { findState.subscriberMethods.add(subscriberMethod); }}}// Omit some code}}Copy the code

When subscriberInfo is not empty, the getSubscriberMethods() method is used to obtain the SubscriberMethod[] array information in the index class. Since the index class uses the SimpleSubscriberInfo class, we look at the implementation of this method in that class:

   @Override
    public synchronized SubscriberMethod[] getSubscriberMethods() {
        int length = methodInfos.length;
        SubscriberMethod[] methods = new SubscriberMethod[length];
        for (int i = 0; i < length; i++) {
            SubscriberMethodInfo info = methodInfos[i];
            methods[i] = createSubscriberMethod(info.methodName, info.eventType, info.threadMode,
                    info.priority, info.sticky);
        }
        return methods;
    }
Copy the code

Looking at the code, we see that the SubscriberMethod object is created using the createSubscriberMethod method, and we continue tracing.

  protected SubscriberMethod createSubscriberMethod(String methodName, Class<? > eventType, ThreadMode threadMode,int priority, boolean sticky) {
        try {
            Method method = subscriberClass.getDeclaredMethod(methodName, eventType);
            return new SubscriberMethod(method, eventType, threadMode, priority, sticky);
        } catch (NoSuchMethodException e) {
            throw new EventBusException("Could not find subscriber method in " + subscriberClass +
                    ". Maybe a missing ProGuard rule?", e); }}Copy the code

From the above code, we can see that the Method object in the SubscriberMethod is actually found by calling the subscriber’s class object and using the getDeclaredMethod () Method.

By now we have a basic understanding of why index classes are more efficient than traditional reflection traversal methods to get subscriptions. Because the auto-generated index class already contains the name and annotation information of the subscribed Method in the relevant subscriber, when EventBus registers the subscriber, it can get the Method object directly from the Method name. This reduces the time spent traversing to find the method.

Generation of index classes

Now let’s continue learning how to create index classes in EventBus3. Index classes are created using APT techniques. If you are not familiar with this technique, you may need to check out the APT tools in the Article Android- Annotations series.

APT(Annotation Processing Tool) is a Tool provided by JavAC to scan and process annotations at compile-time. It checks source code files and finds annotations, and then performs additional Processing according to user-defined Annotation Processing methods. APT not only parses annotations, but also generates other source files based on annotations, eventually compiling the new source file with the original source file (note: APT does not modify the source file, can only generate new files, such as adding methods to an existing class).

Using APT technology requires you to create your own annotation handler, which is also created in EventBus, as you can see from its source code.

Below, we will directly look at the source code:

The following code, as for EventBusAnnotationProcessor

View the process in a EventBusAnnotationProcessor () method:

process(Set
Annotations, RoundEnvironment roundEnv) : This is where you can write your code to scan and process annotations and generate Java files. The RoundEnvironment parameter allows you to query for annotated elements that contain specific annotations.

@ SupportedAnnotationTypes (" org. Greenrobot. Eventbus. Subscribe ") @ SupportedOptions (value = {" eventBusIndex." "Verbose"}) public class EventBusAnnotationProcessor extends AbstractProcessor {public static final String OPTION_EVENT_BUS_INDEX = "eventBusIndex"; @Override public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment env) { Messager messager = processingEnv.getMessager(); Try {// Step 1: 👇 get our configured index class, String index = processingenv.getoptions ().get(OPTION_EVENT_BUS_INDEX); if (index == null) { messager.printMessage(Diagnostic.Kind.ERROR, "No option "+ OPTION_EVENT_BUS_INDEX +" passed to annotation processor "); return false; } // Omit some code // Step 2:👇 Collect current subscriber information collectSubscribers(Annotations, env, messager); // Step 3: 👇 create index class file if (! methodsByClass.isEmpty()) { createInfoIndexFile(index); } else {messager.printmessage (diagno.kind. WARNING, "No @Subscribe Annotations found "); } writerRoundDone = true; } catch (RuntimeException e) {return true; }}Copy the code

There are three main logics in this method:

  • Step 1: Read the package name and class name corresponding to the index class we set in build.gradle in APP.
  • Step 2: Read the contains in the source file@SubscribeAnnotation method. And the subscriber and subscription method are recorded inmethodsByClassMap collection.
  • Step 3: According to the index class Settings read, passcreateInfoIndexFile()Method to create the index class file.

Because the statement @ SupportedAnnotationTypes (” org. Greenrobot. Eventbus. Subscribe “) on the annotation processor, so APT will only handle contains the annotations file.

Now let’s look at the method collectSubscribers() method in Step 2:

    private void collectSubscribers(Set<? extends TypeElement> annotations, RoundEnvironment env, Messager messager) {
        for (TypeElement annotation : annotations) {
            Set<? extends Element> elements = env.getElementsAnnotatedWith(annotation);
            for (Element element : elements) {
                if (element instanceof ExecutableElement) {
                    ExecutableElement method = (ExecutableElement) element;
                    if (checkHasNoErrors(method, messager)) {
                        // Get the class object containing the @subscribe classTypeElement classElement = (TypeElement) method.getEnclosingElement(); methodsByClass.putElement(classElement, method); }}else {
                    messager.printMessage(Diagnostic.Kind.ERROR, "@Subscribe is only valid for methods", element); }}}}Copy the code

During annotation processing, we need to scan all the Java source files. Each part of the source code is a specific type of Element, meaning that Element represents elements in the source file, such as packages, classes, fields, methods, and so on.

In the above method, Annotations are scanned to the Element collection that contains the @SUBSCRIBE annotation. ExecutableElement represents the method, constructor, or initializer (static or instance) of the class or interface, because we can take the nearest parent Element of the current ExecutableElement with the getEnclosingElement () method. Then we can get the element object of the current class. With this method, we know all the subscribers and their corresponding subscription methods.

Let’s keep track of the creation of the index class file:

private void createInfoIndexFile(String index) { BufferedWriter writer = null; try { JavaFileObject sourceFile = processingEnv.getFiler().createSourceFile(index); Int period = index. The 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<Class<? >, SubscriberInfo> SUBSCRIBER_INDEX; \ n \ n "); Writer. Write (" static {\ n "); writer.write(" SUBSCRIBER_INDEX = new HashMap<Class<? >, SubscriberInfo>(); \ n \ n "); //👇 here is the key code writeIndexLines(writer, myPackage); 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 } } } }Copy the code

In this method, we call processIngenv.getFiler ().createsourcefile (index) to get the index file object we need to create, and then stream the file IO to that file to enter the contents of the index class. The main one is the writeIndexLines() method. To view the method:

private void writeIndexLines(BufferedWriter writer, String myPackage) throws IOException { for (TypeElement subscriberTypeElement : methodsByClass.keySet()) { if (classesToSkip.contains(subscriberTypeElement)) { continue; } // The class object of the current subscribed object String subscriberClass = getClassString(subscriberTypeElement, myPackage); if (isVisible(myPackage, subscriberTypeElement)) { writeLine(writer, 2, "PutIndex (new SimpleSubscriberInfo(" + subscriberClass + ". Class,", "true,", "new SubscriberMethodInfo[] {"); List<ExecutableElement> methods = methodsByClass.get(subscriberTypeElement); / / key code 👇 writeCreateSubscriberMethods (writer, the methods, "new SubscriberMethodInfo myPackage); writer.write(" })); \ n \ n "); } else {writer.write(" // Subscriber not visible to index: "+ subscriberClass + "\n"); }}}Copy the code

In this method, we iterate through methodsByClass Map to get our previous subscribers, then get all of their subscription methods, and write the template methods. Closed structure SubscriberMethodInfo code key methods for writeCreateSubscriberMethods (), tracking the method:

private void writeCreateSubscriberMethods(BufferedWriter writer, List<ExecutableElement> methods, String callPrefix, String myPackage) throws IOException {for (ExecutableElement Method: methods) {// Get the parameters of the current method <? extends VariableElement> parameters = method.getParameters(); TypeMirror paramType = getParamTypeMirror(parameters.get(0), null); / / get the type of the first parameter TypeElement paramElement = (TypeElement) processingEnv. GetTypeUtils () asElement (paramType); String methodName = method.getSimplename ().toString(); String eventClass = getClassString(paramElement, myPackage) + ". Class "; Subscribe Subscribe = method.getannotation (Subscribe. Class); List<String> parts = new ArrayList<>(); Parts. The add (callPrefix + "(\" "+ methodName +" \ ", "); String lineEnd = "), "; // Set priority, stickiness, thread mode, subscribe event class type if (subscribe. Priority () == 0 &&! subscribe.sticky()) { if (subscribe.threadMode() == ThreadMode.POSTING) { parts.add(eventClass + lineEnd); } else {parts.add(eventClass + ", "); parts.add("ThreadMode." + subscribe.threadMode().name() + lineEnd); }} else {parts.add(eventClass + ", "); Parts. Add ("ThreadMode." + subscribe.threadmode ().name() + ", "); Parts. The add (subscribe. Priority () + ", "); parts.add(subscribe.sticky() + lineEnd); } writeLine(writer, 3, parts.toArray(new String[parts.size()])); if (verbose) { processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE, "Indexed @ the Subscribe at" + method. GetEnclosingElement () getSimpleName () + "" + methodName +" (" + ParamElement. GetSimpleName () + ") "); }}}Copy the code

In this method, the parameter information of the subscription method is obtained and the SubscriberMethodInfo information is built. This method is not explained in detail here, but you can follow the comments in the code to understand it.

Confusion related

Keep the following classes and methods to keep in mind when using EventBus3 if your project uses obfuscation. The official has given a detailed keep rule, as follows:

-keepattributes *Annotation*
-keepclassmembers class * {
    @org.greenrobot.eventbus.Subscribe <methods>;
}
-keep enum org.greenrobot.eventbus.ThreadMode { *; }

# Only required if you use AsyncExecutor
-keepclassmembers class * extends org.greenrobot.eventbus.util.ThrowableFailureEvent {
    <init>(java.lang.Throwable);
}
Copy the code

Why not confuse annotations

When Android is packaged, the application code is optimized, and the optimization process removes the annotations. In order to read the annotation information while the program is running, we need to save the annotation information from being confused.

Why can’t we confuse methods that contain @subscribe annotations

Because when we use the index class, the method to get the related subscription is obtained by the method name, so when the code is confused, the subscriber’s method name will change, for example, the original subscription method name is onMessageEvent, after confusion may be changed to a, or B method. Could not find subscriber method in + subscriberClass + Maybe a missing ProGuard rule? So in case of confusion we need to preserve all methods of the subscriber that contain the @subscribe annotation.

Why not confuse static variables in enumerated classes

If we didn’t add the following statement to the obfuscation rule:

-keep public enum org.greenrobot.eventbus.ThreadMode { public static *; }
Copy the code

At the time of running the program, will quote Java. Lang. NoSuchFieldError: No static field POSTING. The reason is that in the SubscriberMethodFinder findUsingReflection Method, the enum ThreadMode fails to be retrieved when method.getannotation () is called.

We all know that when we declare an enumeration class, the compiler automatically generates a final class that inherits java.lang.Enum for our enumeration. As follows:

// Use the javap threadmode.class command
public final class com.tian.auto.ThreadMode extends java.lang.Enum<com.tian.auto.ThreadMode> {
  public static final com.tian.auto.ThreadMode POSTING;
  public static final com.tian.auto.ThreadMode MAIN;
  public static final com.tian.auto.ThreadMode MAIN_ORDERED;
  public static final com.tian.auto.ThreadMode BACKGROUND;
  public static final com.tian.auto.ThreadMode ASYNC;
  public static com.tian.auto.ThreadMode[] values();
  public static com.tian.auto.ThreadMode valueOf(java.lang.String);
  static {};
}
Copy the code

That is, the elements we declare in the enumeration actually correspond to static public constants in the class.

Then combine the error message the program prompts when no obfuscation rule is added. We can determine when we include an annotation element of enumeration type in the annotation and set the default value. The default value is obtained by enumerating the class object. GetField (String name). Because only that method throws the exception. The getField() code looks like this:

public Field getField(String name) throws NoSuchFieldException { if (name == null) { throw new NullPointerException (" name = = null "); } Field result = getPublicFieldRecursive(name); if (result == null) { throw new NoSuchFieldException(name); } return result; }Copy the code

If we do not add the keep rule, we will change the static constant names automatically generated by our compiler, because the default enumeration value in the annotation is obtained by getField(String name). So you can’t find the field.

In fact, in many cases, we need to add keep rules, often because the code is the method of directly before get confused name or field names to direct method and field names after the confusion, we only pay attention to these things in the project, add the corresponding keep the rules, you can avoid because of the code are confused, resulting in abnormal.

The last

That’s it for index classes and their related content in EventBus3! I’ve already shown you how important index classes are for performance optimization. Make sure you use the index class when you use EventBus3. I may not continue to update my blog for some time to come, because I have to learn about flutter. There is no way but to keep flutter moving forward. Good people are still trying, let alone they are not smart. Ah ~ sad