一、简介
APT( annotation processing tool )即注解处理工具。用于处理编译时注解,在编译期生成 .java文件,可以方便我们减少一些重复代码的编写。像ButterKnife、Dagger、EventBus、AndroidAnnotation等常用框架都用到了apt。下面我们通过简单的例子实现类似ButterKnife的bindView、bindOnClick功能。
二、实现流程
先看一下项目整体结构:
项目中包含了app 、myannotation 和processor三个module。其中myannotation是我们的自定义注解,processor为注解处理器。为什么要写两个module呢?因为apt需要继承AbstractProcessor类,该类需要在new module–>Java Library里面才能获取到并且编译时注解处理器仅在编译时作用,不会参与apk打包,所以processor独立成一个module。而自定义注解需同时提供给app和processor使用,所以myannotation也独立成一个module。
1. 自定义注解
新建一个Java Library:myannotation,创建两个自定义注解类,
/***
* 自定义BindView注解
*/
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.FIELD)
public @interface BindView {
int value();
}
/**
* 自定义onclick注解
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.CLASS)
public @interface OnClick {
int value();
}
以上定义了BindView、OnClick两个注解,BindView作用对象为成员变量(FIELD)、OnClick作用对象为方法(METHOD),两者的生命周期都是编译时(CLASS)。
注:这两个类里面是不应有中文字符的,我这里为了方便添加了中文注释。在编译时会出现编码GBK的不可映射字符的错误,或者添加以下代码到build.gradle文件下解决该问题:
tasks.withType(JavaCompile) {
options.encoding = "UTF-8"
}
2. 实现注解处理器
新建一个Java Library:processor。依赖myannotation。
2.1 添加MyProcessor类继承AbstractProcessor:
public class MyProcessor extends AbstractProcessor {
private Messager messager;
private Elements elementUtils;
private Map<String, ClassCreatorProxy> mProxyMap = new HashMap<>();
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
return false;
}
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
//获取message、elements
messager = processingEnv.getMessager();
elementUtils = processingEnv.getElementUtils();
}
@Override
public Set<String> getSupportedAnnotationTypes() {
//定义支持的注解类型为 自定义的BindView、OnClick
HashSet<String> supportedType = new LinkedHashSet<>();
supportedType.add(BindView.class.getCanonicalName());
supportedType.add(OnClick.class.getCanonicalName());
return supportedType;
}
@Override
public SourceVersion getSupportedSourceVersion() {
//指定使用的Java版本,通常这里返回最新支持版本
return SourceVersion.latestSupported();
}
}
通常我们需要实现四个方法:
- init():初始化,获取ElementUtils以及Messager,ElementUtils在我们创建代码时用于获取包名等信息,Messager用于编译时打印相关信息。
- getSupportedAnnotationTypes():返回支持注解的列表,这里添加了我们已经定义好的注解BindView和OnClick。
- getSupportedSourceVersion():指定使用的Java版本,通常这里返回最新支持版本。
- process():关键方法,用于处理注解以及生成相关的java代码。
下面来看process()方法的完整代码:
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
//输出打印信息
messager.printMessage(Diagnostic.Kind.NOTE, "processing...");
mProxyMap.clear();
//获取所有被BindView注解修饰的元素
Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(BindView.class);
for (Element element :
elements) {
VariableElement variableElement = (VariableElement) element;
TypeElement classElement = (TypeElement) variableElement.getEnclosingElement();
String fullClassName = classElement.getQualifiedName().toString();
ClassCreatorProxy proxy = mProxyMap.get(fullClassName);
if (proxy == null) {
proxy = new ClassCreatorProxy(elementUtils, classElement);
mProxyMap.put(fullClassName, proxy);
}
BindView bindAnnotation = variableElement.getAnnotation(BindView.class);
int id = bindAnnotation.value();
proxy.putElement(id, variableElement);
}
//获取所有被OnClick注解修饰的元素
Set<? extends Element> onClickElement = roundEnv.getElementsAnnotatedWith(OnClick.class);
for (Element element :
onClickElement) {
TypeElement classElement = (TypeElement) element.getEnclosingElement();
ExecutableElement executableElement = (ExecutableElement) element;
String fullClassName = classElement.getQualifiedName().toString();
ClassCreatorProxy proxy = mProxyMap.get(fullClassName);
if (proxy == null) {
proxy = new ClassCreatorProxy(elementUtils, classElement);
mProxyMap.put(fullClassName, proxy);
}
OnClick bindAnnotation = element.getAnnotation(OnClick.class);
int id = bindAnnotation.value();
proxy.putOnclickElement(id, executableElement);
}
// //通过遍历mProxyMap,创建java文件
for (String key : mProxyMap.keySet()) {
ClassCreatorProxy proxyInfo = mProxyMap.get(key);
try {
messager.printMessage(Diagnostic.Kind.NOTE, " --> create " + proxyInfo.getProxyClassFullName());
JavaFileObject jfo = processingEnv.getFiler().createSourceFile(proxyInfo.getProxyClassFullName(), proxyInfo.getTypeElement());
Writer writer = jfo.openWriter();
writer.write(proxyInfo.generateJavaCode());
writer.flush();
writer.close();
} catch (IOException e) {
messager.printMessage(Diagnostic.Kind.NOTE, " --> create " + proxyInfo.getProxyClassFullName() + "error");
}
}
messager.printMessage(Diagnostic.Kind.NOTE, "process finish ...");
return true;
}
Map<String, ClassCreatorProxy> mProxyMap是保存包含了自定义注解的类的集合,String参数以类的全类名作为标识,ClassCreatorProxy则是用来生成代码的代理类。首先获取到被注解修饰的元素,再从该元素获取全类名,如果该类已经存在于mProxyMap里面,则取出ClassCreatorProxy。否则将全类名和新建的ClassCreatorProxy保存到mProxyMap中。最后将注解信息保存到ClassCreatorProxy类中。
这里有两段逻辑一样的代码分别处理了BindView和OnClick注解。
紧接着遍历mProxyMap,通过createSourceFile方法创建了java文件,ClassCreatorProxy的generateJavaCode方法向文件里面编写代码。
以上就是MyProcessor的基本工作了,接下来看生成代码的类ClassCreatorProxy。
2.2 代码的生成:ClassCreatorProxy
这里是用字符串拼接的方法来实现,逻辑比较简单,直接贴代码了:
public class ClassCreatorProxy {
private Elements elements;
private TypeElement typeElement;
private String mPakageName;
private String mBindingClassName;
private Map<Integer, VariableElement> mVariableElementMap = new HashMap();
private Map<Integer, ExecutableElement> onClickMap = new HashMap();
public ClassCreatorProxy(Elements elementUtils, TypeElement classElement) {
this.elements = elementUtils;
this.typeElement = classElement;
PackageElement packageElement = elements.getPackageOf(typeElement);
mPakageName = packageElement.getQualifiedName().toString();
String className = classElement.getSimpleName().toString();
mBindingClassName = className + "_ViewBinding";
}
public void putElement(int id, VariableElement element) {
mVariableElementMap.put(id, element);
}
public void putOnclickElement(int id, ExecutableElement element) {
onClickMap.put(id, element);
}
//创建java代码
public String generateJavaCode() {
//StringBuilder 速度大于StringBuffer ,线程不安全
StringBuilder sb = new StringBuilder();
sb.append("package ").append(mPakageName).append(";\n\n");
//sb.append("import com.jia.reflectionandannotation.*;\n");
sb.append("\n");
sb.append("public class ").append(mBindingClassName).append(" {\n");
generateBindViewMethod(sb);
generateBindOnclickMethod(sb);
// generateOnclickMethod2(sb);
sb.append(" \n}");
return sb.toString();
}
//添加bindView方法
public void generateBindViewMethod(StringBuilder sb) {
sb.append("\tpublic void bindView(" + typeElement.getQualifiedName() + " activity){\n");
for (int id :
mVariableElementMap.keySet()) {
VariableElement variableElement = mVariableElementMap.get(id);
//获取view的类型:TextView、Button。。。
String viewType = variableElement.asType().toString();
//获取view的名字
String viewName = variableElement.getSimpleName().toString();
//生成 activity.textView=(TextView)activity.findViewById(id);
sb.append("\t\tactivity." + viewName + " = (" + viewType + ")activity.findViewById(" + id + ");\n");
}
sb.append("\t}");
}
private void generateBindOnclickMethod(StringBuilder sb) {
sb.append("\n\tpublic void bindOnClick(final " + typeElement.getQualifiedName() + " activity){\n");
for (int id :
onClickMap.keySet()) {
ExecutableElement executableElement = onClickMap.get(id);
String methodName = executableElement.getSimpleName().toString();//方法的名字
sb.append("\t\tactivity.findViewById(" + id + ").setOnClickListener(new android.view.View.OnClickListener() {\n" +
"\t\t\t@Override\n" +
"\t\t\tpublic void onClick(android.view.View view) {\n" +
"\t\t\t\tactivity." + methodName + "();\n" +
"\t\t\t}\n" +
"\t\t});\n");
}
sb.append("\t}");
}
public String getProxyClassFullName() {
return mPakageName + "." + mBindingClassName;
}
public TypeElement getTypeElement() {
return typeElement;
}
}
分别用了mVariableElementMap、onClickMap保存BindView注解和OnClick注解。注意两者的元素类型为VariableElement、ExecutableElement分别对应修饰FIELD和METHOD的注解,具体Elments解析可以参考这里。
putElement()用于保存BindView注解;
putOnclickElement()用于保存OnClick注解;
之后使用generateBindViewMethod()方法遍历BindView注解,获取注解的值(id)、被注解的变量名,拼接生成findviewbyid代码。OnClick注解同理,最终会在 xxx_ViewBinding类中生成bindView()和bindOnClick()方法。
*或者用javapoet来生成代码,这里就不作介绍了。
2.3 添加auto-service依赖
implementation 'com.google.auto.service:auto-service:1.0-rc2'
//gradle3.4.1以上必须多加这一句,否则不会执行apt注解器
annotationProcessor 'com.google.auto.service:auto-service:1.0-rc2'
最后MyProcessor前面添加注解,表示使用该类处理注解:
@AutoService(Processor.class)
public class MyProcessor extends AbstractProcessor {
....
注意!别忘了加annotationProcessor这一句!gradle3.1.2以下不需要加,高版本必须加,否则就算加了AutoService
也不会处理注解!我这里用的是3.4.1,具体看自己版本。
三、生成代码
在app build.gradle下添加依赖:
annotationProcessor project(path: ':processor')
implementation project(path: ':myannotation')
注意:processor module使用的是annotationProcessor,否则编译时会报错。
在MainActivity中使用注解,是不是跟butterknife差不多呢,哈哈:
public class MainActivity extends AppCompatActivity {
@BindView(R.id.text1)
TextView text1;
@OnClick(R.id.button)
void onButtonClick() {
Toast.makeText(this, "onButtonClick", Toast.LENGTH_SHORT).show();
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
BindViewUtils.bindView(this);
text1.setText("this is apt sample");
}
}
添加完注解之后rebuildProject可以看到generatedJava->com.example.xxx下面生成了相应的MainActivity_ViewBinding类:
四、调用生成的代码
可以看到上面有个BindViewUtils.bindView(this)方法,实际上只是简单的反射调用:
public class BindViewUtils {
public static void bindView(Activity activity) {
Class clazz = activity.getClass();
try {
Class<?> bindViewClass = Class.forName(clazz.getName() + "_ViewBinding");
Method bindView = bindViewClass.getMethod("bindView", activity.getClass());
bindView.invoke(bindViewClass.newInstance(),activity);
Method bindOnClick = bindViewClass.getMethod("bindOnClick", activity.getClass());
bindOnClick.invoke(bindViewClass.newInstance(),activity);
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
}
反射获得MainActivity_ViewBinding类,并调用了里面的bindView(),bindOnClick()方法。
五、结果
原本的helloworld变成了this is apt sample,按钮点击也弹出了Toast onButtonClick。另外butterknife的@OnCheckedChanged、@OnPageChange、@OnItemClick等原理都差不多,就不一一实现了。