Android 註解系列之EventBus3“加速引擎“(五)

AndyJennifer發表於2019-10-22

前言

在上篇文章 Android 註解系列之 EventBus3 原理(四)中我們講解了 EventBus3 的內部原理,在該篇文章中我們將講解 EventBus3 中的 “加速引擎”---索引類。閱讀該篇文章我們能夠學到如下知識點。

  • EventBus3 索引類出現的原因
  • EventBus3 索引類的使用
  • EventBus3 索引類生成的過程
  • EventBus3 混淆注意事項

對 APT 技術不熟悉的小夥伴,可以檢視文章 Android-註解系列之APT工具(三)

前景回顧

Android 註解系列之 EventBus3 原理(四)中,我們特別指出在 EventBus3 中優化了 SubscriberMethodFinder 獲取類中包含 @Subscribe 註解的訂閱方法的流程。使其能在 EventBus.register() 方法呼叫之前就能知道相關訂閱事件的方法,這樣就減少了程式在執行期間使用反射遍歷獲取方法所帶來的時間消耗。優化點如下圖中 紅色虛線框 所示:

EventBus3優化.jpg

EventBus 作者 Markus Junginger 也給出了使用索引類前後 EventBus 的效率對比,如下圖所示:

eventbus3-registration-perf-nexus9m.png

從上圖中,我們可以使用索引類後,EventBus 的效率有著明顯的提升,而效率提升的背後,正是使用了 APT 技術所建立的索引類。那麼接下來我們就來看一看 EventBus3 中是如何結合 APT 技術來進行優化的。

關鍵程式碼

閱讀過 EventBus3 原始碼的小夥伴應該都知道,在 EventBus3 中獲取類中包含 @Subscribe 註解的訂閱方法有兩種方式。

  • 第一種:是直接在程式執行時反射獲取
  • 第二種:就是通過索引類。

而使用索引類的關鍵程式碼為 SubscriberMethodFinder 中的 getSubscriberInfo() 方法與 findUsingInfo() 方法 。 我們分別來看這兩個方法。

findUsingInfo 方法

    private List<SubscriberMethod> findUsingInfo(Class<?> subscriberClass) {
        FindState findState = prepareFindState();
        findState.initForSubscriber(subscriberClass);
        while (findState.clazz != null) {
            //?關鍵程式碼,從索引類中獲取 SubscriberInfo
            findState.subscriberInfo = getSubscriberInfo(findState);
            //方式1:如果 subscriberInfo 不為空,則從該物件中獲取 SubscriberMethod 物件
            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 {
                //方式2:如果 subscriberInfo 為空,那麼直接通過反射獲取
                findUsingReflectionInSingleClass(findState);
            }
            findState.moveToSuperclass();
        }
        return getMethodsAndRelease(findState);
    }
複製程式碼

我們能從該方法中獲得以下資訊:

  • EventBus3 中預設會呼叫 getSubscriberInfo() 方法去獲取 subscriberInfo 物件資訊。
  • 如果 subscriberInfo 不為空,則會從該物件中獲取 SubscriberMethod 陣列。
  • 如果 subscriberInfo 為空,那麼會直接通過反射去獲取 SubscriberMethod 集合資訊。

SubscriberMethod 類中含有 @Subscribe 註解的方法資訊封裝(優先順序,是否粘性,執行緒模式,訂閱的事件),以及當前方法的 Method 物件(java.lang.reflect 包下的物件)。

也就說 EventBus 是否通過反射獲取資訊,是由 getSubscriberInfo()方法來決定,那麼我們檢視該方法。

getSubscriberInfo 方法

    private SubscriberInfo getSubscriberInfo(FindState findState) {
        if (findState.subscriberInfo != null && findState.subscriberInfo.getSuperSubscriberInfo() != null) {
            SubscriberInfo superclassInfo = findState.subscriberInfo.getSuperSubscriberInfo();
            if (findState.clazz == superclassInfo.getSubscriberClass()) {
                return superclassInfo;
            }
        }
        //?這裡是EventBus3中優化的關鍵,索引類
        if (subscriberInfoIndexes != null) {
            for (SubscriberInfoIndex index : subscriberInfoIndexes) {
                SubscriberInfo info = index.getSubscriberInfo(findState.clazz);
                if (info != null) {
                    return info;
                }
            }
        }
        return null;
    }
複製程式碼

從程式碼邏輯中我們能得出,如果 subscriberInfoIndexes 集合不為空的話,那麼就會從 SubscriberInfoIndex(索引類) 中去獲取 SubscriberInfo 物件資訊。該方法的邏輯並不複雜,唯一的疑惑就是這個 SubscriberInfoIndex(索引類) 物件是從何而來的呢?

聰明的小夥伴們已經想到了。對!!!就是通過 APT 技術自動生成的類。那麼我們怎麼使用 EventBus3 中的索引類?以及 EventBus3 中是如何生成的索引類的呢? 不急不急,我們一個一個的解決問題。我們先來看看如何使用索引類。

EventBus中索引類的使用

如果需要使用 EventBus3 中的索引類,我們可以在 App 的 build.gradle 中新增如下配置:

android {
    defaultConfig {
        javaCompileOptions {
            annotationProcessorOptions {
                // 根據專案實際情況,指定索引類的名稱和包名
                arguments = [ eventBusIndex : 'com.eventbus.project.EventBusIndex’ ]
            }
        }
    }
}
dependencies {
    implementation 'org.greenrobot:eventbus:3.1.1’
    // 引入註解處理器
    annotationProcessor 'org.greenrobot:eventbus-annotation-processor:3.1.1’
}
複製程式碼

如果有小夥伴不熟悉 gradle 配置,可以檢視 AnnotationProcessorOptions

在上述配置中,我們需要注意如下幾點:

  • 如果你不使用索引類,那麼就沒有必要設定 annotationProcessorOptions 引數中的值。也沒有必要引入 EventBus 的註解處理器。
  • 如果要使用索引類,並且也引入了 EventBus 的註解處理器(eventbus-annotation-processor),但卻沒有設定 arguments 的話,編譯時就會報錯:No option eventBusIndex passed to annotation processor
  • 索引類的生成,需要我們對程式碼重新編譯。編譯成功後,其該類對應路徑為\ProjectName\app\build\generated\source\apt\你設定的包名

當我們的索引類生成後,我們還需要在初始化 EventBus 時應用我們生成的索引類,程式碼如下所示:

 EventBus.builder().addIndex(new EventBusIndex()).installDefaultEventBus();
複製程式碼

之所以要配置索引類,是因為我們需要將我們生成的索引類新增到 subscriberInfoIndexes 集合中,這樣我們才能從之前講解的 getSubscriberInfo()找到我們配置的索引類。addIndex() 程式碼如下所示:

 public EventBusBuilder addIndex(SubscriberInfoIndex index) {
        if (subscriberInfoIndexes == null) {
            subscriberInfoIndexes = new ArrayList<>();
        }
        //?這裡新增索引類到 subscriberInfoIndexes 集合中
        subscriberInfoIndexes.add(index);
        return this;
    }
複製程式碼

索引類實際使用分析

如果你已經配置好了索引類,那麼我們看下面的例子,這裡我配置的索引類為 EventBusIndex 對應包名為: 'com.eventbus.project' 。我在 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”);
    }
}

複製程式碼

自動生成的索引類,如下所示:

public class EventBusIndex implements SubscriberInfoIndex {
    private static final Map<Class<?>, SubscriberInfo> SUBSCRIBER_INDEX;

    static {
        SUBSCRIBER_INDEX = new HashMap<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;
        }
    }
}
複製程式碼

在生成的索引類中我們可以看出:

  • 生成的索引類中,維護了一個 key 為 訂閱物件 value 為 SimpleSubscriberInfo 的 HashMap。
  • SimpleSubscriberInfo 類中維護了當前訂閱者的 class 物件與 SubscriberMethodInfo[] 陣列
  • HashMap 中的資料新增是放到靜態程式碼塊中執行的。

SubscriberMethodInfo 類中含有 @Subscribe 註解的方法資訊封裝(優先順序,是否粘性,執行緒模式,訂閱的事件),以及當前方法的名稱

到現在,我們已經知道了我們索引類中的內容,那麼現在在回到 findUsingInfo() 方法:

    private List<SubscriberMethod> findUsingInfo(Class<?> subscriberClass) {
        //省略部分程式碼
        while (findState.clazz != null) {
            findState.subscriberInfo = getSubscriberInfo(findState);
            if (findState.subscriberInfo != null) {
                 //?關鍵程式碼,從索引類中獲取 SubscriberMethod
                SubscriberMethod[] array = findState.subscriberInfo.getSubscriberMethods();
                for (SubscriberMethod subscriberMethod : array) {
                    if (findState.checkAdd(subscriberMethod.method, subscriberMethod.eventType)) {
                        findState.subscriberMethods.add(subscriberMethod);
                    }
                }
            }
            //省略部分程式碼
        }
    }
複製程式碼

subscriberInfo 不為空時,會通過 getSubscriberMethods()方法,去獲取索引類中 SubscriberMethod[]陣列 資訊。因為索引類使用的是 SimpleSubscriberInfo 類,我們檢視該類中該方法的實現:

   @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;
    }
複製程式碼

觀察該程式碼,我們發現 SubscriberMethod 物件的建立是通過 createSubscriberMethod 方法建立的,我們繼續跟蹤。

  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);
        }
    }
複製程式碼

從上述程式碼中,我們可以看出 SubscriberMethod 中的 Method 物件,其實是呼叫訂閱者的 class 物件並使用 getDeclaredMethod()方法找到的。

現在為止我們已經基本瞭解,索引類之所以相比傳統的通過反射遍歷去獲取訂閱方法效率要更高。是因為在自動生成的索引類中,已經包含了相關訂閱者中的訂閱方法的名稱及註解資訊,那麼當 EventBus 註冊訂閱者時,就可以直接通過方法名稱拿到 Method 物件。這樣就減少了通過遍歷尋找方法的時間。

索引類的生成

那現在我們繼續學習 EventBus3 中是如何建立索引類的。索引類的建立是通過 APT 技術,如果你不瞭解這門技術,你可能需要檢視文章 Android-註解系列之APT工具(三)

APT(Annotation Processing Tool)是 javac 中提供的一種編譯時掃描和處理註解的工具,它會對原始碼檔案進行檢查,並找出其中的註解,然後根據使用者自定義的註解處理方法進行額外的處理。APT工具不僅能解析註解,還能根據註解生成其他的原始檔,最終將生成的新的原始檔與原來的原始檔共同編譯(注意:APT並不能對原始檔進行修改操作,只能生成新的檔案,例如在已有的類中新增方法

使用APT技術需要建立自己的註解處理器,在 EventBus 中也建立了自己的註解處理器,從其原始碼中我們就可以看出。

EventBus註解處理器.png

那下面,我們就直接檢視原始碼:

以下的程式碼,都出至於 EventBusAnnotationProcessor

檢視 EventBusAnnotationProcessor 中的 process() 方法:

process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv):註解處理器實際處理方法,一般要求子類實現該抽象方法,你可以在在這裡寫你的掃描與處理註解的程式碼,以及生成 Java 檔案。其中引數 RoundEnvironment ,可以讓你查詢出包含特定註解的被註解元素.

@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 {
            //步驟1:?獲取我們配置的索引類,
            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;
            }

            //省略部分程式碼

            //步驟2:?收集當前訂閱者資訊
            collectSubscribers(annotations, env, messager);
            //步驟3:?建立索引類檔案
            if (!methodsByClass.isEmpty()) {
                createInfoIndexFile(index);
            } else {
                messager.printMessage(Diagnostic.Kind.WARNING, "No @Subscribe annotations found”);
            }
            writerRoundDone = true;
        } catch (RuntimeException e) {
            //省略部分程式碼
        }
        return true;
    }
}
複製程式碼

該方法中主要邏輯為三個邏輯:

  • 步驟1:讀取我們之前在 APP 中的 build.gradle 設定的索引類對應的包名與類名。
  • 步驟2:讀取原始檔中的包含 @Subscribe 註解的方法。並將訂閱者與訂閱方法進行記錄在 methodsByClass Map 集合中。
  • 步驟3:根據讀取的索引類設定,通過 createInfoIndexFile() 方法開始建立索引類檔案。

因為宣告瞭@SupportedAnnotationTypes("org.greenrobot.eventbus.Subscribe") 在註解處理器上,那麼 APT 只會處理包含該註解的檔案。

我們接下來看看步驟2中的方法 collectSubscribers() 方法:

    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)) {
                        //獲取包含`@Subscribe`類的class物件
                        TypeElement classElement = (TypeElement) method.getEnclosingElement();
                        methodsByClass.putElement(classElement, method);
                    }
                } else {
                    messager.printMessage(Diagnostic.Kind.ERROR, "@Subscribe is only valid for methods", element);
                }
            }
        }
    }
複製程式碼

在註解處理過程中,我們需要掃描所有的Java原始檔,原始碼的每一個部分都是一個特定型別的Element,也就是說 Element 代表原始檔中的元素,例如包、類、欄位、方法等。

在上述方法中,annotations 為掃描到包含 @Subscribe 註解 的 Element 集合。其中 ExecutableElement 表示類或介面的方法、建構函式或初始化器(靜態或例項),因為我們可以通過 getEnclosingElement()方法,拿到當前 ExecutableElement 的最近的父 Element,那麼我們就能獲得當前的類的 element 物件了。那麼通過該方法,我們就能知道所有訂閱者與其對應的訂閱方法了。

我們繼續跟蹤檢視索引類檔案的建立:

    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<Class<?>, SubscriberInfo> SUBSCRIBER_INDEX;\n\n”);
            writer.write("    static {\n”);
            writer.write("        SUBSCRIBER_INDEX = new HashMap<Class<?>, SubscriberInfo>();\n\n”);
            //?這裡是關鍵的程式碼
            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
                }
            }
        }
    }
複製程式碼

在該方法中,通過 processingEnv.getFiler().createSourceFile(index) 拿到我們需要建立的索引類檔案物件,然後通過檔案IO流向該檔案中輸入索引類中需要的內容。在該方法中,最為主要的就是 writeIndexLines() 方法了。檢視該方法:

    private void writeIndexLines(BufferedWriter writer, String myPackage) throws IOException {
        for (TypeElement subscriberTypeElement : methodsByClass.keySet()) {
            if (classesToSkip.contains(subscriberTypeElement)) {
                continue;
            }
            //當前訂閱物件的class物件
            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);
                //?關鍵程式碼
                writeCreateSubscriberMethods(writer, methods, "new SubscriberMethodInfo", myPackage);
                writer.write("        }));\n\n”);
            } else {
                writer.write("        // Subscriber not visible to index: " + subscriberClass + "\n”);
            }
        }
    }
複製程式碼

在該方法中,會從 methodsByClass Map 中遍歷獲取我們之前的訂閱者,然後獲取其所有的訂閱方法,並書寫模板方法。其中關構造 SubscriberMethodInfo 程式碼的關鍵方法為 writeCreateSubscriberMethods(),跟蹤該方法:

 private void writeCreateSubscriberMethods(BufferedWriter writer, List<ExecutableElement> methods,
                                              String callPrefix, String myPackage) throws IOException {
        for (ExecutableElement method : methods) {
            //獲取當前方法上的引數
            List<? extends VariableElement> parameters = method.getParameters();
            TypeMirror paramType = getParamTypeMirror(parameters.get(0), null);
            //獲取第一個引數的型別
            TypeElement paramElement = (TypeElement) processingEnv.getTypeUtils().asElement(paramType);
            //獲取方法的名稱
            String methodName = method.getSimpleName().toString();
            //獲取訂閱的事件class型別字串資訊
            String eventClass = getClassString(paramElement, myPackage) + ".class”;
            //獲取方法上的註解資訊
            Subscribe subscribe = method.getAnnotation(Subscribe.class);
            List<String> parts = new ArrayList<>();
            parts.add(callPrefix + "(\"" + methodName + "\",”);
            String lineEnd = "),”;
            //設定優先順序,是否粘性,執行緒模式,訂閱事件class型別
            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.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 @Subscribe at “ +
                        method.getEnclosingElement().getSimpleName() + "." + methodName +
                        "(" + paramElement.getSimpleName() + ")”);
            }

        }
    }
複製程式碼

在該方法中,會獲取訂閱方法的引數資訊,並構建 SubscriberMethodInfo 資訊。這裡就不對該方法進行詳細的介紹了,大家可以根據程式碼中的註釋進行理解。

混淆相關

在使用 EventBus3 的時候,如果你的專案採用了混淆,需要注意 keep 以下類及方法。官方中已經給出了詳細的 keep 規則,如下所示:

-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);
}
複製程式碼

為什麼不能混淆註解

android在打包的時候,應用程式會進行程式碼優化,優化的過程就把註解給去掉了。為了在程式執行期間讀取到註解資訊,所以我們需要儲存註解資訊不被混淆。

為什麼不能混淆包含 @Subscribe 註解的方法

因為當我們在使用索引類時,獲取相關訂閱的方法是通過方法名稱獲取的,那麼當程式碼被混淆過後,訂閱者的方法名稱將會發生改變,比如原來訂閱方法名稱為onMessageEvent,混淆後有可能改為a,或b方法。這個時候是找不到相關的訂閱者的方法的 ,就會丟擲 Could not find subscriber method in + subscriberClass + Maybe a missing ProGuard rule? 的異常,所以在混淆的時候我們需要保留訂閱者所有包含 @Subscribe 註解的方法。

為什麼不能混淆列舉類中的靜態變數

如果我們沒有在混淆規則中新增如下語句:

-keep public enum org.greenrobot.eventbus.ThreadMode { public static *; }
複製程式碼

在執行程式的時候,會報java.lang.NoSuchFieldError: No static field POSTING。原因是因為在 SubscriberMethodFinderfindUsingReflection 方法中,在呼叫 Method.getAnnotation()時獲取 ThreadMode 這個 enum 失敗了。

我們都知道當我們宣告列舉類時,編譯器會為我們的列舉,自動生成一個繼承 java.lang.Enumfinal 類。如下所示:

//使用命令 javap ThreadMode.class
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 {};
}
複製程式碼

也就是說,我們在列舉中宣告的元素,其實最後對應的是類中的靜態公有的常量。

那麼在結合在沒有新增混淆規則時,程式所提示的錯誤資訊。我們可以確定當我們在註解中包含列舉型別的註解元素時且設定了預設值時。該預設值是通過列舉類的 class 物件.getField(String name) 去獲取的。因為只有該方法才會丟擲該異常。getField() 程式碼如下所示:

    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;
    }
複製程式碼

那麼也就說如果不新增上述的 keep 規則,就會導致我們編譯器自動生成的靜態常量名發生變化,又因為註解中的預設列舉值,是通過 getField(String name) 獲得的。所以就會出現找不到欄位的情況。

其實在很多情況下,我們需要新增 keep 規則,常常是因為程式碼中是直接拿混淆前的方法名稱或欄位名稱去直接尋找混淆後的方法與欄位名稱,我們只要在專案中注意這些情況,新增相應的 keep 規則,就可以避免因為程式碼被混淆而產生的異常啦。

最後

EventBus3 中的索引類及其相關內容到這裡就講完啦!我相應大家已經瞭解了索引類在效能優化上的重要作用。希望大家在後續使用EventBus3時,一定要使用索引類呦。在接下來的一段時間內,我可能不會繼續更新部落格啦,因為作者我要去學習 flutter 去啦~ 沒有辦法,總要保持前進呢。優秀的人還在努力,更何況自己並不聰明呢。哎~傷心