0%

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
2
3
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.CLASS)
public @interface Test { }

配置build.gradle,主要是规定JDK版本
1
2
3
4
5
6
7
8
9
10
11
12
13
plugins {
id 'java-library'
}

// 控制台中文设置UTF-8
tasks.withType(JavaCompile){
options.encoding = "UTF-8"
}

java {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}

创建Compiler Module

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
plugins {
id 'java-library'
}

dependencies {
implementation fileTree(dir: 'libs', includes: ['*.jar'])

// 编译时期进行注解处理
annotationProcessor 'com.google.auto.service:auto-service:1.0-rc4'
compileOnly 'com.google.auto.service:auto-service:1.0-rc4'

// 帮助我们通过类调用的方式来生成Java代码[JavaPoet]
implementation 'com.squareup:javapoet:1.10.0'

// 依赖于注解
implementation project(':annotation')
}

// 控制台中文设置UTF-8
tasks.withType(JavaCompile){
options.encoding = "UTF-8"
}

java {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}

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

定义Processor类

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

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

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

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

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

在app中使用

配置app模块的build.gradle

1
2
3
4
5
6
7
dependencies {

......

implementation project(':annotation')
annotationProcessor project(':annotation-processor')
}

在随意一个类添加@Test注解,比如在MainActivity中:
1
2
3
4
5
6
7
8
@Test
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
}

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

基于注解的View注入:DIActivity

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

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

1
2
3
4
5
6
7
8
9
10
11
12
package cn.tim.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.CLASS)
public @interface DIActivity {

}

1
2
3
4
5
6
7
8
9
10
11
12
package cn.tim.annotation;

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.RUNTIME)
public @interface DIView {
int value() default 0;
}

创建DIProcessor方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
package cn.tim.annotation_processor;

import com.google.auto.service.AutoService;
import com.squareup.javapoet.ClassName;
import com.squareup.javapoet.JavaFile;
import com.squareup.javapoet.MethodSpec;
import com.squareup.javapoet.TypeName;
import com.squareup.javapoet.TypeSpec;

import java.io.IOException;
import java.util.Collections;
import java.util.List;
import java.util.Set;

import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.ProcessingEnvironment;
import javax.annotation.processing.Processor;
import javax.annotation.processing.RoundEnvironment;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.TypeElement;
import javax.lang.model.util.Elements;

import cn.tim.annotation.DIActivity;
import cn.tim.annotation.DIView;

@AutoService(Processor.class)
public class DIProcessor extends AbstractProcessor {
private Elements elementUtils;
@Override
public Set<String> getSupportedAnnotationTypes() {
// 规定需要处理的注解
return Collections.singleton(DIActivity.class.getCanonicalName());
}
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
System.out.println("DIProcessor");
Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(DIActivity.class);
for (Element element : elements) {
// 判断是否Class
TypeElement typeElement = (TypeElement) element;
List<? extends Element> members = elementUtils.getAllMembers(typeElement);
MethodSpec.Builder bindViewMethodSpecBuilder = MethodSpec.methodBuilder("bindView")
.addModifiers(Modifier.PUBLIC, Modifier.STATIC)
.returns(TypeName.VOID)
.addParameter(ClassName.get(typeElement.asType()), "activity");
for (Element item : members) {
DIView diView = item.getAnnotation(DIView.class);
if (diView == null){
continue;
}
bindViewMethodSpecBuilder.addStatement(String.format("activity.%s = (%s) activity.findViewById(%s)",item.getSimpleName(),ClassName.get(item.asType()).toString(),diView.value()));
}
TypeSpec typeSpec = TypeSpec.classBuilder("DI" + element.getSimpleName())
.superclass(TypeName.get(typeElement.asType()))
.addModifiers(Modifier.PUBLIC, Modifier.FINAL)
.addMethod(bindViewMethodSpecBuilder.build())
.build();
JavaFile javaFile = JavaFile.builder(getPackageName(typeElement), typeSpec).build();
try {
javaFile.writeTo(processingEnv.getFiler());
} catch (IOException e) {
e.printStackTrace();
}
}
return true;
}
private String getPackageName(TypeElement type) {
return elementUtils.getPackageOf(type).getQualifiedName().toString();
}
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
elementUtils = processingEnv.getElementUtils();
}
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.RELEASE_8;
}
}

使用DIActivity
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package cn.tim.apt_demo;

import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;
import android.widget.TextView;

import cn.tim.annotation.DIActivity;
import cn.tim.annotation.DIView;

@DIActivity
public class MainActivity extends AppCompatActivity {

@DIView(value = R.id.text)
TextView textView;

@DIView(value = R.id.text1)
TextView textView1;

@DIView(value = R.id.text2)
TextView textView2;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
DIMainActivity.bindView(this);
textView.setText("Hello, JavaPoet!");

textView2.setText("Tim");
}
}

实际上就是通过apt生成了以下代码:
1
2
3
4
5
6
7
public final class DIMainActivity extends MainActivity {
public static void bindView(MainActivity activity) {
activity.textView = (android.widget.TextView) activity.findViewById(2131231086);
activity.textView1 = (android.widget.TextView) activity.findViewById(2131231087);
activity.textView2 = (android.widget.TextView) activity.findViewById(2131231088);
}
}

示例代码:Github -> aptdemo

参考资料

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

  • 本文作者: Tim
  • 本文链接: https://zouchanglin.cn/1458763250.html
  • 版权声明: 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!