APT技术

1、概念

APT(Annotation Processing Tool)即注解处理器,是一种处理注解的工具,确切的说它是javac的一个工具,它用来在编译时扫描和处理注解。注解处理器以Java代码(或者编译过的字节码)作为输入,生成.java文件作为输出。
简单来说就是在编译期,通过注解生成.java文件。
Java注解中提到根据@Retention可以将注解分为三种,Java注解举的栗子都是运行期注解,通过反射应用,这必然带来性能问题,那么有没有更好的实现方式呢?既可以实现注入,还能保证性能无损耗呢?
当然有,就是编译时注解,一般这类注解会在编译的时候,根据注解标识,动态生成一些类或者生成一些xml都可以,在运行时期,这类注解是没有的,会依靠动态生成的类做一些操作,因为没有反射,效率和直接调用方法没什么区别。

2、作用

使用APT的优点就是方便、简单,可以少些很多重复的代码。

用过ButterKnife、ARouter、EventBus等注解框架的同学就能感受到,利用这些框架可以少些很多代码,只要写一些注解就可以了。
其实,他们不过是通过注解,生成了一些代码。

Java API 已经提供了扫描源码并解析注解的框架,开发者可以通过继承 AbstractProcessor 类来实现自己的注解解析逻辑。APT 的原理就是在注解了某些代码元素(如字段、函数、类等)后,在编译时编译器会检查 AbstractProcessor 的子类,并且自动调用其 process() 方法,然后将添加了指定注解的所有代码元素作为参数传递给该方法,开发者再根据注解元素在编译期输出对应的 Java 代码。

3、示例

实现一个轻量的 “ButterKnife”,实现一个轻量的控件绑定框架,即通过注解来自动生成 findViewById() 代码。

3.1、创建项目

首先在工程中新建一个Java Library,命名为 apt_processor,用于存放 AbstractProcessor 的实现类。再新建一个 Java Library,命名为 apt_annotation ,用于存放各类注解。

当中,apt_processor 需要导入如下依赖:

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation 'com.google.auto.service:auto-service:1.0-rc6'
    implementation 'com.squareup:javapoet:1.10.0'
    implementation project(':apt_annotation')
}

Android Plugin for Gradle >= 3.4 或者 Gradle Version >=5.0 都要在自己的annotation processor工程里面增加如下的语句:

annotationProcessor 'com.google.auto.service:auto-service:1.0-rc6'

当中,JavaPoet 是 square 开源的 Java 代码生成框架,可以很方便地通过其提供的 API 来生成指定格式(修饰符、返回值、参数、函数体等)的代码。auto-service 是由 Google 开源的注解注册处理器。

实际上,上面两个依赖库并不是必须的,可以通过硬编码代码生成规则来替代,但还是建议使用这两个库,因为这样代码的可读性会更高,且能提高开发效率。

app Module 需要依赖这两个Library:

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation 'androidx.appcompat:appcompat:1.1.0'
    implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'androidx.test:runner:1.2.0'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'

    implementation project(':apt_annotation')
    annotationProcessor project(':apt_processor')
}

这样子,我们需要的所有基础依赖关系就搭建好了:


image.png

3.2、自定义注解类

我们先声明一个注解,类似ButterKnife,BindView 注解的声明如下所示,放在 apt_annotation 中:

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.CLASS)
public @interface BindView {
    int value();
}

@Retention(RetentionPolicy.CLASS):表示编译时注解
@Target(ElementType.FIELD):表示注解范围为类成员(构造方法、方法、成员变量)
注解值 value 用于声明 viewId

3.2、定义注解处理器

在 apt_processor Module 中创建 BindViewProcessor 类并继承 AbstractProcessor 抽象类,该抽象类含有一个抽象方法 process() 以及一个非抽象方法 getSupportedAnnotationTypes() 需要由我们来实现。如果找不到AbstractProcessor类,则需要检查一下自己创建的Module对不对,类型必须是Java Library。

@AutoService(Processor.class)
public class BindViewProcessor extends AbstractProcessor {
    private static final String TAG = "BindViewProcessor";
    private Messager mMessager;
    private Elements mElementUtils;

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

    @Override
    public Set<String> getSupportedAnnotationTypes() {
        HashSet<String> supportTypes = new LinkedHashSet<>();
        supportTypes.add(BindView.class.getCanonicalName());
        return supportTypes;
    }

    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }

    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        return false;
    }
}
  • init:初始化。可以得到ProcessingEnviroment,ProcessingEnviroment提供很多有用的工具类Elements, Types 和 Filer;
  • getSupportedAnnotationTypes:用于指定该 AbstractProcessor 的目标注解对象,这里说明是注解BindView;
  • getSupportedSourceVersion:指定使用的Java版本,通常这里返回SourceVersion.latestSupported();
  • process:用于处理包含指定注解对象的代码元素。可以在这里写扫描、评估和处理注解的代码,生成Java文件。

要自动生成 findViewById() 方法,则需要获取到控件变量的引用以及对应的 viewid,所以需要先遍历出每个 Activity 包含的所有注解对象。

@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
    //获取所有包含BindView注解的元素
    Set<? extends Element> elementSet = roundEnvironment.getElementsAnnotatedWith(BindView.class);
    Map<TypeElement, Map<Integer, VariableElement>> typeElementMapHashMap = new HashMap<>();

    for (Element element : elementSet) {
        //因为 BindView 的作用对象是 FIELD,因此 element 可以直接转化为 VariableElement
        VariableElement variableElement = (VariableElement) element;
        //getEnclosingElement 方法返回封装此 Element 的最里层元素
        //如果 Element 直接封装在另一个元素的声明中,则返回该封装元素
        //此处表示的即 Activity 类对象
        TypeElement typeElement = (TypeElement) variableElement.getEnclosingElement();

        Map<Integer, VariableElement> variableElementMap = typeElementMapHashMap.get(typeElement);
        if (variableElementMap == null) {
            variableElementMap = new HashMap<>();
            typeElementMapHashMap.put(typeElement, variableElementMap);
        }

        //获取注解值,即 ViewId
        BindView bindAnnotation = variableElement.getAnnotation(BindView.class);
        int viewId = bindAnnotation.value();
        //将每个包含了 BindView 注解的字段对象以及其注解值保存起来
        variableElementMap.put(viewId, variableElement);
    }
    return true;
}

通过roundEnvironment.getElementsAnnotatedWith(BindView.class)得到所有注解elements,Element 用于代表程序的一个元素,这个元素可以是:包、类、接口、变量、方法等多种概念。这里以 Activity 对象作为 Key ,通过 map 来存储不同 Activity 下的所有注解对象。

3.3、生成代码

获取到所有的注解对象后,就可以来构造 bind() 方法了。MethodSpec 是 JavaPoet 提供的一个概念,用于抽象出生成一个函数时需要的基础元素,直接看以下方法应该就可以很容易理解其含义了。

通过 addCode() 方法把需要的参数元素填充进去,循环生成每一行 findViewById 方法:

/**
 * 生成方法
 *
 * @param typeElement        注解对象上层元素对象,即 Activity 对象
 * @param variableElementMap Activity 包含的注解对象以及注解的目标对象
 * @return
 */
private MethodSpec generateMethodByPoet(TypeElement typeElement, Map<Integer, VariableElement> variableElementMap) {
    ClassName className = ClassName.bestGuess(typeElement.getQualifiedName().toString());
    //方法参数名
    String parameter = "_" + StringUtils.decapitalize(className.simpleName());
    MethodSpec.Builder methodBuilder = MethodSpec.methodBuilder("bind")
            .addModifiers(Modifier.PUBLIC, Modifier.STATIC)
            .returns(void.class)
            .addParameter(className, parameter);
    for (int viewId : variableElementMap.keySet()) {
        VariableElement element = variableElementMap.get(viewId);
        //被注解的字段名
        String name = element.getSimpleName().toString();
        //被注解的字段的对象类型的全名称
        String type = element.asType().toString();
        String text = "{0}.{1}=({2})({3}.findViewById({4}));";
        methodBuilder.addCode(MessageFormat.format(text, parameter, name, type, parameter, String.valueOf(viewId)));
    }
    return methodBuilder.build();
}

完整代码如下:

@AutoService(Processor.class)
public class BindViewProcessor extends AbstractProcessor {
    private static final String TAG = "BindViewProcessor";
    private Messager mMessager;
    private Elements mElementUtils;

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

    @Override
    public Set<String> getSupportedAnnotationTypes() {
        HashSet<String> supportTypes = new LinkedHashSet<>();
        supportTypes.add(BindView.class.getCanonicalName());
        return supportTypes;
    }

    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }

    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        //获取所有包含BindView注解的元素
        Set<? extends Element> elementSet = roundEnvironment.getElementsAnnotatedWith(BindView.class);
        Map<TypeElement, Map<Integer, VariableElement>> typeElementMapHashMap = new HashMap<>();

        for (Element element : elementSet) {
            //因为 BindView 的作用对象是 FIELD,因此 element 可以直接转化为 VariableElement
            VariableElement variableElement = (VariableElement) element;
            //getEnclosingElement 方法返回封装此 Element 的最里层元素
            //如果 Element 直接封装在另一个元素的声明中,则返回该封装元素
            //此处表示的即 Activity 类对象
            TypeElement typeElement = (TypeElement) variableElement.getEnclosingElement();

            Map<Integer, VariableElement> variableElementMap = typeElementMapHashMap.get(typeElement);
            if (variableElementMap == null) {
                variableElementMap = new HashMap<>();
                typeElementMapHashMap.put(typeElement, variableElementMap);
            }

            //获取注解值,即 ViewId
            BindView bindAnnotation = variableElement.getAnnotation(BindView.class);
            int viewId = bindAnnotation.value();
            //将每个包含了 BindView 注解的字段对象以及其注解值保存起来
            variableElementMap.put(viewId, variableElement);
        }

        for (TypeElement key : typeElementMapHashMap.keySet()) {
            Map<Integer, VariableElement> elementMap = typeElementMapHashMap.get(key);
            PackageElement packageElement = mElementUtils.getPackageOf(key);
            String packageName = packageElement.getQualifiedName().toString();

            JavaFile javaFile = JavaFile.builder(packageName, generateCodeByPoet(key, elementMap)).build();
            try {
                javaFile.writeTo(processingEnv.getFiler());
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return true;
    }

    /**
     * 生成 Java 类
     *
     * @param typeElement        注解对象上层元素对象,即 Activity 对象
     * @param variableElementMap Activity 包含的注解对象以及注解的目标对象
     * @return
     */
    private TypeSpec generateCodeByPoet(TypeElement typeElement, Map<Integer, VariableElement> variableElementMap) {
        //自动生成的文件以 Activity名 + ViewBinding 进行命名
        return TypeSpec.classBuilder(typeElement.getQualifiedName().toString() + "ViewBinding")
                .addModifiers(Modifier.PUBLIC)
                .addMethod(generateMethodByPoet(typeElement, variableElementMap))
                .build();
    }


    /**
     * 生成方法
     *
     * @param typeElement        注解对象上层元素对象,即 Activity 对象
     * @param variableElementMap Activity 包含的注解对象以及注解的目标对象
     * @return
     */
    private MethodSpec generateMethodByPoet(TypeElement typeElement, Map<Integer, VariableElement> variableElementMap) {
        ClassName className = ClassName.bestGuess(typeElement.getQualifiedName().toString());
        //方法参数名
        String parameter = "_" + StringUtils.decapitalize(className.simpleName());
        MethodSpec.Builder methodBuilder = MethodSpec.methodBuilder("bind")
                .addModifiers(Modifier.PUBLIC, Modifier.STATIC)
                .returns(void.class)
                .addParameter(className, parameter);
        for (int viewId : variableElementMap.keySet()) {
            VariableElement element = variableElementMap.get(viewId);
            //被注解的字段名
            String name = element.getSimpleName().toString();
            //被注解的字段的对象类型的全名称
            String type = element.asType().toString();
            String text = "{0}.{1}=({2})({3}.findViewById({4}));";
            methodBuilder.addCode(MessageFormat.format(text, parameter, name, type, parameter, String.valueOf(viewId)));
        }
        return methodBuilder.build();
    }
}

3.4、注解绑定效果