平常开发中可能不多会本身写注解处理器,可是不少开源库都用到了,如ButterKnife、EventBus、Glide
等。所以咱们必需要了解其原理,才能读懂其余大牛写的代码。java
通常状况下编写编译时注解的项目时会分三个模块:android
我的以为若是注解不是特别多的话,仍是把注解模块和 Api 模块合二为一更好,用户引入的时候就会像下面这个样子:git
annotationProcessor "com.test.processor"
implementation "com.test.api"
复制代码
这样看起来会更清爽,平白无故有多个依赖让人感受有些麻烦。github
写注解必需要知道元注解,尤为是 @Retention
和 @Target
,这里简单介绍下。api
**@Retention **有三个枚举类型:缓存
RetentionPolicy.SOURCE
表示注解只保留在源文件,当 Java 文件编译成 class 文件的时候注解被遗弃;bash
RetentionPolicy.CLASS
表示注解被保留到 class 文件,当 JVM 加载 class 文件的时候被遗弃。数据结构
RetentionPolicy.RUNTIME
表示 JVM 加载 class 文件后依然存在,在运行过程当中能够在任意时间被调用。app
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;
复制代码
编译时注解往简单的说就是在 Java 代码编译成 class 字节码的过程当中执行注解处理器并生成你须要的 Java 文件。
因此第一个问题就是如何在编译时执行?答案是继承 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
:通常表明Package。上面的代码注释也比较清晰了,就是每一个有指定注解的类会被遍历到,而后把它里面全部 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
注解类。
Api 模块就是提供给开发者调用以前注解处理器生成的代码。
在这个示例中咱们提供一个接口,全部生成的 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
了。
看起来编译时注解能作不少事情,并且把操做放在编译期就不会拖慢程序运行时的速度,因此不少框架采起这种方式代替注解反射。不过一样的,注解处理器生成的类也会增大 app 的体积,这多是编译时注解的一个不足。
在 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 类图:
EventBus 时序图(注解反射):
本文主要从编译时注解为核心,讲述了编译时注解的基础以及如何编写一个简单的注解处理器,这对于阅读使用到编译时注解的开源库源码有很大的帮助。接着从 EventBus 3.0
的注解处理器开始分析,在了解了编译时注解的基础后能较容易的理解 MyEventBusIndex
类是如何生成的。而后继续跟进分析了 EventBus.register
订阅流程和 EventBus.post
事件分发的流程。
参考资料