盒子
盒子
文章目录
  1. 前言
  2. APT的基本原理
  3. APT的运作流程
  4. APT的应用场景
  5. 优势
  6. 使用代码示例
    1. 定义注解
      1. @Target
      2. @Retention
    2. 编写注解器
    3. 注解运用
    4. 自动生成的类
  7. 注意事项与优化技巧
  8. 结语
  9. 推荐

提高10倍开发效率?APT如何让Android开发变得更轻松

前言

在Android开发中,APT(Annotation Processing Tool)是一种强大的工具,它可以让开发者在编译期间处理注解,生成额外的代码。通过APT,我们可以实现很多高级功能,比如自动生成代码、实现依赖注入、生成路由表等。本文将深入探讨APT的运用以及背后的原理。

APT的基本原理

APT的基本原理是在编译期间扫描和处理源代码中的注解,然后根据注解生成相应的Java代码。这些生成的代码可以在编译后被编译器包含到最终的APK中。

APT的运作流程

  1. 扫描注解: 首先,编译器会扫描源代码中的注解,找到被标记的元素。
  2. 解析注解: 然后,APT会解析这些注解,获取注解中定义的信息。
  3. 生成代码: 接着,根据注解中的信息,APT会生成相应的Java代码。
  4. 编译代码: 最后,生成的Java代码会被编译器编译成.class文件,与其他源代码一起构建成APK。

APT的应用场景

APT在Android开发中有着广泛的应用,其中一些典型的应用场景包括:

  1. 自动生成代码: 通过APT,我们可以在编译期间生成一些重复性的代码,比如Parcelable实现、ViewHolder的生成与添加日志等,从而减少手动编写重复代码的工作量。
  2. 依赖注入: APT可以结合注解处理器,实现依赖注入的功能。通过在注解中指定依赖的对象,注解处理器可以在编译期间生成依赖注入的代码,从而实现依赖注入的功能。
  3. 路由管理: APT可以用来生成路由表,从而实现页面跳转的管理。通过在注解中指定页面的路径和参数,APT可以在编译期间生成路由表,从而实现页面跳转的自动化管理。

优势

APT 具有以下优势:

  1. 提高开发效率: APT 可以自动生成代码,减少开发人员的手动编码工作。
  2. 代码更加简洁优雅: 通过 APT 生成的代码,通常更加简洁优雅,易于理解和维护。
  3. 提高代码质量: APT 可以帮助开发人员避免一些常见的错误,提高代码质量。

使用代码示例

下面通过一个简单的例子来演示APT的使用,实现一个简单的findViewByIdsetText的功能。

定义注解

首先定义一个注解@BindView

1
2
3
4
5
@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.FIELD)
public @interface BindView {
@IdRes int[] value();
}

在这里使用了@Target@Retention两个重要的元注解,用来对注解进行定义和修饰。

@Target

@Target注解用于指定注解可以应用的范围,即注解可以放置在哪些元素之上。它有一个参数,类型为ElementType[],表示注解可以应用的目标元素类型。

常见的ElementType包括:

  • ElementType.TYPE: 类、接口、枚举
  • ElementType.FIELD: 字段
  • ElementType.METHOD: 方法
  • ElementType.PARAMETER: 参数
  • ElementType.CONSTRUCTOR: 构造方法
  • ElementType.LOCAL_VARIABLE: 局部变量
  • ElementType.ANNOTATION_TYPE: 注解类型
  • ElementType.PACKAGE: 包

例如,当我们指定@Target(ElementType.FIELD)时,表示该注解只能应用在字段上,不能应用在其他元素上。

@Retention

@Retention注解用于指定注解的生命周期,即注解在编译后是否保留到运行时。它有一个参数,类型为RetentionPolicy,表示注解的保留策略。

常见的保留策略包括:

  • RetentionPolicy.SOURCE: 注解仅保留在源代码中,编译时会被丢弃,不会包含在生成的class文件中。
  • RetentionPolicy.CLASS: 注解保留在编译后的class文件中,但在运行时会被忽略,默认值。在Kotlin中对应的是BINARY
  • RetentionPolicy.RUNTIME: 注解保留在编译后的class文件中,并且在运行时可以通过反射获取到。

通常情况下,我们希望自定义注解在运行时保留,以便在运行时通过反射来获取注解信息,因此,一般会指定@Retention(RetentionPolicy.RUNTIME)

例如,当我们指定@Retention(RetentionPolicy.RUNTIME)时,表示该注解在编译后的class文件中保留,并且可以在运行时通过反射获取到。

编写注解器

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
82
83
84
85
86
87
88
89
90
91
92
93
94
public class Processor extends AbstractProcessor {

private Filer mFiler;
private Messager mMessager;
private Elements mElementUtils;

@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
mFiler = processingEnv.getFiler();
mMessager = processingEnv.getMessager();
mElementUtils = processingEnv.getElementUtils();
}

@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
if (!roundEnv.processingOver()) {
//获取与annotation相匹配的TypeElement,即有注释声明的class
Set<TypeElement> elements = getTypeElementsByAnnotationType(annotations, roundEnv.getRootElements());

for (TypeElement typeElement : elements) {
//包名
String packageName = mElementUtils.getPackageOf(typeElement).getQualifiedName().toString();
//类名
String typeName = typeElement.getSimpleName().toString();
//全称类名
ClassName className = ClassName.get(packageName, typeName);
//自动生成类全称名
ClassName autoGenerationClassName = ClassName.get(packageName,
NameUtils.getAutoGeneratorTypeName(typeName));

//构建自动生成的类
TypeSpec.Builder typeBuilder = TypeSpec.classBuilder(autoGenerationClassName)
.addModifiers(Modifier.PUBLIC)
.addAnnotation(Keep.class);

//添加构造方法
typeBuilder.addMethod(MethodSpec.constructorBuilder()
.addModifiers(Modifier.PUBLIC)
.addParameter(className, NameUtils.Variable.ANDROID_ACTIVITY)
.addStatement("$N($N)",
NameUtils.Method.BIND_VIEW,
NameUtils.Variable.ANDROID_ACTIVITY)
.build());

//添加bindView成员方法
MethodSpec.Builder bindViewBuilder = MethodSpec.methodBuilder(NameUtils.Method.BIND_VIEW)
.addModifiers(Modifier.PRIVATE)
.returns(TypeName.VOID)
.addParameter(className, NameUtils.Variable.ANDROID_ACTIVITY);

//添加方法内容
for (VariableElement variableElement : ElementFilter.fieldsIn(typeElement.getEnclosedElements())) {
BindView bindView = variableElement.getAnnotation(BindView.class);
if (bindView != null) {
bindViewBuilder.addStatement("$N.$N=($T)$N.findViewById($L)",
NameUtils.Variable.ANDROID_ACTIVITY,
variableElement.getSimpleName(),
variableElement,
NameUtils.Variable.ANDROID_ACTIVITY,
bindView.value()[0]
).addStatement("$N.$N.setText($N.getString($L))",
NameUtils.Variable.ANDROID_ACTIVITY,
variableElement.getSimpleName(),
NameUtils.Variable.ANDROID_ACTIVITY,
bindView.value()[1]);
}
}

typeBuilder.addMethod(bindViewBuilder.build());

//写入java文件
try {
JavaFile.builder(packageName, typeBuilder.build()).build().writeTo(mFiler);
} catch (IOException e) {
mMessager.printMessage(Diagnostic.Kind.ERROR, e.getMessage(), typeElement);
}
}
}
return true;
}

private Set<TypeElement> getTypeElementsByAnnotationType(Set<? extends TypeElement> annotations, Set<? extends Element> elements) {
....
}

@Override
public Set<String> getSupportedAnnotationTypes() {
return new TreeSet<>(Arrays.asList(
BindView.class.getCanonicalName(),
Keep.class.getCanonicalName())
);
}
}

在这个处理器中,按照一个类的创建顺序做了以下几步:

  1. 生成构建的类
  2. 添加类的构造方法,并在构造方法中引用我们需要的bindView方法
  3. 为类添加bindView成员方法
  4. bindView方法中添加实现代码,也就是findVieByIdsetText的代码实现
  5. 通过javaPoet写入到java文件中

JavaPoet是一个用于生成Java代码的库,它提供了一套API来构建Java源代码,并且可以输出成Java文件。

经过上面的步骤,机会自动帮我们生成一个绑定View的代码类。

注解运用

接下来,我们来演示如何使用@BindView注解:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class MainActivity : AppCompatActivity() {

@BindView(R.id.public_service, R.string.public_service)
lateinit var sName: TextView

@BindView(R.id.personal_wx, R.string.personal_wx)
lateinit var sPhone: TextView

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
Butterknife.bind(this)
}
}

MainActivity中,我们使用了@BindView注解来标记TextView字段,然后在onCreate方法中调用Butterknife.bind(this)方法,即可自动为textView字段进行赋值,无需手动调用findViewById方法。

Butterknife是一个自定义的类,内部提供bind方法,通过反射来构建上面我们自动生成的绑定类的实例。

自动生成的类

最后,再来看下自动生成的类的真正面目。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Keep
public class MainActivity$Binding {
public MainActivity$Binding(MainActivity activity) {
bindView(activity);
}

private void bindView(MainActivity activity) {
activity.sName=(TextView)activity.findViewById(2131165265);
activity.sName.setText(activity.getString(2131427362));
activity.sPhone=(TextView)activity.findViewById(2131165262);
activity.sPhone.setText(activity.getString(2131427360));
}

}

注意事项与优化技巧

在使用APT时,有一些注意事项和优化技巧需要我们注意:

  • 避免滥用APT: 虽然APT能够帮助我们实现很多高级功能,但是滥用APT会导致编译时间过长,增加项目的复杂度。因此,在使用APT时,需要权衡利弊,避免过度使用。
  • 优化代码生成: 在编写注解处理器时,需要尽量优化生成的代码,减少生成的代码量,提高代码的执行效率。
  • 处理异常情况: 在处理注解时,需要考虑到各种异常情况,比如注解不存在、注解参数错误等情况,从而提高代码的健壮性。

结语

通过本文的介绍,相信大家已经对APT有了更深入的理解,并且能够在实际的项目中运用APT来提高开发效率。APT作为一种强大的工具,在Android开发中有着广泛的应用前景,希望大家能够善加利用,发挥其最大的作用。

推荐

android_startup: 提供一种在应用启动时能够更加简单、高效的方式来初始化组件,优化启动速度。不仅支持Jetpack App Startup的全部功能,还提供额外的同步与异步等待、线程控制与多进程支持等功能。

AwesomeGithub: 基于Github的客户端,纯练习项目,支持组件化开发,支持账户密码与认证登陆。使用Kotlin语言进行开发,项目架构是基于JetPack\&DataBinding的MVVM;项目中使用了Arouter、Retrofit、Coroutine、Glide、Dagger与Hilt等流行开源技术。

flutter_github: 基于Flutter的跨平台版本Github客户端,与AwesomeGithub相对应。

android-api-analysis: 结合详细的Demo来全面解析Android相关的知识点, 帮助读者能够更快的掌握与理解所阐述的要点。

daily_algorithm: 每日一算法,由浅入深,欢迎加入一起共勉。

支持一下
赞赏是一门艺术