100行代碼拆解EventBus核心邏輯(三)

  • 2019 年 12 月 17 日
  • 筆記

關於我 一個有思想的程序猿,終身學習實踐者,目前在一個創業團隊任team lead,技術棧涉及Android、Python、Java和Go,這個也是我們團隊的主要技術棧。 Github:https://github.com/hylinux1024 微信公眾號:終身開發者(angrycode)

在前文的講解中對 EventBus 的實現邏輯有了大概的理解之後,我們知道 Java 解析註解可以在運行時解析也可以在編譯期間解析。由於運行時解析是通過反射來獲取註解標記的類、方法、屬性等對象,它的性能要受到反射的影響。因此在一些基礎組件中更常見的做法是使用註解解析器技術,像 DaggerbutterknifeARouter 以及本文所接觸的 EventBus等框架庫都是使用到了註解解析器的技術。接下來我們來實現一個註解解析器。(本文代碼有點多)

項目結構

首先我們需要把項目結構改造一下

# 項目結構省略了部分文件展示  ├── annotation              # 註解等元數據定義  ├── annotationProcessor     # 註解解析以及代碼生成  ├── app                     # 客戶端使用入口  ├── easybuslib              # 核心接口  ├── local.properties  └── settings.gradle

app 同級的目錄增加了 annotationannotationProcessoreasybuslib。其中創建 annotationannotationProcessor 這兩個項目時一定要選擇 java library。前者主要是用於定義註解和封裝一些基礎數據結構,後者是用於解析註解。注意 annotationProcessor 在項目使用時,並不會打包到 app 中,它只會在編譯期間對註解進行解析處理。easybuslibandroid library

它們之間的關係為

# 符號 「->」 表示庫依賴  # 符號 「=>」 apt 依賴,並不會打包到 app 中  app -> easybuslib -> annotation  app => annotationProcessor  annotationProcessor -> annotation
annotation

annotation 是一個純粹的 java 項目,主要定義了註解 EasySubscribeSubscriberMethodSubscription 這個是 EasyBus 會直接使用到的類,而在 meta 包中定義了註解解析器需要使用到的數據結構。這個包結構分工是很明確的。

# annotation 主要的項目結構  └── src/main/java      └── com.gitlab.annotation          ├── EasySubscribe.java          ├── SubscriberMethod.java          ├── Subscription.java          └── meta              ├── SubscriberInfo.java              ├── SubscriberInfoIndex.java              └── SubscriberMethodInfo.java

在這個庫中實現自定義的註解

annotationProcessor
# annotationProcessor 主要的項目結構  └── src/main/java      └── com.gitlab.annotationprocessor          └── EasyBusAnnotationProcessor.java          └── resources/META-INF.services              └── javax.annotation.processing.Processor

這個只有一個 java 類和一個配置 Processor 的文件。解析註解生成 java 代碼的邏輯就在 EasyBusAnnotationProcessor 裏面。

easybuslib
# easybuslib 主要項目結構  └── src      └── main/java          └── com.gitlab.easybuslib              ├── EasyBus.java              └── Logger.java              └── res

這裡封裝了 EasyBus 主要接口,其邏輯在前面已經解釋過了。不過今天也會對它進行改造使它支持編譯期間解析得到的訂閱者的 onEvent 方法(不是必需以 onEvent 開頭,本文為了表達方便而使用)。

項目結構改造完成之後,接下來我們自上而下對註解解析器進行解讀和實現。

改造EasyBus

定義註解

定義註解在前面已經解讀過,這裡直接貼出代碼

EasySubscribe.java

/**   * 自定義註解   * 指定該註解修飾方法   * 由於我們使用編譯期間處理註解,所以指定其生命周期為只保留在源碼文件中   */  @Retention(RetentionPolicy.SOURCE)  @Target(ElementType.METHOD)  public @interface EasySubscribe {  }
添加索引列表

修改 EasyBus 中的註冊邏輯,添加由註解解析器生成的索引列表,並從索引列表中獲取到訂閱者被 @EasySubscribe 標記的方法。

SubscriberInfoIndex.java

/**   * 訂閱者的索引接口   * 通過Class獲取到該Class下定義的被標記的 @EasySubscribe 方法   */  public interface SubscriberInfoIndex {      SubscriberInfo getSubscriberInfo(Class<?> subscriberClass);  }

這個接口非常重要,我們使用註解解析器生成的類將繼承於這個接口,這樣我們在 EasyBus 中就依賴於該接口,而接口的實現交給註解解析器。

修改後的 EasyBus

public class EasyBus {        //省略部分代碼...      /**       * 編譯期間生成訂閱者索引,通過訂閱者 Class 類獲取到 @EasySubscribe 的方法       */      private List<SubscriberInfoIndex> subscriberInfoIndexList;        //省略部分代碼...        /**       * 添加訂閱者索引       *       * @param subscriberInfoIndex       */      public void addIndex(SubscriberInfoIndex subscriberInfoIndex) {          if (subscriberInfoIndexList == null) {              subscriberInfoIndexList = new ArrayList<>();          }          subscriberInfoIndexList.add(subscriberInfoIndex);      }        public void register(Object subscriber) {          Class<?> subscriberClass = subscriber.getClass();          List<SubscriberMethod> subscriberMethods = new ArrayList<>();          //使用反射獲取 onEvent 方法          if (subscriberInfoIndexList == null) {              Method[] methods = subscriberClass.getDeclaredMethods();              for (Method method : methods) {                  Class<?>[] parameterTypes = method.getParameterTypes();                  if (parameterTypes.length != 1) {                      continue;                  }                  // 這裡可以修改成使用反射獲取,這樣就不需要求方法以 onEvent 開頭                  if (method.getName().startsWith("onEvent")) {                      subscriberMethods.add(new SubscriberMethod(method, parameterTypes[0]));                  }              }          } else {              //注意這裡!!!              //使用註解解析器獲取 onEvent 方法              subscriberMethods = findSubscriberMethods(subscriberClass);          }          synchronized (this) {              for (SubscriberMethod method : subscriberMethods) {                  subscribe(subscriber, method);              }          }      }        /**       * 從索引中獲取訂閱者方法信息       *       * @param subscriberClass       * @return       */      private List<SubscriberMethod> findSubscriberMethods(Class<?> subscriberClass) {          List<SubscriberMethod> subscriberMethods = new ArrayList<>();          for (SubscriberInfoIndex subscriberIndex : subscriberInfoIndexList) {              SubscriberInfo subscriberInfo = subscriberIndex.getSubscriberInfo(subscriberClass);              List<SubscriberMethod> methodList = Arrays.asList(subscriberInfo.getSubscriberMethods());              subscriberMethods.addAll(methodList);          }          return subscriberMethods;      }      // 省略部分代碼...  }

主要對 register 方法進行了改造,當 subscriberInfoIndexList 不為空時,就從索引列表中查詢訂閱者信息。findSubscriberMethods() 遍歷索引列表並執行 subscriberIndex.getSubscriberInfo(subscriberClass) 方法得到訂閱者的信息。那麼 subscriberIndex 具體是怎麼實現的呢?

打開 app/HomeActivity

看到以下代碼

EasyBus.getInstance().addIndex(new MyEventBusIndex());

通過 addIndex() 方法將 MyEventBusIndex 實例添加到索引列表中。接下來我們看看其內部到底有何乾坤。

MyEventBusIndex.java這個類是由註解解析器生成的

/** This class is generated by EasyBus, do not edit. */  public class MyEventBusIndex implements SubscriberInfoIndex {      private static final Map<Class<?>, SubscriberInfo> SUBSCRIBER_INDEX;        static {          SUBSCRIBER_INDEX = new HashMap<Class<?>, SubscriberInfo>();            putIndex(new SubscriberInfo(com.github.easybus.demo.HomeActivity.class, new SubscriberMethodInfo[] {              new SubscriberMethodInfo("onUpdateMessage", com.github.easybus.demo.MessageEvent.class),              new SubscriberMethodInfo("onEventNotify", com.github.easybus.demo.MessageEvent.class),          }));        }        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;          }      }  }

它代碼邏輯很簡單,首先定義一個靜態 Map 變量 SUBSCRIBER_INDEX,它的 keyClass<?> 對象, valueSubscriberInfo 對象。然後 在一個靜態的代碼塊中將訂閱者的方法名稱和參數類型封裝成 SubscriberInfo 後添加到這個 Map 中。

SubscriberInfo.java

/**   * 訂閱者信息   * 主要是從註解中解析出Class以及通知方法(即被@EasySubscribe標記的方法)   */  public class SubscriberInfo {      private Class subscriberClass;      private SubscriberMethodInfo[] subscriberMethodInfos;        public SubscriberInfo(Class subscriberClass, SubscriberMethodInfo[] subscriberMethods) {          this.subscriberClass = subscriberClass;          this.subscriberMethodInfos = subscriberMethods;      }        //省略代碼...        public synchronized SubscriberMethod[] getSubscriberMethods() {          int length = subscriberMethodInfos.length;            SubscriberMethod[] methods = new SubscriberMethod[length];          for (int i = 0; i < length; i++) {              SubscriberMethodInfo info = subscriberMethodInfos[i];              SubscriberMethod method = createSubscribeMethod(info);              if (method != null) {                  methods[i] = method;              }          }          return methods;      }        private SubscriberMethod createSubscribeMethod(SubscriberMethodInfo info) {          try {              Method method = subscriberClass.getDeclaredMethod(info.getMethodName(), info.getEventType());              return new SubscriberMethod(method, info.getEventType());          } catch (NoSuchMethodException e) {              e.printStackTrace();          }          return null;      }  }

SubscriberMethodInfo.java

/**   * 用於編譯期間生成的訂閱者信息   */  public class SubscriberMethodInfo {      private final String methodName;      private final Class<?> eventType;        public SubscriberMethodInfo(String methodName, Class<?> eventType) {          this.methodName = methodName;          this.eventType = eventType;      }      // 省略代碼...  }

SubscriberInfoSubscriberMethodInfo 都是元數據類,主要是由生成的 MyEventBusIndex 類使用

如何生成代碼呢?

註解解析器

我們重點看 annotationProcessor 這個項目

首先配置 build.gradle

// annotationProcessor 工程庫必須使用 java 工程  // 不要使用 android lib 工程  // 本工程只會生成輔助代碼,不會打包到 apk 中  apply plugin: 'java-library'    dependencies {      implementation fileTree(dir: 'libs', include: ['*.jar'])      implementation 'com.squareup:javapoet:1.11.1'      implementation project(':annotation')  }    sourceCompatibility = "7"  targetCompatibility = "7"

添加 javapoet 依賴,這個框架幫助我們生成代碼(注意只能生成新代碼,而不能修改現有代碼哦)

然後繼承 AbstractProcessor

// 可以使用註解指定要解析的自定義註解以及Java版本號  // 也可以重寫 AbstractProcessor 中的方法達到類似的目的  // @SupportedAnnotationTypes({"com.gitlab.annotation.EasySubscribe"})  // @SupportedSourceVersion(SourceVersion.RELEASE_8)  public class EasyBusAnnotationProcessor extends AbstractProcessor {      @Override      public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {          collectSubscribers(set, roundEnvironment, messager);          return true;      }        // 省略代碼...        @Override      public SourceVersion getSupportedSourceVersion() {          return SourceVersion.latestSupported();      }        @Override      public Set<String> getSupportedAnnotationTypes() {          Set<String> annotations = new LinkedHashSet<>();  //        annotations.add("com.gitlab.annotation.EasySubscribe");          //指定要解析的註解          annotations.add(EasySubscribe.class.getCanonicalName());          return annotations;      }  }

需要實現核心的幾個方法

  • init 初始化方法
  • process 處理註解的核心方法
  • getSupportedSourceVersion 指定 Java 版本,一般使用 SourceVersion.latestSupported()
  • getSupportedAnnotationTypes 指定要解析的註解,有一個或多個註解,將其添加到 set中,並返回。

我們重點關注 process 方法。這裡有兩個參數,一個是 TypeElement 類型的 setRoundEnvironment 變量。其中 TypeElementElement 的子類。而 Element 是對包、類、接口、(構造)方法、屬性、參數等對象的抽象,可以結合以下對應關係進行理解。

package com.example;            // PackageElement    public class Foo {                // TypeElement        private int a;              // VariableElement      private Foo other;          // VariableElement        public Foo () {}            // ExecuteableElement        public void setA (          // ExecuteableElement                       int newA   // TypeElement                       ) {}  }

RoundEnvironment 是一個接口,是對上下文信息的抽象。

我們回到 process() 方法,它在編譯時會被執行,此時會將被註解標記的類、方法等信息傳遞過來

process() 方法會執行 collectSubscribers() 方法(此方法是從 EventBus 里中 copy 過來的)

private void collectSubscribers(Set<? extends TypeElement> annotations, RoundEnvironment env, Messager messager) {          // 遍歷要解析的註解          for (TypeElement annotation : annotations) {              messager.printMessage(Diagnostic.Kind.NOTE, "annotation:" + annotation.getSimpleName());              // 獲取被註解標記的對象              Set<? extends Element> elements = env.getElementsAnnotatedWith(annotation);              // Element 是接口,是對包括:包名、類、接口、方法、構造方法等的抽象              for (Element element : elements) {                  // 自定義註解 EasySubscribe 是作用在方法上的                  // 所以檢查一下是否是 ExecutableElement 對象                  // 它可以表示方法以及構造方法                  if (element instanceof ExecutableElement) {                      ExecutableElement method = (ExecutableElement) element;                      if (checkHasNoErrors(method, messager)) {                          // 獲取到這個被自定義註解的標記的方法所在類                          TypeElement classElement = (TypeElement) method.getEnclosingElement();                          List<ExecutableElement> list = methodsByClass.get(classElement);                          if (list == null) {                              list = new ArrayList<>();                          }                          list.add(method);                          methodsByClass.put(classElement, list);                      }                  } else {                      messager.printMessage(Diagnostic.Kind.ERROR, "@EasySubscribe is only valid for methods", element);                  }              }          }            if (!writeDone && !methodsByClass.isEmpty()) {              createInfoIndexFile("com.github.easybus.MyEventBusIndex");              writeDone = true;          } else {              messager.printMessage(Diagnostic.Kind.WARNING, "No @EasySubscribe annotations found");          }      }

Messager 對象可以用於輸入打印信息。 annotations 集合是所有待解析的註解,如果你定義了兩個註解,並在 getSupportedAnnotationTypes 中返回了,那麼這裡就是兩個需要解析的註解。 遍歷註解集合,並使用 RoundEnvironment 獲取到被註解標記的 Element,由於 EasySubscribe 是作用在方法上,所以我們主要關注 ExecutableElement 就可以了。 然後再通過 ExecutableElement.getEnclosingElement() 方法獲取方法所在的類對象 Class 信息。 最後將其保存在 key 為代表 ClassTypeElementvalue 為代表方法列表的 Map 對象 methodsByClass 中。 這樣就將類信息 Class 與被 @EasySubscribe 標記的方法列表對應起來了。這樣就為接下來的生成代碼邏輯作好了鋪墊。 有了 methodsByClass 接下來就是生成代碼的邏輯了。

代碼生成

代碼生成的邏輯在 createInfoIndexFile() 方法中,它有個參數 index,用來指定生成文件的包和類名的。(在 EventBus 中這裡是在 gradle 中配置的,本文為了展示核心流程省略了) 由於 process() 方法會被執行多次,所以這裡使用一個變量 writeDone 來判斷是否已經生成過代碼了,避免重複執行。

private void createInfoIndexFile(String index) {          BufferedWriter writer = null;          try {              JavaFileObject sourceFile = filer.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 + ";nn");              }              writer.write("import com.gitlab.annotation.meta.SubscriberInfoIndex;n");              writer.write("import com.gitlab.annotation.meta.SubscriberInfo;n");              writer.write("import com.gitlab.annotation.meta.SubscriberMethodInfo;n");              writer.write("import java.util.HashMap;n");              writer.write("import java.util.Map;nn");              writer.write("/** This class is generated by EasyBus, do not edit. */n");              writer.write("public class " + clazz + " implements SubscriberInfoIndex {n");              writer.write("    private static final Map<Class<?>, SubscriberInfo> SUBSCRIBER_INDEX;nn");              writer.write("    static {n");              writer.write("        SUBSCRIBER_INDEX = new HashMap<Class<?>, SubscriberInfo>();nn");              writeIndexLines(writer, myPackage);              writer.write("    }nn");              writer.write("    private static void putIndex(SubscriberInfo info) {n");              writer.write("        SUBSCRIBER_INDEX.put(info.getSubscriberClass(), info);n");              writer.write("    }nn");              writer.write("    @Overriden");              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) {              e.printStackTrace();              throw new RuntimeException("Could not write source for " + index, e);          } finally {              if (writer != null) {                  try {                      writer.close();                  } catch (IOException e) {                      //Silent                      e.printStackTrace();                  }              }          }      }

如果你還對前面的 MyEventBusIndex.java 的內容還有印象的話,這裡的邏輯還是比較好理解的,主要是使用 javapoet 中的接口生成代碼。具體就不再贅述了,閱讀代碼還是比較清晰的,接下來看看如何調試。

如何調試

由於代碼是在編譯期間執行的,如果你是剛開始接觸註解解析器的編碼,不能調試將是非常痛苦的過程。

要調試註解解析器需要做以下配置

1、首先在項目的根目錄下 gradle.properties 添加以下配置

org.gradle.jvmargs=-Xmx1536m -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005

2、然後點擊 EditConfigurations 配置 remote 填寫名稱,例如 processorDebug 後保存。

3、選擇 processorDebug

4、添加斷點後 RebuildProject

現在就可以對註解解析器進行調試了

總結

註解解析器的實現邏輯其實不是很複雜,主要有以下幾步:

  • 定義註解
  • 繼承 AbstractProcessor 解析註解
  • 使用 javapoet 生成代碼
  • 調試

面對一個新技術首先要掌握它的使用方法,然後了解其內部實現原理,最後自己動手實踐。這樣一個流程下來基本上對一個技術的理解是比較深刻的了。註解解析器作為很多基礎組件實現的通用技術,掌握它對實現基礎框架以及理解很多開源框架是很有幫助的。

本文的源碼

  • https://github.com/hylinux1024/EasyBus