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和关联代码。这里简单自定义了一个注解:
配置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