日常开发中可能很少会自己写注解处理器,但是很多开源库都用到了,如 ButterKnife、EventBus、Glide
等。因此我们必须要了解其原理,才能读懂其他大牛写的代码。
一般情况下编写编译时注解的项目时会分三个模块:
个人觉得如果注解不是特别多的话,还是把注解模块和 Api 模块合二为一更好,用户引入的时候就会像下面这个样子:
annotationProcessor "com.test.processor" implementation "com.test.api" 复制代码
这样看起来会更清爽,无缘无故有多个依赖让人感觉有些麻烦。
写注解必须要知道 元注解 ,尤其是 @Retention
和 @Target
,这里简单介绍下。
**@Retention **有三个枚举类型:
RetentionPolicy.SOURCE
表示注解只保留在源文件,当 Java 文件编译成 class 文件的时候注解被遗弃;
RetentionPolicy.CLASS
表示注解被保留到 class 文件,当 JVM 加载 class 文件的时候被遗弃。
RetentionPolicy.RUNTIME
表示 JVM 加载 class 文件后依然存在,在运行过程中可以在任意时间被调用。
RetentionPolicy.RUNTIME
比较容易理解,它就是用在为反射而生的。另外两个都可以用于编译时注解。
@Target有九种枚举类型:
TYPE
作用于接口、类、枚举。
FIELD
作用于字段、变量。
METHOD
作用于方法。
PARAMETER
作用于形参。
CONSTRUCTOR
作用于构造方法。
LOCAL_VARIABLE
作用于局部变量。
ANNOTATION_TYPE
作用于注解。
PACKAGE
作用于包名。
TYPE_PARAMETER
作用于类型参数(如泛型、类型转换)。
TYPE_USE
作用于类型使用时。
@Retention(RetentionPolicy.CLASS) @Target(ElementType.FIELD) public @interface TestField { String value(); } 复制代码
以上代码就通过了元注解实现了注解, TestField
能注解在变量上,并且只保留在 class
文件,运行后这个注解就会消失。使用如下:
@TestField("hello") String value; 复制代码
所以第一个问题就是如何在编译时执行?答案是继承 AbstractProcessor
就可以了,编译器会自动寻找继承 AbstractProcessor
的类,并调用它的 process
方法,一般来说我们会重写以下几个方法:
public class TestProcessor extends AbstractProcessor { @Override public Set<String> getSupportedAnnotationTypes(){ Set<String> annotationTypes = new LinkedHashSet<String>(); annotationTypes.add(TestField.class.getCanonicalName()); return annotationTypes; } @Override public SourceVersion getSupportedSourceVersion(){ return SourceVersion.latestSupported(); } @Override public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv){} } 复制代码
getSupportedAnnotationTypes
方法添加我们需要解析的注解, getSupportedSourceVersion
方法返回最新的版本支持即可。主要的代码处理在 process
方法,继续实现上一小节的注解,我们可以把 TestField
注解的值赋给 value
变量, 这个过程中我们需要先检测有此注解的变量并保存到一个集合中,然后再拿集合内的信息去生成 Java 代码 。先来看下搜集信息的并保存到集合的代码:
private Map<String, ClassInfo> classInfos = new HashMap<>(); public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv){ classInfos.clear(); for(TypeElement annotation: annotations){ // 获取一个类中所有节点,这里可以是域、方法、类节点等等。 Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(annotation); for(Element element : elements) { // 找到变量节点 if(element instanceof VariableElement){ // 获取变量所在类的全限定类名 String qualifiedClassName = ((TypeElement)element.getEnclosingElement()).getQualifiedName().toString(); ClassInfo classInfo = classInfos.get(qualifiedClassName); if(classInfo == null){ classInfo = new ClassInfo(); } classInfo.qualifiedClassName = qualifiedClassName; classInfo.typeElement = (TypeElement)element.getEnclosingElement(); classInfo.variableElements.add(element); // 以全限定类名作为key可保证唯一性 classInfos.put(qualifiedClassName, classInfo); }else { // 在本例中TestField只修饰所有域,因此如果出现不是「变量节点」的话就抛异常吧 } } } } /** * 保存一个类文件中我们所需要的信息 */ public static final class ClassInfo { String qualifiedClassName; //全限定类名 TypeElement typeElement; // 类节点 List<VariableElement> variableElements = new ArrayList<>(); // 一个类中所有有该注解的变量节点 // 获取非限定类名 public String getClassName(){ if(qualifiedClassName == null){ return null; } return qualifiedClassName.substring(qualifiedClassName.lastIndexof(".") + 1, qualifiedClassName.length()); } } 复制代码
简单介绍下 Element
的子类:
VariableElement ExecutableElement TypeElement PackageElement
上面的代码注释也比较清晰了, 就是每个有指定注解的类会被遍历到,然后把它里面所有 VariableElement 保存起来 。
第二个步骤就是把已经收集的信息生成对应的 Java 文件。
private static final String SUFFIX = "$ITest"; public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv){ // 收集信息的代码... // 开始生成Java文件 for(ClassInfo classInfo : classInfos.values()){ try{ // 创建java文件对象,全限定类名+指定后缀 JavaFileObject sourceFile = processingEnv.getFiler().createSourceFile(classInfo.qualifiedClassName + SUFFIX. classInfo.typeElement); // 开始写入代码 Writer writer = sourceFile.openWriter(); writer.write(generateCode(classInfo)); writer.flush(); writer.close(); }catch (IOException e){ e.printStackTrace(); } } return true; } public String generateCode(ClassInfo classInfo){ StringBuilder builder = new StringBuilder(); // 获取包名 String packageName = processEnv.getElementUtils().getPackageOf(classInfo.typeElement).getQualifiedName().toString(); builder.append("package " + packageName + ";/n") .append("import com.test.api.*;/n") .append("public class " + classInfo.getClassName() + SUFFIX + " implements ") .append("ITest<" + classInfo.qualifiedClassName + ">{/n") .append(" public void inject(" + classInfo.qualifiedClassName + " host){/n") .append(generateInject(classInfo)) .append("/n }") .append("/n}"); } public String generateInject(ClassInfo classInfo){ StringBuilder builder = new StringBuilder(); for(VariableElement element : classInfo.variableElements){ TestField test = element.getAnnotation(TestField.class); if(test != null){ // 获取注解上的值 int value = test.value(); String variableName = element.getSimpleName().toString(); // 给变量赋值 builder.append(" host." + elementName + "=" + value + "/n"); } } return builder.toString(); } 复制代码
这段代码创建了 Java 文件,然后在里面使用收集到的信息拼接字符串,非常容易理解。如果复杂一些的项目可以考虑使用 javapoet
库。
最后在需要在 src/main/
下新建 resources
文件夹,再新建 META-INF.services
文件夹,在此文件夹内新建 javax.annotation.processing.Processor
文件,在文件内写入你的注解器的全限定类名,如:
com.test.processor.TestProcessor 复制代码
这样注解处理器就注册成功了,在编译器会自动执行到这个注解。不过还有更简单的一种方式,在build.gradle下加入以下依赖:
compile 'com.google.auto.service:auto-service:1.0-rc4' 复制代码
然后在自定义注解处理器的类上加上如下代码:
@AutoService(Processor.class) public class TestProcessor extends AbstractProcessor { } 复制代码
这样就会自动注册 TestProcessor
注解类。
在这个示例中我们提供一个接口,所有生成的 Java 类都会实现这个接口,方便统一调用。就是上面生成代码中已经出现的 ITest
:
public interface ITest<T> { void inject(T obj); } 复制代码
最终写一个 app 可调用的方法:
public class TestApi { public static void inject(Object obj){ Class<?> clazz = obj.getClass(); String proxyName = clazz.getName() + "$ITest"; // 省略 try catch Class<?> proxyClazz = Class.forName(proxyName); ITest test = (ITest) proxyClazz.newInstance(); test.inject(obj); } } 复制代码
app
内使用如下:
public class MainActivity extends AppCompatActivity { @TestField(2) public int value; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); //..... TestApi.inject(this); } } 复制代码
使用也很简单,就是在变量上使用了 @TestField
注解,然后调用 TestApi.inject(this)
去调用已经生成的 Java 类。最后看下编译时我们生成的 Java 文件是怎样的,其位置位于 build/generated/source/apt/
:
package com.test.project; import com.test.api.*; public class MainActivity$ITest implements ITest<com.test.project.MainActivity>{ public void inject(com.test.project.MainActivity host){ host.value = 2; } } 复制代码
所以在调用 inject
方法之后, MainActivity
中的 value
就被赋值为 2
了。
在 EventBus 3.0 之后也加入了编译时注解, 以下内容主要讲解注解处理器是如何生成 Index 类,并通过使用编译时生成的 Index 类来订阅、分发事件的整个流程。
要想使用编译时注解,需要在 build.gradle
内添加如下脚本:
android { defaultConfig { javaCompileOptions { annotationProcessorOptions { arguments = [ eventBusIndex : 'com.example.myapplication.MyEventBusIndex' ] } } } } dependencies { implementation 'org.greenrobot:eventbus:3.1.1' annotationProcessor 'org.greenrobot:eventbus-annotation-processor:3.1.1' } 复制代码
annotationProcessor
依赖注解处理器没什么问题,那么 arguments
这个参数又是什么用处呢?我们带着问题去看下注解处理器的代码:
public class EventBusAnnotationProcessor extends AbstractProcessor { public static final String OPTION_EVENT_BUS_INDEX = "eventBusIndex"; public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment env) { // ...... // 这里拿到了gradle内的配置,也就是 com.example.myapplication.MyEventBusIndex String index = processingEnv.getOptions().get(OPTION_EVENT_BUS_INDEX); // 收集节点信息并保存 collectSubscribers(annotations, env, messager); // 生成 Java 文件 createInfoIndexFile(index); } } 复制代码
套路和前一节讲的一样。
先是收集信息,由于 EventBus
的 Subscribe
注解只作用在方法上,因此只要使用一个集合,其 key
为 全限定类名 或 TypeElement
(事实上 Eventbus
是以 TypeElement
为 key
), value
为 ExecutableElement
方法节点列表。
然后是根据收集到的信息生成 Java
文件,这个文件的路径是 com.example.myapplication.MyEventBusIndex
。这里就不再详细展开,想详细了解可以去官方文档里看下源码,理解了编译时注解基础后这些代码是比较容易理解的。假设我们在 MainActivity
中某个方法上做了如下注解:
@Subscribe public void testMethod(TestEvent event){ } 复制代码
那么在 build/generated/source/apt/
生成的 MyEventBusIndex
类就是如下:
public class MyEventBusIndex implements SubscriberInfoIndex { private static final Map<Class<?>, SubscriberInfo> SUBSCRIBER_INDEX; static { SUBSCRIBER_INDEX = new HashMap<Class<?>, SubscriberInfo>(); putIndex(new SimpleSubscriberInfo(MainActivity.class, true, new SubscriberMethodInfo[] { new SubscriberMethodInfo("testMethod", TestEvent.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; } } } 复制代码
这里简单解释下几个类的作用。
SubscriberInfoIndex
接口,主要有两个逻辑,一是保存以订阅者 class
对象为 key
, SubscriberInfo
为 value
的集合;二是重写 getSubscriberInfo
方法,将指定的 class
对象的 SubscriberInfo
返回出去。 class
对象、订阅者被注解修饰的方法、订阅者父类的 SubscriberInfo
。 AbstractSubscriberInfo
, AbstractSubscriberInfo
则实现了 SubscriberInfo
, SimpleSubscriberInfo
是 EventBus
默认唯一一个实现 SubscriberInfo
的类,可想而知它提供了让你自己去编写注解处理器和自定义 SubscriberInfo
的可能性。从它的实现上能看出 SimpleSubscriberInfo
内保存了订阅者 class
对象、订阅者方法信息等。 SimpleSubscriberInfo
的一个成员变量,编译时会把 @Subscribe
注解所修饰的方法名、形参类型、 threadMode、priority、sticky
都解析出来,并保存到 SubscriberMethodInfo
中。 最后需要调用提供的 api
,将 MyEventBusIndex
添加到其中:
EventBus.builder().addIndex(new MyEventBusIndex()).installDefaultEventBus(); 复制代码
接下来看下 register
和 post
流程,主要看使用 MyEventBusIndex
类的逻辑,本文不涉及注解反射的逻辑。
public void register(Object subscriber) { // 获取订阅者的 class 对象 Class<?> subscriberClass = subscriber.getClass(); // 通过subscriberMethodFinder解析出这个订阅者被订阅的所有方法 List<SubscriberMethod> subscriberMethods = subscriberMethodFinder.findSubscriberMethods(subscriberClass); synchronized (this) { for (SubscriberMethod subscriberMethod : subscriberMethods) { // 把订阅信息保存到集合中 subscribe(subscriber, subscriberMethod); } } } 复制代码
接下来走到 subscriberMethodFinder
里是如何解析出被 @Subscribe
注解的方法:
List<SubscriberMethod> findSubscriberMethods(Class<?> subscriberClass) { // 先从缓存中取,因此即使是注解反射也不会耗费多少时间 List<SubscriberMethod> subscriberMethods = METHOD_CACHE.get(subscriberClass); if (subscriberMethods != null) { return subscriberMethods; } if (ignoreGeneratedIndex) { // 通过注解反射获取被订阅的方法 subscriberMethods = findUsingReflection(subscriberClass); } else { // 通过编译时注解生成的 MyEventBusIndex 获取被订阅的方法 subscriberMethods = findUsingInfo(subscriberClass); } // ...... METHOD_CACHE.put(subscriberClass, subscriberMethods); return subscriberMethods; } 复制代码
其实即使使用反射也并不会耗费多少时间,因为 EventBus
会只会在第一次使用时反射,之后都使用缓存。跳过反射部分,直接看 findUsingInfo
方法:
private List<SubscriberMethod> findUsingInfo(Class<?> subscriberClass) { FindState findState = prepareFindState(); findState.initForSubscriber(subscriberClass); while (findState.clazz != null) { findState.subscriberInfo = getSubscriberInfo(findState); // ...... } // ...... } 复制代码
FindState
类可以理解为保存了 SubscriberInfo 、SubscriberMethod、class对象
等信息,在之后会使用到。关键在于 getSubscriberInfo
方法。
private SubscriberInfo getSubscriberInfo(FindState findState) { // ...... if (subscriberInfoIndexes != null) { for (SubscriberInfoIndex index : subscriberInfoIndexes) { SubscriberInfo info = index.getSubscriberInfo(findState.clazz); if (info != null) { return info; } } } return null; } 复制代码
subscriberInfoIndexes
是一个 List
数据结构,我们之前调用 EventBus.builder().addIndex(new MyEventBusIndex())
其实就是将 MyEventBusIndex
添加到 subscriberInfoIndexes
中,这个时候我们就可以取出订阅者 class
对象对应的 SubscriberInfo
,还记得它保存了订阅者被注解 @Subscribe
所修饰的方法。最终会通过 SubscriberInfo
返回对应的方法列表,我们再回到 register
方法,在拿到订阅方法列表后,调用 subscribe
方法:
private void subscribe(Object subscriber, SubscriberMethod subscriberMethod) { Class<?> eventType = subscriberMethod.eventType; Subscription newSubscription = new Subscription(subscriber, subscriberMethod); CopyOnWriteArrayList<Subscription> subscriptions = subscriptionsByEventType.get(eventType); if (subscriptions == null) { subscriptions = new CopyOnWriteArrayList<>(); subscriptionsByEventType.put(eventType, subscriptions); } // ... } 复制代码
eventType
是被订阅方法的参数的 class
对象, EventBus
事件分发就是根据参数分发到对应的方法上的,因此要保存以 eventType
为 key
的 subscriptionsByEventType
集合,在之后的分发流程中会使用到。
public void post(Object event) { // 某个线程都会有自己的PostingThreadState PostingThreadState postingState = currentPostingThreadState.get(); List<Object> eventQueue = postingState.eventQueue; // 保证消息能按添加顺序分发 eventQueue.add(event); // isPosting标志位防止多次分发 if (!postingState.isPosting) { postingState.isMainThread = isMainThread(); postingState.isPosting = true; try { while (!eventQueue.isEmpty()) { postSingleEvent(eventQueue.remove(0), postingState); } } finally { postingState.isPosting = false; postingState.isMainThread = false; } } } 复制代码
这段方法核心就是从消息队列中取 event
消息然后调用 postSingleEvent
方法, postSingleEvent
方法内部主要是对父类对象 eventType
的检查,默认是开启父类检查的,如果想要加快事件分发的速度而且不需要分发给父类,可以考虑把标志位改为不检查父类,接着会调用 postToSubscription
方法,
private void postToSubscription(Subscription subscription, Object event, boolean isMainThread) { switch (subscription.subscriberMethod.threadMode) { case POSTING: invokeSubscriber(subscription, event); break; case MAIN: if (isMainThread) { invokeSubscriber(subscription, event); } else { mainThreadPoster.enqueue(subscription, event); } break; case MAIN_ORDERED: if (mainThreadPoster != null) { mainThreadPoster.enqueue(subscription, event); } else { // temporary: technically not correct as poster not decoupled from subscriber invokeSubscriber(subscription, event); } break; case BACKGROUND: if (isMainThread) { backgroundPoster.enqueue(subscription, event); } else { invokeSubscriber(subscription, event); } break; case ASYNC: asyncPoster.enqueue(subscription, event); break; } } 复制代码
这里有 5 种 threadMode
:
默认是 POSTING
策略,我们看下 invokeSubscriber
方法做了什么:
void invokeSubscriber(Subscription subscription, Object event) { try { subscription.subscriberMethod.method.invoke(subscription.subscriber, event); } catch (InvocationTargetException e) { handleSubscriberException(subscription, event, e.getCause()); } catch (IllegalAccessException e) { throw new IllegalStateException("Unexpected exception", e); } } 复制代码
这里再熟悉不过了,通过 method.invoke
反射调用到真实的方法,这里就有疑问了,你这还是用到了反射啊?其实很多框架是避免不了反射的,只是尽量的少用反射能节省不少时间。
以上就是编译时注解生成 MyEventBusIndex
, 然后 EventBus
订阅分发的整个流程。下面用两张图总结下订阅、分发两个流程。
本文主要从编译时注解为核心,讲述了编译时注解的基础以及如何编写一个简单的注解处理器,这对于阅读使用到编译时注解的开源库源码有很大的帮助。接着从 EventBus 3.0
的注解处理器开始分析,在了解了编译时注解的基础后能较容易的理解 MyEventBusIndex
类是如何生成的。然后继续跟进分析了 EventBus.register
订阅流程和 EventBus.post
事件分发的流程。
Android 如何编写基于编译时注解的项目
EventBus官方文档