0%

Android-EventBus修改纪实(四)-注解处理器

Android-EventBus修改纪实(四)-注解处理器

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第 6 天,点击查看活动详情


前言

本文是 EventBus 修改纪实的第四篇,笔者在写第一篇文章时只是想记录下修改 EventBus 的过程,分享解决问题和查看源码的思路,没想到不知不觉会写这么多,“天下无不散之筵席”,本文将分析 EventBus 注解处理器的流程,也是 EventBus 修改纪实的最后一篇文章。

纪实

EventBus 的注解处理器程序只有一个类 EventBusAnnotationProcessor

1
2
3
4
5
6
@SupportedAnnotationTypes("com.yarward.org.greenrobot.eventbus.Subscribe")
@SupportedOptions(value = {"eventBusIndex", "verbose"})
@IncrementalAnnotationProcessor(AGGREGATING)
public class EventBusAnnotationProcessor extends AbstractProcessor {
// ......
}

EventBusAnnotationProcessor 上有三个注解,下面一一介绍:

  • @SupportedAnnotationTypes 注解标识该注解处理器只处理 @Subscribe 注解,
  • @SupportedOptions 注解标识改注解处理器可以有两个参数,eventBusIndex 标识要生成 Index 类的全量限定名称,verbose 主要用于日志输出
  • @IncrementalAnnotationProcessor 注解方便构建增量注解处理器

process

接下来分析处理流程,处理流程主要在 process 方法中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
public static final String OPTION_EVENT_BUS_INDEX = "eventBusIndex";
public static final String OPTION_VERBOSE = "verbose";

// annotations 是 `@Subscribe` 注解集合
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment env) {
// 获取日志输出器
Messager messager = processingEnv.getMessager();
try {
// 获取 `eventBusIndex` 参数
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('.');
// 获取 eventBusIndex 的包名
String indexPackage = lastPeriod != -1 ? index.substring(0, lastPeriod) : null;

round++;
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.");
}
// 收集订阅者和订阅方法
collectSubscribers(annotations, env, messager);
// 检查是否可以为订阅者生成 Index
checkForSubscribersToSkip(messager, indexPackage);

if (!methodsByClass.isEmpty()) {
// 生成 Index 文件
createInfoIndexFile(index);
} else {
messager.printMessage(Diagnostic.Kind.WARNING, "No @Subscribe annotations found");
}
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);
}
return true;
}

collectSubscribers

我们首先分析是如何收集订阅者和订阅方法的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 自定义集合,实际上是 Map<TypeElement, List<ExecutableElement>>
private final ListMap<TypeElement, ExecutableElement> methodsByClass = new ListMap<>();

// annotations 是 `@Subscribe ` 注解集合
private void collectSubscribers(Set<? extends TypeElement> annotations, RoundEnvironment env, Messager messager) {
// 遍历 `@Subscribe` 注解集合
for (TypeElement annotation : annotations) {
// 获取有 `@Subscribe` 注解的元素集合
Set<? extends Element> elements = env.getElementsAnnotatedWith(annotation);
for (Element element : elements) {
// 判断是不是可执行的元素,一般表示是 Java 方法
if (element instanceof ExecutableElement) {
ExecutableElement method = (ExecutableElement) element;
// 检查是否是订阅方法
if (checkHasNoErrors(method, messager)) {
TypeElement classElement = (TypeElement) method.getEnclosingElement();
// 添加进 Map 集合
methodsByClass.putElement(classElement, method);
}
} else {
messager.printMessage(Diagnostic.Kind.ERROR, "@Subscribe is only valid for methods", element);
}
}
}
}

首先遍历 @Subscribe 注解集合,通过 getElementsAnnotatedWith 获取有 @Subscribe 注解的元素集合,然后遍历元素集合,在遍历元素集合时判断元素是否是 ExecutableElement,即当前元素是不是方法,如何是方法再通过 checkHasNoErrors 检查此方法是否符合订阅方法的规范:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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;
}

// 判断是否是公开方法
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;
}

检查方法比较简单,需要满足以下条件:

  • 必须不能是静态方法
  • 必须是公开方法
  • 方法有且仅有一个入参

注意这里没有判断是否是桥接方法和合成方法,因为注解处理器处于编译期,桥接方法和合成方法应该经过 javac 编译后才会有。

最后把符合订阅方法规范的方法收集起来,存储在 methodsByClass 中,methodsByClass 是 greenrobot 自定义的,其实际类型是 Map<TypeElement, List<ExecutableElement>>,以订阅者为 Key,订阅者中的订阅方法集合为 Value。

checkForSubscribersToSkip

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
private void checkForSubscribersToSkip(Messager messager, String myPackage) {
// 遍历 订阅者
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);
// 判断订阅方法的入参是否是声明类型,即不是基本类型;或者是声明类型但不是类或接口
if (!(typeMirror instanceof DeclaredType) ||
!(((DeclaredType) typeMirror).asElement() instanceof TypeElement)) {
skipReason = "event type cannot be processed";
}
if (skipReason == null) {
TypeElement eventTypeElement = (TypeElement) ((DeclaredType) typeMirror).asElement();
// 判断 Event 是否可见
if (!isVisible(myPackage, eventTypeElement)) {
skipReason = "event type is not public";
}
}
if (skipReason != null) {
// 添加进跳过订阅者集合,退出 for 循环
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 = getSuperclass(subscriberClass);
}
}
}

首先调用 isVisible 方法判断订阅者是否可见:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
private boolean isVisible(String myPackage, TypeElement typeElement) {
// 获取描述符集合
Set<Modifier> modifiers = typeElement.getModifiers();
boolean visible;

// 判断是否是 PUBLIC
if (modifiers.contains(Modifier.PUBLIC)) {
visible = true;
// 判断是否是 PRIVATE 或者 PROTECTED
} else if (modifiers.contains(Modifier.PRIVATE) || modifiers.contains(Modifier.PROTECTED)) {
visible = false;
} else {
// 其他情况
// 获取元素的包名
String subscriberPackage = getPackageElement(typeElement).getQualifiedName().toString();
// 若 Index 类包名为空,则根据元素包名的长度判断,否则比较 Index 类和元素是否在同一个包下
if (myPackage == null) {
visible = subscriberPackage.length() == 0;
} else {
visible = myPackage.equals(subscriberPackage);
}
}
return visible;
}

如果订阅者符合以下条件即认为可见:

  • 订阅者含有 public 修饰符
  • Index 类包名为空时,订阅者包名长度为 0,
  • Index 类包名不为空,订阅者和 Index 类同属一个包下

如果订阅者不符合可见性条件,则添加进 classesToSkip 集合,后续生成 Index 类时会忽略此这些订阅者。

如果订阅者符合可见性条件,接下来判断订阅方法是否符合规范:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
for (ExecutableElement method : methods) {
String skipReason = null;
VariableElement param = method.getParameters().get(0);
TypeMirror typeMirror = getParamTypeMirror(param, messager);
// 判断订阅方法的入参是否是声明类型,即不是基本类型;或者是声明类型但不是类或接口
if (!(typeMirror instanceof DeclaredType) ||
!(((DeclaredType) typeMirror).asElement() instanceof TypeElement)) {
skipReason = "event type cannot be processed";
}
if (skipReason == null) {
TypeElement eventTypeElement = (TypeElement) ((DeclaredType) typeMirror).asElement();
// 判断 Event 是否可见
if (!isVisible(myPackage, eventTypeElement)) {
skipReason = "event type is not public";
}
}
if (skipReason != null) {
// 添加进跳过订阅者集合,退出 for 循环
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;
}
}

遍历订阅方法,判断订阅方法的入参如果不是声明类型,即不是基本类型;或者入参是声明类型但不是类或接口,则将订阅者添加进跳过订阅者集合并退出 for 循环,否则再判断入参是否可见,若是不可见,则将订阅者添加进跳过订阅者集合并退出 for 循环。

最后获取当前订阅者的父类,循环判断父类是否也符合以上规范,若是父类不符合以上规范,则将当前订阅者添加进跳过订阅者集合并退出 for 循环。

createInfoIndexFile

收集和过滤完订阅者后,接下来就可以生成 Index 类了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
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 com.yarward.org.greenrobot.eventbus.meta.SimpleSubscriberInfo;\n");
writer.write("import com.yarward.org.greenrobot.eventbus.meta.SubscriberMethodInfo;\n");
writer.write("import com.yarward.org.greenrobot.eventbus.meta.SubscriberInfo;\n");
writer.write("import com.yarward.org.greenrobot.eventbus.meta.SubscriberInfoIndex;\n\n");
writer.write("import com.yarward.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");

// 类声明,实现 `SubscriberInfoIndex` 接口
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");

// 写入 Index
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");

// 实现 `SubscriberInfoIndex` 接口方法
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
}
}
}
}

createInfoIndexFile 方法比较简单,主要是模板代码,生成 Index 的逻辑主要在 writeIndexLines 中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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(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 方法也比较简单,遍历之前收集的订阅者集合,首先判断是否忽略当前订阅者,随后获取订阅者类名,如果是内部类则包含外部类的类名,然后再次判断当前订阅者是否可见,如果可见则生成 SimpleSubscriberInfo 对象添加进 Index。

SimpleSubscriberInfo 是 Index 元素,其构造方法有三个入参,第一个入参是:订阅者 Class 对象,第二个入参是:是否检查父类,默认为 true,第三个参数是:订阅方法信息数组。

接下来调用 writeCreateSubscriberMethods 方法生成订阅方法信息 SubscriberMethodInfo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
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();
// 获取 Event 事件 Class 对象
String eventClass = getClassString(paramElement, myPackage) + ".class";

// 获取 `Subscribe` 注解
Subscribe subscribe = method.getAnnotation(Subscribe.class);
List<String> parts = new ArrayList<>();
parts.add(callPrefix + "(\"" + methodName + "\",");
String lineEnd = "),";
// 获取 `Subscribe` 注解的参数,并根据参数生成 `SubscriberMethodInfo` 的入参
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() + ",");
// 此处是笔者对必达事件的处理
// begin - 添加必达事件
parts.add(subscribe.sticky() + ",");
parts.add(subscribe.rendezvous() + lineEnd);
// end - 添加必达事件
}
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() + ")");
}
}
}

遍历当前订阅者中所有的订阅方法,首先订阅方法的名称,入参 Event 事件的 Class 对象,然后获取 @Subscribe 注解的参数,并根据注解的参数生成 SubscriberMethodInfo 的入参,这里有笔者对必达事件的处理(注意:这里处理有Bug,不知读者有没有看出来呢?),最后调用 writeLine 完整的生成 SubscriberMethodInfo 方法。

至此,EventBus 注解处理器的流程分析完毕,下面是生成的 Index 类示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class EventBusTestsIndex implements SubscriberInfoIndex {
private static final Map<Class<?>, SubscriberInfo> SUBSCRIBER_INDEX;

static {
SUBSCRIBER_INDEX = new HashMap<Class<?>, SubscriberInfo>();

putIndex(new SimpleSubscriberInfo(EventBusMainThreadTest.class, true, new SubscriberMethodInfo[] {
new SubscriberMethodInfo("onEventMainThread", String.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;
}
}
}

总结

  • 在第一篇 Android-EventBus修改纪实 文章中笔者记录了为 EventBus 增加 必达事件 的过程,
  • 在第二篇 Android-EventBus修改纪实(二) 文章中笔者对第一篇文章中未实现的部分进行了补充,并简单介绍了 EventBus 提供的线程模型及使用注意事项,
  • 在第三篇 Android-EventBus修改纪实(三) 文章中笔者详细分析了 EventBus 的线程调度过程及使用注意事项,
  • 在第四篇文章中笔者详细分析了 EventBus 注解处理器生成 Index 类的流程,第一篇文章中添加 必达事件 时没有修改注解处理器,在本文中也进行了简单的描述。

本文分析订阅者是否符合规范时,有一处根据订阅方法检查过滤订阅者,如果订阅者中有一个方法不符合规范,整个订阅者都要跳过,这里你认为是否合理呢?

大家求同存异,欢迎交流。

happy~,希望可以帮你更好的使用 EventBus

欢迎关注我的其它发布渠道