概述
EventBus一般使用在調用register註冊時,會通過反射去解析記錄訂閱方法,運行期反射比較耗費性能。3.0提供了高級用法即索引,通過註解處理器在編譯期就提前解析記錄訂閱方法。EventBus在索引生成過程中有使用到Type、Element、JavaFileObject等接口,需要先對這些接口有一定了解。
實例解析
使用索引詳細的配置方法可以按照官方文檔http://greenrobot.org/eventbus/documentation/subscriber-index/。
這裏繼續用上篇的例子:
public class MyEventSubscriber {
private WeakReference<Activity> mActivity;
public EventSubscriber(Activity activity) {
this.mActivity = new WeakReference<>(activity);
}
public void register() {
EventBus.getDefault().register(this);
}
public void unregister() {
EventBus.getDefault().unregister(this);
}
@Subscribe(threadMode = ThreadMode.MAIN)
public void onMyEvent(MyEvent event) {
//todo 處理事件
···
}
}
在build.gradle中添加配置:
javaCompileOptions {
annotationProcessorOptions {
arguments = [ eventBusIndex : 'com.example.myapp.MyEventBusIndex' ]
}
}
當編譯完成後,會在build->generated->source->apt->debug/release下生成對應的索引類。本例中生成com.example.myapp包,包下有一個MyEventBusIndex類。
源碼解析
EventBus註解處理依賴庫就一個類EventBusAnnotationProcessor,打開源碼看看它是如何生成索引類。
一.繼承自AbstractProcessor
1.先來看該類註冊的兩個註解:
@SupportedAnnotationTypes("org.greenrobot.eventbus.Subscribe")
@SupportedOptions(value = {"eventBusIndex", "verbose"})
public class EventBusAnnotationProcessor extends AbstractProcessor {
···
}
- SupportedAnnotationTypes用於註冊該註解處理器支持的註解,這裏僅處理EventBus定義的Subscribe註解;
- SupportedOptions用於註冊支持的編譯選項,即在本例中在build.gradle中配置的arguments = [ eventBusIndex : ‘com.example.myapp.MyEventBusIndex’ ],eventBusIndex的值對應’com.example.myapp.MyEventBusIndex’ ,verbose默認false。
2.重寫部分方法:
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latest();
}
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment env) {
···
}
- getSupportedSourceVersion返回支持的Java版本,這裏返回最新;
- process核心方法,編譯過程中會回調該方法,在這裏實現相應的方法。如果我們成功處理,返回true。否則返回false,會有後續的Processor去處理。
ps:根據觀察日誌發現,註解處理器會執行多輪,它還會處理生成的源文件中的註解或上一個Processor未處理的註解。
二.重要成員變量
先介紹下該類中的一些重要的成員變量的作用:
public class EventBusAnnotationProcessor extends AbstractProcessor {
/** 記錄訂閱方法(即註冊了@Subscribe的method),訂閱者類(即method所在的類)爲key。 */
private final ListMap<TypeElement, ExecutableElement> methodsByClass = new ListMap<>();
/** 記錄不處理的訂閱者類。 */
private final Set<TypeElement> classesToSkip = new HashSet<>();
/** 標記索引類是否生成完成。 */
private boolean writerRoundDone;
/** 標記第幾輪執行process */
private int round;
}
ps:ListMap是greenrobot對Map擴展的集合,對HashMap<K, List<V>>的封裝。
三.核心方法process
直接看代碼:
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment env) {
// annotations爲註冊的註解的類型元素的集合,在本例中該集合僅有Subscribe。env用於獲取註解處理環境信息
// Messager用於打印日誌
Messager messager = processingEnv.getMessager();
try {
// 獲取在build.gradle中配置的eventBusIndex參數的值,本例即爲com.example.myapp.MyEventBusIndex
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;
}
// 獲取verbose參數值
verbose = Boolean.parseBoolean(processingEnv.getOptions().get(OPTION_VERBOSE));
int lastPeriod = index.lastIndexOf('.');
// 截取生成索引類的目標包名,即com.example.myapp
String indexPackage = lastPeriod != -1 ? index.substring(0, lastPeriod) : null;
// 標記輪次增加
round++;
// verbose僅用於判斷是否打印這段日誌
if (verbose) {
messager.printMessage(Diagnostic.Kind.NOTE, "Processing round " + round + ", new annotations: " +
!annotations.isEmpty() + ", processingOver: " + env.processingOver());
}
// 判斷註解處理最後一輪是否結束
if (env.processingOver()) {
// 判斷是否有該處理器支持的註解或代碼中是否有使用註解
if (!annotations.isEmpty()) {
// 若註解處理過程已經結束,但是仍然有註解集合傳入,則打印錯誤日誌
messager.printMessage(Diagnostic.Kind.ERROR,
"Unexpected processing state: annotations still available after processing over");
return false;
}
}
// 再對註解集合做一次非空檢查
if (annotations.isEmpty()) {
return false;
}
// 若已經生成完成,也打印錯誤日誌
if (writerRoundDone) {
messager.printMessage(Diagnostic.Kind.ERROR,
"Unexpected processing state: annotations still available after writing.");
}
// 以上是檢查完畢,下面開始進行解析記錄
// 記錄合法的訂閱方法,保存進methodsByClass集合---①
collectSubscribers(annotations, env, messager);
// 記錄不處理的訂閱方法---②
checkForSubscribersToSkip(messager, indexPackage);
if (!methodsByClass.isEmpty()) {
// 若存在合法的訂閱方法,則開始生成索引類---③
createInfoIndexFile(index);
} else {
messager.printMessage(Diagnostic.Kind.WARNING, "No @Subscribe annotations found");
}
// 完成後標記writerRoundDone爲true
writerRoundDone = true;
} catch (RuntimeException e) {
// IntelliJ does not handle exceptions nicely, so log and print a message
e.printStackTrace();
messager.printMessage(Diagnostic.Kind.ERROR, "Unexpected error in EventBusAnnotationProcessor: " + e);
}
// 最終返回true,表示已經正確處理
return true;
}
process方法中先進行初步的合法性檢查,之後分三步開始處理:1.解析出所有訂閱方法;2.記錄不處理的訂閱方法;3.生成索引類。接下來逐步分析。
1.初步收集訂閱方法
關鍵在collectSubscribers方法中:
private void collectSubscribers(Set<? extends TypeElement> annotations, RoundEnvironment env, Messager messager) {
// 遍歷該處理器註冊的註解元素的集合
for (TypeElement annotation : annotations) {
// 獲取所有使用了該註解的元素集合,在這裏即所有添加了@Subscribe的方法的元素
Set<? extends Element> elements = env.getElementsAnnotatedWith(annotation);
for (Element element : elements) {
if (element instanceof ExecutableElement) {
// ExecutableElement表示可執行方法的元素,因爲@Subscribe只能用在method上
ExecutableElement method = (ExecutableElement) element;
// 合法性校驗
if (checkHasNoErrors(method, messager)) {
// 獲取ExecutableElement所在類的元素,即訂閱方法所在的訂閱者類
TypeElement classElement = (TypeElement) method.getEnclosingElement();
// 根據class作key、method作value緩存到集合中
methodsByClass.putElement(classElement, method);
}
} else {
messager.printMessage(Diagnostic.Kind.ERROR, "@Subscribe is only valid for methods", element);
}
}
}
}
上面方法很簡單,就是取得所有添加@Subscribe的method,將符合要求的method根據它所在的class歸類緩存到methodsByClass中。
接着看下checkHasNoErrors方法,這裏面如何對method進行校驗:
private boolean checkHasNoErrors(ExecutableElement element, Messager messager) {
// 判斷方法修飾符,不能是靜態方法
if (element.getModifiers().contains(Modifier.STATIC)) {
messager.printMessage(Diagnostic.Kind.ERROR, "Subscriber method must not be static", element);
return false;
}
// 方法必須是public的
if (!element.getModifiers().contains(Modifier.PUBLIC)) {
messager.printMessage(Diagnostic.Kind.ERROR, "Subscriber method must be public", element);
return false;
}
// 獲取該方法的參數集合
List<? extends VariableElement> parameters = ((ExecutableElement) element).getParameters();
// 參數個數必須是一個
if (parameters.size() != 1) {
messager.printMessage(Diagnostic.Kind.ERROR, "Subscriber method must have exactly 1 parameter", element);
return false;
}
return true;
}
可以看出,我們創建的訂閱方法必須符合public、非static、僅有一個參數,否則即使添加@Subscribe也是無效的。
2.篩選訂閱方法
進入checkForSubscribersToSkip方法:
private void checkForSubscribersToSkip(Messager messager, String myPackage) {
// 遍歷集合key,依次檢查訂閱者類
for (TypeElement skipCandidate : methodsByClass.keySet()) {
TypeElement subscriberClass = skipCandidate;
while (subscriberClass != null) {
// 判斷訂閱者類是否可訪問
if (!isVisible(myPackage, subscriberClass)) {
// 若該訂閱者類對於即將生成的目標索引類不可訪問,則添加進篩除集合,結束while循環,檢查下一個訂閱者類
boolean added = classesToSkip.add(skipCandidate);
if (added) {
String msg;
if (subscriberClass.equals(skipCandidate)) {
msg = "Falling back to reflection because class is not public";
} else {
msg = "Falling back to reflection because " + skipCandidate +
" has a non-public super class";
}
messager.printMessage(Diagnostic.Kind.NOTE, msg, subscriberClass);
}
break;
}
// 從集合中取出該訂閱者類中的訂閱方法
List<ExecutableElement> methods = methodsByClass.get(subscriberClass);
if (methods != null) {
for (ExecutableElement method : methods) {
String skipReason = null;
// 獲取方法的第一個也是唯一一個參數
VariableElement param = method.getParameters().get(0);
// 獲取參數對象的類型
TypeMirror typeMirror = getParamTypeMirror(param, messager);
// 參數必須是一個類的聲明類型和類元素,即如本例定義的MyEvent實體類
if (!(typeMirror instanceof DeclaredType) ||
!(((DeclaredType) typeMirror).asElement() instanceof TypeElement)) {
skipReason = "event type cannot be processed";
}
// 參數類型校驗通過,則檢查參數對象類的可訪問性
if (skipReason == null) {
TypeElement eventTypeElement = (TypeElement) ((DeclaredType) typeMirror).asElement();
if (!isVisible(myPackage, eventTypeElement)) {
skipReason = "event type is not public";
}
}
// 如果有一項校驗不通過,則將該類加入篩除集合
if (skipReason != null) {
boolean added = classesToSkip.add(skipCandidate);
if (added) {
String msg = "Falling back to reflection because " + skipReason;
if (!subscriberClass.equals(skipCandidate)) {
msg += " (found in super class for " + skipCandidate + ")";
}
messager.printMessage(Diagnostic.Kind.NOTE, msg, param);
}
break;
}
}
}
// subscriberClass切換到父類,若父類是系統的類,則返回null,結束while循環
subscriberClass = getSuperclass(subscriberClass);
}
}
}
該方法中依次遍歷訂閱者類及父類,校驗訂閱者類的可訪問性和訂閱方法參數的類型及訪問性,若有不符合要求的就添加進篩除集合。
接着再詳細看下校驗訪問性的方法isVisible:
private boolean isVisible(String myPackage, TypeElement typeElement) {
Set<Modifier> modifiers = typeElement.getModifiers();
boolean visible;
// 也是通過判斷修飾符
if (modifiers.contains(Modifier.PUBLIC)) {
visible = true;
} else if (modifiers.contains(Modifier.PRIVATE) || modifiers.contains(Modifier.PROTECTED)) {
visible = false;
} else {
// 若是默認則判斷訂閱者類所在包名和索引類所在包名是否一致
// getPackageElement方法中不斷獲取typeElement的頂層元素直至取到包元素,然後獲取包名
String subscriberPackage = getPackageElement(typeElement).getQualifiedName().toString();
if (myPackage == null) {
visible = subscriberPackage.length() == 0;
} else {
visible = myPackage.equals(subscriberPackage);
}
}
return visible;
}
接着看獲取參數類型的方法getParamTypeMirror:
private TypeMirror getParamTypeMirror(VariableElement param, Messager messager) {
TypeMirror typeMirror = param.asType();
// Check for generic type
if (typeMirror instanceof TypeVariable) {
// 獲取該類型變量的上邊界,如果該對象有通過extends繼承其他對象,則upperBound爲DeclaredType類型
TypeMirror upperBound = ((TypeVariable) typeMirror).getUpperBound();
if (upperBound instanceof DeclaredType) {
if (messager != null) {
messager.printMessage(Diagnostic.Kind.NOTE, "Using upper bound type " + upperBound +
" for generic parameter", param);
}
// 替換參數類型爲上邊界的類型
typeMirror = upperBound;
}
}
return typeMirror;
}
經過篩選將不需要寫入索引類的訂閱者類存進classesToSkip集合中,後續生成的時候會比較這個集合中的類,判斷是跳過還是處理。
3.生成索引類
如果methodsByClass中不爲空,則調用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<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
}
}
}
}
writeIndexLines方法:
private void writeIndexLines(BufferedWriter writer, String myPackage) throws IOException {
// 遍歷集合
for (TypeElement subscriberTypeElement : methodsByClass.keySet()) {
// 如果和篩除集合中匹配則跳過
if (classesToSkip.contains(subscriberTypeElement)) {
continue;
}
// 獲取訂閱者類名稱字符串
String subscriberClass = getClassString(subscriberTypeElement, myPackage);
// 再次檢查是否可訪問
if (isVisible(myPackage, subscriberTypeElement)) {
// 寫入第三個參數字符串數據,writeLine方法中封裝了縮進換行操作
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");
}
}
}
writeIndexLines方法中遍歷methodsByClass,然後依次寫入訂閱方法相關信息。
先看裏面的getClassString方法,該方法獲取訂閱者類的類名字符串:
private String getClassString(TypeElement typeElement, String myPackage) {
PackageElement packageElement = getPackageElement(typeElement);
// 獲取訂閱者類的包元素全限定名稱
String packageString = packageElement.getQualifiedName().toString();
// 訂閱者類全限定名稱
String className = typeElement.getQualifiedName().toString();
if (packageString != null && !packageString.isEmpty()) {
// 比較包名
if (packageString.equals(myPackage)) {
// 若包名一致,則只取訂閱者類的simple名稱
// cutPackage通過截取字符串方式,而不是調用getSimpleName,避免內部類的時候取到$
className = cutPackage(myPackage, className);
} else if (packageString.equals("java.lang")) {
// 若是java.lang包,則直接取SimpleName
className = typeElement.getSimpleName().toString();
}
}
return className;
}
再看寫訂閱方法相關信息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();
// 獲取參數類名字符串
String eventClass = getClassString(paramElement, myPackage) + ".class";
Subscribe subscribe = method.getAnnotation(Subscribe.class);
List<String> parts = new ArrayList<>();
parts.add(callPrefix + "(\"" + methodName + "\",");
String lineEnd = "),";
// 獲取註解中的值
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() + ")");
}
}
}
以上就是生成索引類源文件,整個過程大致是從methodsByClass中獲取訂閱者類、方法相關信息,跳過不合法的訂閱者類,然後獲取類名、方法名、參數對象類字符串以及註解中的值,寫入生成。
四.分析最終生成的源文件
以下是最終生成的源文件代碼:
package com.example.myapp;
import org.greenrobot.eventbus.meta.SimpleSubscriberInfo;
import org.greenrobot.eventbus.meta.SubscriberMethodInfo;
import org.greenrobot.eventbus.meta.SubscriberInfo;
import org.greenrobot.eventbus.meta.SubscriberInfoIndex;
import org.greenrobot.eventbus.ThreadMode;
import java.util.HashMap;
import java.util.Map;
/** This class is generated by EventBus, 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 SimpleSubscriberInfo(com.example.myapp.subscribe.MyEventSubscriber.class, true,
new SubscriberMethodInfo[] {
new SubscriberMethodInfo("onMyEvent", com.example.myapp.model.event.MyEvent.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;
}
}
}
在靜態代碼塊中,構造了SimpleSubscriberInfo對象,然後存進HashMap集合中。
回到EventBus3實例源碼淺析(上),結合裏面的源碼來分析:
1)在初始化時通過EventBusBuilder的addIndex方法添加索引
/** Adds an index generated by EventBus' annotation preprocessor. */
public EventBusBuilder addIndex(SubscriberInfoIndex index) {
if(subscriberInfoIndexes == null) {
subscriberInfoIndexes = new ArrayList<>();
}
// 可添加多個索引,存入ArrayList集合中
subscriberInfoIndexes.add(index);
return this;
}
只需實例化索引類然後傳入即可。
2)註冊時會優先使用索引
在SubscriberMethodFinder的findUsingInfo方法中,會查找可用索引類,最終調用索引類的getSubscriberInfo方法獲取SubscriberInfo對象。SubscriberInfo中包含訂閱方法、參數、註解值等相關信息,因此省去了反射查找、校驗的操作。
總結
索引將註冊時的繁瑣操作放在編譯期完成,大大節省了事件,不過發送事件時仍需要反射。