阅读 318

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
文章标签