欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Android使用APT编译时注解生成代码

程序员文章站 2022-05-29 22:01:04
...

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,里面用来存放注解。

Android使用APT编译时注解生成代码

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()方法可以用下面两个注解替代。

@AutoService(Processor.class),生成 META-INF 信息;
@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一下工程,会发现下面的文件夹多了个文件(因为工程里面多写了一个注解类,所以生成了多个)

Android使用APT编译时注解生成代码

打开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

最后感谢以下同学提供的参考:

秒懂,Java 注解 (Annotation)你可以这样学

Android 如何编写基于编译时注解的项目

《从0到1:实现 Android 编译时注解》