100行程式碼拆解EventBus核心邏輯(三)
- 2019 年 12 月 17 日
- 筆記
關於我 一個有思想的程式猿,終身學習實踐者,目前在一個創業團隊任team lead,技術棧涉及Android、Python、Java和Go,這個也是我們團隊的主要技術棧。 Github:https://github.com/hylinux1024 微信公眾號:終身開發者(angrycode)
在前文的講解中對 EventBus
的實現邏輯有了大概的理解之後,我們知道 Java
解析註解可以在運行時解析也可以在編譯期間解析。由於運行時解析是通過反射來獲取註解標記的類、方法、屬性等對象,它的性能要受到反射的影響。因此在一些基礎組件中更常見的做法是使用註解解析器技術,像 Dagger
、 butterknife
、 ARouter
以及本文所接觸的 EventBus
等框架庫都是使用到了註解解析器的技術。接下來我們來實現一個註解解析器。(本文程式碼有點多)
項目結構
首先我們需要把項目結構改造一下
# 項目結構省略了部分文件展示 ├── annotation # 註解等元數據定義 ├── annotationProcessor # 註解解析以及程式碼生成 ├── app # 客戶端使用入口 ├── easybuslib # 核心介面 ├── local.properties └── settings.gradle
與 app
同級的目錄增加了 annotation
、 annotationProcessor
和 easybuslib
。其中創建 annotation
和 annotationProcessor
這兩個項目時一定要選擇 java library
。前者主要是用於定義註解和封裝一些基礎數據結構,後者是用於解析註解。注意 annotationProcessor
在項目使用時,並不會打包到 app
中,它只會在編譯期間對註解進行解析處理。easybuslib
是 android library
。
它們之間的關係為
# 符號 「->」 表示庫依賴 # 符號 「=>」 apt 依賴,並不會打包到 app 中 app -> easybuslib -> annotation app => annotationProcessor annotationProcessor -> annotation
annotation
annotation
是一個純粹的 java
項目,主要定義了註解 EasySubscribe
、 SubscriberMethod
和 Subscription
這個是 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
,它的 key
是 Class<?>
對象, value
是 SubscriberInfo
對象。然後 在一個靜態的程式碼塊中將訂閱者的方法名稱和參數類型封裝成 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; } // 省略程式碼... }
SubscriberInfo
與 SubscriberMethodInfo
都是元數據類,主要是由生成的 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
類型的 set
和 RoundEnvironment
變數。其中 TypeElement
是 Element
的子類。而 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
為代表 Class
的 TypeElement
, value
為代表方法列表的 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