关于我
一个有思想的程序猿,终身学习实践者,目前在一个创业团队任team lead,技术栈涉及Android、Python、Java和Go,这个也是我们团队的主要技术栈。
Github: 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
是一个纯粹的 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 主要的项目结构 └── src/main/java └── com.gitlab.annotationprocessor └── EasyBusAnnotationProcessor.java └── resources/META-INF.services └── javax.annotation.processing.Processor 复制代码
这个只有一个 java
类和一个配置 Processor
的文件。解析注解生成 java
代码的逻辑就在 EasyBusAnnotationProcessor
里面。
# easybuslib 主要项目结构 └── src └── main/java └── com.gitlab.easybuslib ├── EasyBus.java └── Logger.java └── res 复制代码
这里封装了 EasyBus
主要接口,其逻辑在前面已经解释过了。不过今天也会对它进行改造使它支持编译期间解析得到的订阅者的 onEvent
方法(不是必需以 onEvent
开头,本文为了表达方便而使用)。
项目结构改造完成之后,接下来我们自上而下对注解解析器进行解读和实现。
定义注解在前面已经解读过,这里直接贴出代码
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 + ";/n/n"); } 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;/n/n"); 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;/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) { 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、然后点击 Edit Configurations
配置 remote
填写名称,例如 processorDebug
后保存。
3、选择 processorDebug
4、添加断点后 Rebuild Project
现在就可以对注解解析器进行调试了