Android使用APT编译时注解生成代码
1.前言
最近在使用Butterknife的时候感觉它使用的注解挺有意思的,就了解一下,顺便自己花点时间实现一个类似的框架。加深对这块的理解,下面上干货。
2.注解
注解和class、interface一样属于一种类型。是在javaSE5.0后引入的概念。
注解通过关键字 @interface 进行定义:
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.CLASS)
public @interface BindView {
int value();
}
元注解是可以注解到注解上的注解,是一种基本注解。元注解有 @Retention、@Documented、@Target、@Inherited、@Repeatable 5 种。@Retention
Retention 的英文意为保留期的意思。当 @Retention 应用到一个注解上的时候,它解释说明了这个注解的的存活时间。
它的取值如下:
- RetentionPolicy.SOURCE 注解只在源码阶段保留,在编译器进行编译时它将被丢弃忽视。
- RetentionPolicy.CLASS 注解只被保留到编译进行的时候,它并不会被加载到 JVM 中。
- RetentionPolicy.RUNTIME 注解可以保留到程序运行的时候,它会被加载进入到 JVM 中,所以在程序运行时可以获取到它们。
@Documented
这个元注解肯定是和文档有关。它的作用是能够将注解中的元素包含到 Javadoc 中去。
@Target
Target 是目标的意思,@Target 指定了注解运用的地方。
你可以这样理解,当一个注解被 @Target 注解时,这个注解就被限定了运用的场景。
类比到标签,原本标签是你想张贴到哪个地方就到哪个地方,但是因为 @Target 的存在,它张贴的地方就非常具体了,比如只能张贴到方法上、类上、方法参数上等等。@Target 有下面的取值
- ElementType.ANNOTATION_TYPE 可以给一个注解进行注解
- ElementType.CONSTRUCTOR 可以给构造方法进行注解
- ElementType.FIELD 可以给属性进行注解
- ElementType.LOCAL_VARIABLE 可以给局部变量进行注解
- ElementType.METHOD 可以给方法进行注解
- ElementType.PACKAGE 可以给一个包进行注解
- ElementType.PARAMETER 可以给一个方法内的参数进行注解
- ElementType.TYPE 可以给一个类型进行注解,比如类、接口、枚举
@Inherited
Inherited 是继承的意思,但是它并不是说注解本身可以继承,而是说如果一个超类被 @Inherited 注解过的注解进行注解的话,那么如果它的子类没有被任何注解应用的话,那么这个子类就继承了超类的注解。
@Repeatable
Repeatable 自然是可重复的意思。@Repeatable 是 Java 1.8 才加进来的,所以算是一个新的特性。
什么样的注解会多次应用呢?通常是注解的值可以同时取多个。
注解属性
注解的属性也叫做成员变量。注解只有成员变量,没有方法。注解的成员变量在注解的定义中以“无形参的方法”形式来声明,其方法名定义了该成员变量的名字,其返回值定义了该成员变量的类型。下面的注解定义了value属性。在使用的时候应该给它赋值
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Profile {
public int id() default -1;
public int heigh() default 0;
public String nativePlace() default "";
}
public class Person {
@Profile(id = 23,heigh = 180,nativePlace = "中国")
String profile;
}
注解属性的提取一般通过反射来获取注解通过反射获取。首先可以通过 Class 对象的 isAnnotationPresent() 方法判断它是否应用了某个注解
public boolean isAnnotationPresent(Class<? extends Annotation> annotationClass) {}
然后通过 getAnnotation() 方法来获取 Annotation 对象。
public <A extends Annotation> A getAnnotation(Class<A> annotationClass) {}
或者是 getAnnotations() 方法。
public Annotation[] getAnnotations() {}
拿到Annotation对象后就可以获取到里面的值
public class CustomUtils {
public static String getInfo(Class<?> clazz){
String str = "";
Field[] fields = clazz.getFields();
for (Field field:
fields) {
if(field.isAnnotationPresent(Name.class)){
Name arg0 = field.getAnnotation(Name.class);
str = name + arg0.value() + "==";
}else if(field.isAnnotationPresent(Sex.class)){
Sex arg0 = field.getAnnotation(Sex.class);
str = str + sex + arg0.sex() + "==";
}else if(field.isAnnotationPresent(Profile.class)){
Profile arg0 = field.getAnnotation(Profile.class);
str = str + arg0.id() + ";" + arg0.heigh() + ";" + arg0.nativePlace();
}
}
return str;
}
3.Android使用APT(Annotation Processing Tool)
了解了以上关于注解的基本知识后,下面来在Android中使用APT进行开发。仿照Butterknife的结构,使用的gradle版本是3.0+,所以在build.gradle文件中使用的是 annotationProcessor。
基本思路就是使用注解标记某个域的属性进行赋值,然后在程序编译的时候自动生成对应的临时文件,最后通过反射把注解里面的值赋给被注解标记的域。
- 首先在Android studio中建立一个Java Library,这里我们取名为anno,里面用来存放注解。
package paic.com.anno;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.CLASS)
public @interface BindView {
int value();
}
- 新建一个Java Library
注意必须是Java,因为AbstractProcessor位于javax.annotation.processing这个包下面,Android工程没有。
引入一个第三方库和上面的java library
implementation 'com.google.auto.service:auto-service:1.0-rc2'
implementation project(':anno')
auto-service:Google 公司出品,用于自动为 JAVA Processor 生成 META-INF 信息
接下来的类是用于编译时生成java文件,新建一个类继承AbstractProcessor
@AutoService(Processor.class)
public class BindProcessor extends AbstractProcessor {
private Elements mElementUtils;
private Messager messager;
private Map<String,ProxyInfo> mProxyMap = new HashMap<>();
@Override
public synchronized void init(ProcessingEnvironment processingEnvironment) {
super.init(processingEnvironment);
mElementUtils = processingEnvironment.getElementUtils();
messager = processingEnvironment.getMessager();
}
@Override
public Set<String> getSupportedAnnotationTypes() {
Set<String> annotationType = new LinkedHashSet<>();
annotationType.add(BindView.class.getCanonicalName());
return annotationType;
}
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.RELEASE_7;
}
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
messager.printMessage(Diagnostic.Kind.NOTE,"process...");
mProxyMap.clear();
//获取所有标注了BindView的元素
Set<? extends Element> elements = roundEnvironment.getElementsAnnotatedWith(BindView.class);
//遍历元素
for(Element element:elements){
//判断是否是域
if(!checkAnnotationValid(element)){
return false;
}
//获取变量比如(button,textview...)
VariableElement variableElement = (VariableElement) element;
//获取变量所在的类(比如paic.com.annotation.ManinActivity)
TypeElement typeElement = (TypeElement) variableElement.getEnclosingElement();
//获取类名全称
String fqClassName = typeElement.getQualifiedName().toString();
ProxyInfo proxyInfo = mProxyMap.get(fqClassName);
if(proxyInfo == null){
proxyInfo = new ProxyInfo(mElementUtils,typeElement);
mProxyMap.put(fqClassName,proxyInfo);
}
BindView bindAnnotation = variableElement.getAnnotation(BindView.class);
int id = bindAnnotation.value();
proxyInfo.mInjectElements.put(id,variableElement);
}
for(String key:mProxyMap.keySet()){
ProxyInfo proxyInfo = mProxyMap.get(key);
try {//用于编译时创建java文件
JavaFileObject jfo = processingEnv.getFiler().createSourceFile(proxyInfo.getFullClassName(),proxyInfo.getTypeElement());
Writer writer = jfo.openWriter();
writer.write(proxyInfo.generateJavaCode());
writer.flush();
writer.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return true;
}
private boolean checkAnnotationValid(Element element){
if(element.getKind() != ElementKind.FIELD){
return false;
}
if(ClassValidator.isPrivate(element)){
return false;
}
return true;
}
}
其中最主要的是process方法,getSupportedAnnotationType()和getSupportedSourceVersion()方法可以用下面两个注解替代。
@SupportedAnnotationTypes({"com.example.BindView"}),声明 Processor 处理的注解,注意这是一个数组,表示可以处理多个注解;
@SupportedSourceVersion(SourceVersion.RELEASE_7),声明支持的源码版本
public class ProxyInfo {
private String packageName;
private String proxyClassName;
private TypeElement typeElement;
public Map<Integer,VariableElement> mInjectElements = new HashMap<>();
public static final String PROXY = "ViewInject";
public ProxyInfo(Elements elementUtils,TypeElement classElement){
this.typeElement = classElement;
PackageElement packageElement = elementUtils.getPackageOf(classElement);
String packageName = packageElement.getQualifiedName().toString(); //调用注解的类所在的包
String className = ClassValidator.getClassName(classElement,packageName); //调用注解的类名(MainActivity)
this.packageName = packageName;
this.proxyClassName = className + "$$" + PROXY; //生成的类名
}
public String generateJavaCode(){
StringBuilder builder = new StringBuilder();
builder.append("// Generated code,Do not modify!\n");
builder.append("package ").append(packageName).append(";\n\n");
// builder.append("import paic.com.lib.bind.*;\n");
builder.append('\n'); //类名的全称 paic.com.annotation.MainActivity
builder.append("public class ").append(proxyClassName).append(" implements " + PROXY + "<" + typeElement.getQualifiedName() + ">");
builder.append(" {\n");
generateJavaMethod(builder);
builder.append('\n');
builder.append("}\n");
return builder.toString();
}
public String generateJavaMethod(StringBuilder builder){
builder.append("@Override\n");
builder.append("public void inject(" + typeElement.getQualifiedName() +" host,Object source){\n");
for(int id:mInjectElements.keySet()){
VariableElement variableElement = mInjectElements.get(id);
String name = variableElement.getSimpleName().toString();//注解对应的参数(button)
String type = variableElement.asType().toString(); //注解对应参数的类型(android.widget.Button)
builder.append(" if(source instanceof android.app.Activity){\n");
builder.append("host."+name).append(" = ");
builder.append("("+type+")(((android.app.Activity)source).findViewById("+id+"));");
builder.append("\n}\n").append("else").append("\n{\n");
builder.append("host."+name).append(" = ");
builder.append("("+type+")(((android.view.View)source).findViewById("+id+"));");
builder.append("\n}\n");
}
builder.append("\n}\n");
return builder.toString();
}
public String getFullClassName(){
return packageName + "." + proxyClassName;
}
public TypeElement getTypeElement(){
return typeElement;
}
}
上面的代码逻辑还是比较容易看懂的
ProxyInfo这个类主要就是用来封装生成java文件的代码,通过id来区分自动构建相应的代码。
BindProcessor这个类则通过注解所在的类来生成构建对应的java文件。- 新建一个Android model
接下来新建一个Android model来使用注解。记得build.gradle里面需要这样配置:
annotationProcessor project(':lib')
compile project(':anno')//这样才能使用注解
我这里为了方便把反射注解生成的代码一起放在了这个工程
public interface ViewInject<T>
{
void inject(T t, Object source);
}
public class ViewInjector
{
private static final String SUFFIX = "$$ViewInject";
public static void injectView(Activity activity)
{
//获取生成的代理对象
ViewInject proxyActivity = findProxyActivity(activity);
//代理对象里的inject方法里面是实现的具体逻辑
//比如本例就是 activity.控件 = activity.findViewBy(id);
proxyActivity.inject(activity, activity);
}
public static void injectView(Object object, View view)
{
ViewInject proxyActivity = findProxyActivity(object);
proxyActivity.inject(object, view);
}
private static ViewInject findProxyActivity(Object activity)
{
try
{
Class clazz = activity.getClass();
Class injectorClazz = Class.forName(clazz.getName() + SUFFIX);
return (ViewInject) injectorClazz.newInstance();
} catch (ClassNotFoundException e)
{
e.printStackTrace();
} catch (InstantiationException e)
{
e.printStackTrace();
} catch (IllegalAccessException e)
{
e.printStackTrace();
}
throw new RuntimeException(String.format("can not find %s , something when compiler.", activity.getClass().getSimpleName() + SUFFIX));
}
}
新建一个Activity,进行调用
public class MainActivity extends AppCompatActivity {
@BindView(R.id.btn)
Button button;
@BindView(R.id.text)
TextView textView;
@BindString("fuck the world shit")
String word;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ViewInjector.injectView(this);
StringInjector.inject(this);
button.setText("this is a button");
textView.setText(word);
}
}
在AS中进行rebuild一下工程,会发现下面的文件夹多了个文件(因为工程里面多写了一个注解类,所以生成了多个)
打开ViewInject
// Generated code,Do not modify!
package paic.com.annotation;
public class MainActivity$$ViewInject implements ViewInject<paic.com.annotation.MainActivity> {
@Override
public void inject(paic.com.annotation.MainActivity host,Object source){
if(source instanceof android.app.Activity){
host.button = (android.widget.Button)(((android.app.Activity)source).findViewById(2131165218));
}
else
{
host.button = (android.widget.Button)(((android.view.View)source).findViewById(2131165218));
}
if(source instanceof android.app.Activity){
host.textView = (android.widget.TextView)(((android.app.Activity)source).findViewById(2131165306));
}
else
{
host.textView = (android.widget.TextView)(((android.view.View)source).findViewById(2131165306));
}
}
}
结合上面的BindProcessor和ProxyInfo,打个断点就能很清晰的知道这个文件是如何定义的。如何打断点后面会讲到。
然后run一下工程就能成功调用了!
4.调试APT代码
这部分《从0到1:实现 Android 编译时注解》 这个博客里有详细配置,我就不啰嗦了。
项目源码:DEMO
最后感谢以下同学提供的参考: