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

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

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

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:

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类

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

@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的代码:

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

修改上述TestProcessor的process方法

@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

dependencies {

    ......

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

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

@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注解。

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 {

}
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方法

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

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生成了以下代码:

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(编译时代码生成)最佳实践》