2

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

 1 month ago
source link: http://rousetime.com/2024/03/16/%E6%8F%90%E9%AB%9810%E5%80%8D%E5%BC%80%E5%8F%91%E6%95%88%E7%8E%87%EF%BC%9FAPT%E5%A6%82%E4%BD%95%E8%AE%A9Android%E5%BC%80%E5%8F%91%E5%8F%98%E5%BE%97%E6%9B%B4%E8%BD%BB%E6%9D%BE/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

在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

@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文件中保留,并且可以在运行时通过反射获取到。

编写注解器

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注解:

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方法,通过反射来构建上面我们自动生成的绑定类的实例。

自动生成的类

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

@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: 每日一算法,由浅入深,欢迎加入一起共勉。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK