Android APT最佳实践

在之前的 《纯手写路由框架实现Android组件化》 中讲到了Android APT技术,并且在讲解视频中使用APT了技术,加以JavaPoet辅助代码生成实现了一个最简单的基于注解的View注入(其实就是省略了大量的findViewById方法)。如果只是单纯的想体验APT技术带来的便捷性,那么这篇文章非常适合,APT从讲是一个编译期的注解处理工具(Annotation Processing Tool)。一些主流的三方库,如ButterKnife、EventBus等都用到了这个技术来生成代码。

下面的内容转载自 《Android APT(编译时代码生成)最佳实践》 ,其中进行了部分删改

APT

APT(Annotation Processing Tool)是一种处理注释的工具,它对源代码文件进行检测找出其中的Annotation,使用Annotation进行额外的处理。 Annotation处理器在处理Annotation时可以根据源文件中的Annotation生成额外的源文件和其它的文件(文件具体内容由Annotation处理器的编写者决定),APT还会编译生成的源文件和原来的源文件,将它们一起生成class文件。

APT HelloWorld

创建Annotation Module

首先,我们需要新建一个名称为annotation的Java Library,主要放置一些项目中需要使用到的Annotation和关联代码。这里简单自定义了一个注解:

1@Target(ElementType.TYPE)
2@Retention(RetentionPolicy.CLASS) 
3public @interface Test {   }

配置build.gradle,主要是规定JDK版本

 1plugins {
 2    id 'java-library'
 3}
 4
 5// 控制台中文设置UTF-8
 6tasks.withType(JavaCompile){
 7    options.encoding = "UTF-8"
 8}
 9
10java {
11    sourceCompatibility = JavaVersion.VERSION_1_8
12    targetCompatibility = JavaVersion.VERSION_1_8
13}

创建Compiler Module

创建一个名为compiler的Java Library,这个类将会写代码生成的相关代码。核心就是在这里,配置build.gradle:

 1plugins {
 2    id 'java-library'
 3}
 4
 5dependencies {
 6    implementation fileTree(dir: 'libs', includes: ['*.jar'])
 7
 8    // 编译时期进行注解处理
 9    annotationProcessor 'com.google.auto.service:auto-service:1.0-rc4'
10    compileOnly 'com.google.auto.service:auto-service:1.0-rc4'
11
12    // 帮助我们通过类调用的方式来生成Java代码[JavaPoet]
13    implementation 'com.squareup:javapoet:1.10.0'
14
15    // 依赖于注解
16    implementation project(':annotation')
17}
18
19// 控制台中文设置UTF-8
20tasks.withType(JavaCompile){
21    options.encoding = "UTF-8"
22}
23
24java {
25    sourceCompatibility = JavaVersion.VERSION_1_8
26    targetCompatibility = JavaVersion.VERSION_1_8
27}

1、定义编译的jdk版本为1.8,这个很重要,不写会报错。 2、AutoService 主要的作用是注解 processor 类,并对其生成 META-INF 的配置信息。 3、JavaPoet 这个库的主要作用就是帮助我们通过类调用的形式来生成代码。 4、依赖上面创建的annotation Module。

定义Processor类

生成代码相关的逻辑就放在这里。

 1@AutoService(Processor.class)
 2public class TestProcessor extends AbstractProcessor {
 3    @Override
 4    public Set<String> getSupportedAnnotationTypes() {
 5        return Collections.singleton(Test.class.getCanonicalName());
 6    }
 7    @Override
 8    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
 9        return false;
10    }
11}

生成第一个类,我们接下来要生成下面这个HelloWorld的代码:

1package com.example.helloworld;
2public final class HelloWorld {
3    public static void main(String[] args) {
4        System.out.println("Hello, JavaPoet!");
5    }
6}

修改上述TestProcessor的process方法

 1@Override
 2public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
 3    MethodSpec main = MethodSpec.methodBuilder("main")
 4            .addModifiers(Modifier.PUBLIC, Modifier.STATIC)
 5            .returns(void.class)
 6            .addParameter(String[].class, "args")
 7            .addStatement("$T.out.println($S)", System.class, "Hello, JavaPoet!")
 8            .build();
 9    TypeSpec helloWorld = TypeSpec.classBuilder("HelloWorld")
10            .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
11            .addMethod(main)
12            .build();
13    JavaFile javaFile = JavaFile.builder("com.example.helloworld", helloWorld)
14            .build();
15    try {
16        javaFile.writeTo(processingEnv.getFiler());
17    } catch (IOException e) {
18        e.printStackTrace();
19    }
20    return false;
21}

在app中使用

配置app模块的build.gradle

1dependencies {
2
3    ......
4
5    implementation project(':annotation')
6    annotationProcessor project(':annotation-processor')
7}

在随意一个类添加@Test注解,比如在MainActivity中:

1@Test
2public class MainActivity extends AppCompatActivity {
3    @Override
4    protected void onCreate(Bundle savedInstanceState) { 
5        super.onCreate(savedInstanceState);
6        setContentView(R.layout.activity_main);
7    }
8}

点击Android Studio的ReBuild Project,可以在在app的build/generated/ap_generated_sources/debug/out目录下,即可看到生成的代码。

基于注解的View注入:DIActivity

到目前我们还没有使用注解,上面的@Test也没有实际用上,下面我们做一些更加实际的代码生成。实现基于注解的View,代替项目中的findByView。这里仅仅是学习怎么用APT,如果真的想用DI框架,推荐使用ButterKnife,功能全面。

第一步,在annotation module创建@DIActivity、@DIView注解。

 1package cn.tim.annotation;
 2
 3import java.lang.annotation.ElementType;
 4import java.lang.annotation.Retention;
 5import java.lang.annotation.RetentionPolicy;
 6import java.lang.annotation.Target;
 7
 8@Target(ElementType.TYPE)
 9@Retention(RetentionPolicy.CLASS)
10public @interface DIActivity {
11
12}
 1package cn.tim.annotation;
 2
 3import java.lang.annotation.ElementType;
 4import java.lang.annotation.Retention;
 5import java.lang.annotation.RetentionPolicy;
 6import java.lang.annotation.Target;
 7
 8@Target(ElementType.FIELD)
 9@Retention(RetentionPolicy.RUNTIME)
10public @interface DIView {
11    int value() default 0;
12}

创建DIProcessor方法

 1package cn.tim.annotation_processor;
 2
 3import com.google.auto.service.AutoService;
 4import com.squareup.javapoet.ClassName;
 5import com.squareup.javapoet.JavaFile;
 6import com.squareup.javapoet.MethodSpec;
 7import com.squareup.javapoet.TypeName;
 8import com.squareup.javapoet.TypeSpec;
 9
10import java.io.IOException;
11import java.util.Collections;
12import java.util.List;
13import java.util.Set;
14
15import javax.annotation.processing.AbstractProcessor;
16import javax.annotation.processing.ProcessingEnvironment;
17import javax.annotation.processing.Processor;
18import javax.annotation.processing.RoundEnvironment;
19import javax.lang.model.SourceVersion;
20import javax.lang.model.element.Element;
21import javax.lang.model.element.Modifier;
22import javax.lang.model.element.TypeElement;
23import javax.lang.model.util.Elements;
24
25import cn.tim.annotation.DIActivity;
26import cn.tim.annotation.DIView;
27
28@AutoService(Processor.class)
29public class DIProcessor extends AbstractProcessor {
30    private Elements elementUtils;
31    @Override
32    public Set<String> getSupportedAnnotationTypes() {
33        // 规定需要处理的注解
34        return Collections.singleton(DIActivity.class.getCanonicalName());
35    }
36    @Override
37    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
38        System.out.println("DIProcessor");
39        Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(DIActivity.class);
40        for (Element element : elements) {
41            // 判断是否Class
42            TypeElement typeElement = (TypeElement) element;
43            List<? extends Element> members = elementUtils.getAllMembers(typeElement);
44            MethodSpec.Builder bindViewMethodSpecBuilder = MethodSpec.methodBuilder("bindView")
45                    .addModifiers(Modifier.PUBLIC, Modifier.STATIC)
46                    .returns(TypeName.VOID)
47                    .addParameter(ClassName.get(typeElement.asType()), "activity");
48            for (Element item : members) {
49                DIView diView = item.getAnnotation(DIView.class);
50                if (diView == null){
51                    continue;
52                }
53                bindViewMethodSpecBuilder.addStatement(String.format("activity.%s = (%s) activity.findViewById(%s)",item.getSimpleName(),ClassName.get(item.asType()).toString(),diView.value()));
54            }
55            TypeSpec typeSpec = TypeSpec.classBuilder("DI" + element.getSimpleName())
56                    .superclass(TypeName.get(typeElement.asType()))
57                    .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
58                    .addMethod(bindViewMethodSpecBuilder.build())
59                    .build();
60            JavaFile javaFile = JavaFile.builder(getPackageName(typeElement), typeSpec).build();
61            try {
62                javaFile.writeTo(processingEnv.getFiler());
63            } catch (IOException e) {
64                e.printStackTrace();
65            }
66        }
67        return true;
68    }
69    private String getPackageName(TypeElement type) {
70        return elementUtils.getPackageOf(type).getQualifiedName().toString();
71    }
72    @Override
73    public synchronized void init(ProcessingEnvironment processingEnv) {
74        super.init(processingEnv);
75        elementUtils = processingEnv.getElementUtils();
76    }
77    @Override
78    public SourceVersion getSupportedSourceVersion() {
79        return SourceVersion.RELEASE_8;
80    }
81}

使用DIActivity

 1package cn.tim.apt_demo;
 2
 3import androidx.appcompat.app.AppCompatActivity;
 4
 5import android.os.Bundle;
 6import android.widget.TextView;
 7
 8import cn.tim.annotation.DIActivity;
 9import cn.tim.annotation.DIView;
10
11@DIActivity
12public class MainActivity extends AppCompatActivity {
13
14    @DIView(value = R.id.text)
15    TextView textView;
16
17    @DIView(value = R.id.text1)
18    TextView textView1;
19
20    @DIView(value = R.id.text2)
21    TextView textView2;
22
23    @Override
24    protected void onCreate(Bundle savedInstanceState) {
25        super.onCreate(savedInstanceState);
26        setContentView(R.layout.activity_main);
27        DIMainActivity.bindView(this);
28        textView.setText("Hello, JavaPoet!");
29
30        textView2.setText("Tim");
31    }
32}

实际上就是通过apt生成了以下代码:

1public final class DIMainActivity extends MainActivity {
2  public static void bindView(MainActivity activity) {
3    activity.textView = (android.widget.TextView) activity.findViewById(2131231086);
4    activity.textView1 = (android.widget.TextView) activity.findViewById(2131231087);
5    activity.textView2 = (android.widget.TextView) activity.findViewById(2131231088);
6  }
7}

示例代码: Github -> aptdemo

参考资料

《Android APT(编译时代码生成)最佳实践》