Android编译时注解处理APT

Posted 殇神马

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android编译时注解处理APT相关的知识,希望对你有一定的参考价值。

一、注解

在使用Java语言开发的过程中,我们会经常看到各种各样的注解,@Override(表示方法的重写),@Deprecated(标记过时的元素 方法,类或属性),@LayoutRes(表示的是布局资源),@IdRes(表示的是ID资源),@DrawableRes(表示的图片资源)等等,另外在一些第三方库中,如Butterknife,EventBus,Retrofit中也会使用了很多自定义的注解;

那到底什么是注解?

注解本身是没有什么意义的,单独的注解就是一种注释或者说是一种标记,他要结合APT(编译时注解处理),字节码插桩,反射等这些技术使用才有意义;

使用注解的主要作用是什么?

(1)为了简化代码,减少模板代码的编写;

注解和APT结合,如Butterknife,我们使用一个@BindView的注解,就可以不用去写findViewById()的重复代码,如Retrofit中大量使用注解,我们可以通过简单的注解@POST,@GET,@Header就可以简洁方便的设置HTTP请求的方式,请求头,请求参数等;
所以注解的一个很大作用就是简化代码,减少模板代码的编写

(2)编写代码时可以进行语法的检查

像@LayoutRes(表示的是布局资源),@IdRes(表示的是ID资源),@DrawableRes(表示的图片资源),当我们的方法的参数中使用了注解,在编写代码时,传入的参数不是布局,ID,图片类型的整形数据时,是会立即有提示的;

(3)就是一个注释作用

@Override(表示方法的重写),@Deprecated(标记过时的元素 方法,类或属性)

注解的本质

自定义一个注解

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.SOURCE)      
public @interface MQBindView {
    int value();
}

注解对应的字节码

public abstract @interface com/example/annotationmodule/MQBindView implements java/lang/annotation/Annotation {


  @Ljava/lang/annotation/Target;(value={Ljava/lang/annotation/ElementType;.FIELD})

  @Ljava/lang/annotation/Retention;(value=Ljava/lang/annotation/RetentionPolicy;.SOURCE)

  // access flags 0x401
  public abstract value()I
}

查看注解的字节码可以看到,注解本质上就是一个实现了Annotation接口的接口

元注解

元注解是对注解的注解

(1)@Target

表明注解可以应用的Java元素类型

ElementType.TYPE 应用于类,接口,枚举

ElementType.FIELD 应用于属性(包括枚举中的变量)

ElementType.METHOD 应用于方法

ElementType.PARAMETER 应用于方法中的形参

ElementType.CONSTRUCTOR 应用于构造方法

ElementType.LOCAL_VARIABLE 应用于局部变量

ElementType.ANNOTATION_TYPE 应用于注解类型

ElementType.PACKAGE 应用于包

(2)@Retention

表明注解的生命周期

RetentionPolicy.SOURCE 即在源文件中保留,编译之后就没有了;

RetentionPolicy.CLASS 保留到编译后的字节码文件中,但是在类加载进虚拟机时就会被忽略了;

RetentionPolicy.RUNTIME 保留到程序运行时,在类加载到虚拟机中时,仍然保留,可以通过反射获取;

(3)@Document

使用了@Document注解的注解,表示该标记的元素可以被JavaDoc或类似工具文档化

(4)Inherited

使用Inherited注解标记的注解,所标记的类的子类也会拥有该注解

自定义注解

自定义注解,要使用@Interface关键字修饰

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.SOURCE)
public @interface MQBindView {
    int value();
}

二、APT

1.APT

上面对 什么是注解,以及注解在我们平时写代码中的作用有了了解和总结,下面我们就开始学习一下APT,编译时注解处理技术;

APT即Annotations Processing Tool,注解处理工具,APT就是javac的一个工具,在程序编译时,会对我们代码中的所有注解进行扫描和处理,注解处理工具最终生成处理注解逻辑的.java源代码文件,减少模板代码的编写,提高编码效率,使得代码更为简介,可读性高

2.使用APT的步骤

(1)创建自定义注解

(2)创建注解处理器 继承自AbstractProcessor,生成处理注解逻辑的.java文件,封装一个供外部调用的API接口,也就是调用第二步中生成的注解处理逻辑文件中的方法,实现逻辑处理

(3)然后在需要使用的Module中依赖注解,使用自定义注解处理器,然后调用API接口即可

下面我们以手写实现一个简易的Butterknife为例,来实际使用一下APT, 我们这里主要以通过注解的方式实现findViewById()的操作;

2.1 创建自定义注解

我们首先创建一个Java Library Module,名称为AnnotationModule,在该Module下定义一个注解@MQBindView

 @Target(ElementType.FIELD)
      @Retention(RetentionPolicy.SOURCE)
      public @interface MQBindView {
         int value();
      } 

2.2 自定义注解处理器

创建一个Java Library Module,名称为APTModule,定义一个类继承自AbstractProcessor;
这里自定义处理器里面会用到我们自定义的注解,所以这个Module的build.gradle里面要依赖AnnotationModule

dependencies {
       implementation project(path: ':AnnotationModule')
      }
 public class MQProcessor extends AbstractProcessor {

       @Override
       public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {

       }

    //重写getSupportedAnnotationTypes方法,添加支持处理的注解类型
    @Override
    public Set<String> getSupportedAnnotationTypes() {
        HashSet<String> supportTypes = new LinkedHashSet<>();
        supportTypes.add(MQBindView.class.getCanonicalName());
        return supportTypes;
    }

    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }
      
      }

自定义注解处理器一定要重写getSupportedAnnotationTypes方法,添加需要处理的注解类型

在编写注解处理逻辑之前,这里我先对Processor中的一些基本概念进行一下了解;

我们的app Module中如果依赖了这个APTModule,当我们如果build(编译)项目的时候,这个Processor的process方法就会被执行;

当我们想要在这个Processor中添加一些打印信息,我们需要用Messager(javax.annotation.processing.Messager)去打印,打印出来的内容,在build的时候,会在下方的build视图中打印出来,这个build视图,正常会显示的是编译的过程信息,我们使用Messager打印的信息,也会在这里打印出来;

public class MQProcessor extends AbstractProcessor {
        

        Messager mMessager;

        @Override
        public synchronized void init(ProcessingEnvironment processingEnv) {

        super.init(processingEnv);

        mMessager = processingEnv.getMessager();

        mMessager.printMessage(Diagnostic.Kind.NOTE, "----------->init");

        }


          @Override
          public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {

           mMessager.printMessage(Diagnostic.Kind.NOTE, "----------->process");

          }

       }

在这里插入图片描述
Element

Element代表程序的一个元素,这个元素可以是:包、类/接口、属性变量、方法/方法形参、泛型参数,Element是java-apt( 编译时注解处理器)技术的基础,在编译期间可以获取元素的各类信息,结合APT技术动态生成代码实现相应的逻辑处理;

Element接口中的一些方法:

getEnclosingElement()

获取包含该Element的父Element

public class MainActivity extends AppCompatActivity {

        @MQBindView(R.id.tv_activity_main)
        TextView mTextView;

        }

这里,当我们拿到使用了@MQBindView这个注解的Element(mTextView),然后通过getEnclosingElement()方法就能获取到父Element,这里父Element就是MainActivity

process方法中,有一个参数RoundEnvironment,通过这个参数可以获取包含特定注解的被注解元素

   Set<? extends Element> elementsAnnotatedWith = roundEnvironment.getElementsAnnotatedWith(MQBindView.class);

init方法中有一个ProcessingEnvironment参数,通过这个参数可以获取元素所在的包

String packageName = mProcessingEnv.getElementUtils().getPackageOf(element).toString();

Filer

Filer是文件生成器,可以用来生成Java源文件等

JavaFileObject sourceFile = mFiler.createSourceFile(文件名);

这里文件名,要是创建的类的全类名

  JavaFileObject sourceFile = mFiler.createSourceFile(
                        packageName + "." + activityName + "_ViewBinding"
                );

这样创建文件之后,编译项目之后,会在app/build/generated/ap_generated_sources/debug/out/应用包名/下生成这个Java文件

Writer

我们创建Java源文件对象 JavaFileObject之后可以通过openWriter()方法获取文件的Writer对象,然后就可以通过Writer对象往文件里面写内容

而我们要实现Butterknife一样的通过注解的形式,帮我们实现findViewById()的功能,我们查看Butterknifer就知道实际上Butterknife就是使用了APT技术,在编译时给每一个使用了@BindView注解的类都生成了一个对应的Java源文件,名字为使用了注解的类的类名加上 _ViewBinding,比如我们的MainActivity使用了注解@BindView,则编译之后会生成MainActivity_ViewBinding这个Java源文件,生成的Java源文件就在app/build/generated/ap_generated_sources/debug/out/应用包名/ 这个目录下,也即新建了一个MainActivity_ViewBinding类,这个类的构造方法里面就会对MainActivtiy里面的所有使用了@BindView的View成员进行findViewById的操作,如下图所示

ButterKnife通过APT生成的Java源文件

在这里插入图片描述

Buterknife.bind(Activity)这个方法的实现如下

在这里插入图片描述
在这里插入图片描述
查看Butterknife的源码可知,Butterknife.bind(Activity)的实现,就是利用反射通过Class获取生成的Java源文件中定义的类的Class对象,然后获取Class的构造方法,通过反射,创建生成的Java类的对象,从而触发构造方法,触发findViewById相关代码;

了解了Butterknife的实现原理,那我们自己就可以照着这个逻辑去实现;

我们在自己自定义的注解处理器的process方法中,实现给每一个使用了@MQBindView的注解的类都生成一个对应的Java源文件,Java 源文件中的内容,为定义一个对应的类,类中的构造方法需要一个Activtiy参数,构造方法中的内容为给类中每一个使用了@BindView注解的成员进行findViewById的操作;

public class MQProcessor extends AbstractProcessor {

    private static final String TAG = "MQProcessor";

    Filer mFiler;
    ProcessingEnvironment mProcessingEnv;
    Messager mMessager;

    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
        mFiler = processingEnv.getFiler();
        mProcessingEnv = processingEnv;
        mMessager = processingEnv.getMessager();
        mMessager.printMessage(Diagnostic.Kind.NOTE, "----------->init");
    }

    @Override
    public Set<String> getSupportedAnnotationTypes() {
        HashSet<String> supportTypes = new LinkedHashSet<>();
        supportTypes.add(MQBindView.class.getCanonicalName());
        return supportTypes;
    }

    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }

    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {

        mMessager.printMessage(Diagnostic.Kind.NOTE, "----------->process");

        //我们的源代码中所有使用了@MQBindView注解的元素
        Set<? extends Element> elementsAnnotatedWith = roundEnvironment.getElementsAnnotatedWith(MQBindView.class);
		//定义一个Map集合,以每一个Activity类名为键,这个Activtiy类中使用了@MQBindViwe注解的元素列表为值	
        Map<String, List<VariableElement>> map = new HashMap<>();

        for (Element element : elementsAnnotatedWith) {

            VariableElement variableElement = (VariableElement) element;

            //Activity的名字
            String activityName = variableElement.getEnclosingElement().getSimpleName().toString();

            List<VariableElement> variableElements;

            if (map.get(activityName) != null) {
                variableElements = map.get(activityName);
                variableElements.add(variableElement);
            } else {
                variableElements = new ArrayList<>();
                variableElements.add(variableElement);
                map.put(activityName, variableElements);
            }

        }

        Writer writer = null;
        
		//给每一个使用了@MQVindView注解的类(Activity/Fragment等)都生成对应的Java源文件
        for (Map.Entry<String, List<VariableElement>> stringListEntry : map.entrySet()) {

            List<VariableElement> variableElementList = stringListEntry.getValue();

            TypeElement enclosingTypeElement = (TypeElement) variableElementList.get(0).getEnclosingElement();

            String packageName =
                    mProcessingEnv.getElementUtils().getPackageOf(enclosingTypeElement).toString();

            String activityName = variableElementList.get(0).getEnclosingElement().getSimpleName().toString();

            try {
                mMessager.printMessage(Diagnostic.Kind.NOTE, "----------->process:" + mFiler);

                JavaFileObject sourceFile = mFiler.createSourceFile(
                        packageName + "." + activityName + "_ViewBinding"
                );

         		/**
         		* Java源文件里的内容为定义一个类,类名就为使用了@BindView的注解的Activity的类名加_ViewBinding
         		* 然后构造方法有一个Activity类型的target参数,构造方法里面给这个Activity中使用了@BindView注解                的View成员进行dinfViewByid操作 
         		*
         		*/
                writer = sourceFile.openWriter();
                writer.write("package " + packageName + ";\\n");
                writer.write("import android.view.View;\\n");
                writer.write("public class " + activityName + "_ViewBinding {\\n");
                writer.write("public " + activityName + "_ViewBinding (" + activityName + " target){\\n");
                writer.write("View decorView = target.getWindow().getDecorView();\\n");

                for (VariableElement variableElement : variableElementList) {
                    String variableName = variableElement.getSimpleName().toString();
                    int value = variableElement.getAnnotation(MQBindView.class).value();
                    writer.write("target." + variableName + "=decorView.findViewById(" + value + ");\\n");
                }
                writer.write("}\\n");
                writer.write("}\\n");
                try {
                    mMessager.printMessage(Diagnostic.Kind.NOTE, "----------->process:" + writer);

					//最终Writer这个写对象,要关闭,否则内容是不会写进创建的Java源文件中的	
                    writer.close();
                } catch (IOException exception) {
                    exception.printStackTrace();
                }
            } catch (Exception exception) {
                exception.printStackTrace();
            }
        }

        mMessager.printMessage(Diagnostic.Kind.NOTE, "----------->process:end");
        return false;
    }

}

2.3 配置注解处理器

自定义处理器代码编写好之后,还有一个非常重要的步骤,就是配置注解处理器
一共有两种方式:

第一种:

在自定义注解处理器所在的Module,APTModule的src/main目录下新建一个 resources/META-INF/services
三级目录,然后在services目录下新建一个名字为javax.annotation.processing.Processor的文件
文件里面的内容,写上自定义处理器的全类名即可

在这里插入图片描述
第二种:

通过依赖google的auto-services库,帮我们自动生成META-INF/services/javax.annotation.processing.Processor 文件;
在自定义处理器所在的Module,APTModule的build.gradle添加auto-services依赖

 dependencies {
    implementation 'com.google.auto.service:auto-service:1.0-rc6'
    annotationProcessor 'com.google.auto.service:auto-service:1.0-rc6'
    
    implementation project(path: ':AnnotationModule')
}

然后在我们编写的MQProcessor上方使用@AutoService注解

在这里插入图片描述
然后编译代码,我们可以看到在APTModule的build/classess/java/main/下会生成 META-INF/services/javax.annotation.processing.Processor文件,文件里面的内容就是com.example.aptmodule.MQProcessor

在这里插入图片描述

所以也就成功配置了依赖处理器,然后我们build一下项目,查看app/build/generated/ap_generated_sources/debug/out/应用包名/下成功生成了对应的Java源文件,表示注解处理器生效了

在这里插入图片描述

2.4 定义一个使用入口,给使用者调用

我们这里定义了一个MQButterKnife类,提供了一个静态方法bind(Activity), bind 方法里面就是通过反射创建Activity对应生成的 Activity_ViewBinding对象触发findViewById操作

public class MQButterKnife {

    private static final String TAG = "MQButterKnife";

    public static void bind(Activity activity) {

        String viewBindingClassName =
                activity.getClass().getCanonicalName() + "_ViewBinding";
        Log.e(TAG, "bind:" + viewBindingClassName);
        try {
            Class<?> aClass = Class.forName(viewBindingClassName);
            Log.e(TAG, "bind: " + aClass);
            Constructor<?> constructor = aClass.getConstructor(activity.getClass());
            Object o = constructor.newInstance(activity);
            Log.e(TAG, "bind: " + o);
        } catch (Exception e) {
            e.printStackTrace();
        }

    }

}

2.5 使用

在app主Module里面先依赖AnnotationModule,以及使用APTModule注解模块,在主Module的build.gradle里面添加如下,注意自定义处理器模板的依赖方式是通过annotationProcessor

dependencies {
   
    implementation project(path: ':AnnotationModule')

    annotationProcessor project(':APTModule')

}

然后在我们的Activty中去使用我们自定义的MQButterknife,实现View的findViewById操作

 public class MainActivity extends AppCompatActivity {

    @MQBindView(R.id.tv_activity_main)
    TextView mTextView;
    private static final String TAG = "MainActivity";

    @Override
    protected void onCreate(Bundle savedInstanceState) {

        super.onCreate(savedInstanceState);

        setContentView(R.layout.activity_main);

        MQButterKnife.bind(this);

        mTextView.setText("测试");

    }

}

运行项目,mTextView成功显示 “测试”,说明我们的简单版本Bunnerknife成功实现了

在这里插入图片描述
这样我们就成功的在Android项目中利用APT编译

以上是关于Android编译时注解处理APT的主要内容,如果未能解决你的问题,请参考以下文章

Android APT注解处理器 ( 注解标注 与 初始化方法 )

Android编译时注解处理APT

Android编译时注解处理APT

Android编译时注解处理APT

Android编译时注解处理APT

Android APT编译时技术 ( ButterKnife 原理分析 )