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

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


打开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 编译时注解》

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值