APT即注解处理器(Annotation Processing Tool)的简称

简单来说就是个javac的一个工具,可以在代码编译的阶段扫描注解,然后做你想干的事情 比生成代码文件、实现一些功能等等…很多开源框架都应用了这一技术如:Butter Knife、Dagger等等…

一、这篇文章通过实现和Butter Knife一样的自动findViewByid()功能,来了解整个APT的过程

1.先来看下整个项目的模块和依赖关系

Profile Android代码打trace android写代码工具_APT

  • findview是个Android的Library:处理找控件的具体操作
  • findview-annotation是一个Java的Library:用来存放注解类
  • findview-compiler是一个Java的Library:用来存放注解处理器的
  • sample是一个Android项目:这里就是用来写示例代码的了
2.模块之间的依赖关系如下
  • findview不依赖其他Module
  • findview-annotation不依赖其他Module
  • findview-compiler依赖findview-annotation
  • sample依赖findviewfindview-annotationfindview-compiler

二、那就开始来把Module一个个创建了

1.首先创建 findview-annotation模块并创建一个BindView注解类
//作用在属性之上
@Target(ElementType.FIELD)
//编译期
@Retention(RetentionPolicy.CLASS)
public @interface BindView {
    int value();
}
  • @Target()这个属性表明注解可以作用于类,方法,变量,参数…等等;这里表明作用于变量之上
  • @Retention()这个属性表明注解需要在什么时期保留,这里表明保留到class文件中
  • RetentionPolicy.SOURCE:保留在源文件,当Java文件编译成class文件的时候,注解被遗弃
  • RetentionPolicy.CLASS:注解被保留到class文件,但jvm加载class文件时候被遗弃
  • RetentionPolicy.RUNTIME:注解不仅被保存到class文件中,jvm加载class文件之后,仍然存在

三、继续创建findview-compiler模块并创建一个注解处理器

  • 通过菜单File —> New Module —> 选择Java Library创建即可
  • 还需要依赖google提供的两个注解处理器工具,gradle文件内容如下
  • 同时依赖findview-annotation模块
apply plugin: 'java-library'

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    //google
    compileOnly 'com.google.auto.service:auto-service:1.0-rc4'
    annotationProcessor 'com.google.auto.service:auto-service:1.0-rc4'
    //依赖注解Module
    implementation project(path: ':findview-annotation')
}
//解决乱码
tasks.withType(JavaCompile) {
    options.encoding = "UTF-8"
}
sourceCompatibility = "7"
targetCompatibility = "7"
1.创建一个FindViewProcessor处理器来处理上面定义的BindView注解
@AutoService(Processor.class)
//需要扫描哪些注解
@SupportedAnnotationTypes("com.azhon.findview.annotation.BindView")
//指定jdk的编译版本
@SupportedSourceVersion(SourceVersion.RELEASE_8)
public class FindViewProcessor extends AbstractProcessor {
    
    //操作Element工具
    private Elements elementUtils;
    //类信息工具
    private Types typeUtils;
    //日志工具
    private Messager messager;
    //文件生成工具
    private Filer filer;

    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
        elementUtils = processingEnv.getElementUtils();
        typeUtils = processingEnv.getTypeUtils();
        messager = processingEnv.getMessager();
        filer = processingEnv.getFiler();
        //打印日志
        messager.printMessage(Diagnostic.Kind.NOTE, "FindViewProcessor");
    }

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        return false;
    }
}
  • @AutoService(Processor.class) 表示这个是一个注解处理器,这样当编译项目的时候这个地方的代码就会去执行了
  • @SupportedAnnotationTypes(“com.azhon.findview.annotation.BindView”)需要扫描的注解类路径
  • @SupportedSourceVersion(SourceVersion.RELEASE_8)指定jdk的编译版本
2.让sample示例代码的gradle去依赖注解和注解处理器模块
  • sample/build.gradle文件
implementation project(path: ':findview-annotation')
//依赖注解处理器
annotationProcessor project(path: ':findview-compiler')
3.点击菜单的Build —> Rebuild Project就可以在看到打印的日志了,如下:

Profile Android代码打trace android写代码工具_android_02

到这里说明注解处理器已经正常工作了,接下来就只要去扫描我们自定义的注解然后做自己想干的事情即可

4.要想注解处理器扫描得到注解,我们就得先去使用它
  • sample模块中的build.gradle文件依赖注解和注解处理器
//依赖注解
implementation project(path: ':findview-annotation')
//依赖注解处理器
annotationProcessor project(path: ':findview-compiler')
  • 使用就就很简单了,布局就一个TextView然后给个id为tv
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <TextView
        android:id="@+id/tv"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Hello World!"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>
  • 在MainActivity中使用自定义的注解
public class MainActivity extends AppCompatActivity {
	
    @BindView(R.id.tv)
    TextView textView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }
}
5.回到FindViewProcessor处理器中,通过代码找到的我们上面标记的TextView
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
    //获取所有使用到注解的节点
    Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(BindView.class);
    //遍历所有的类节点
    for (Element element : elements) {
        //类的的包名
        String packageName = elementUtils.getPackageOf(element).getQualifiedName().toString();
        //类名
        String clsName = element.getEnclosingElement().getSimpleName().toString();
        String simpleName = element.getSimpleName().toString();
        int value = element.getAnnotation(BindView.class).value();
        messager.printMessage(Diagnostic.Kind.NOTE, "packageName:" + packageName);
        messager.printMessage(Diagnostic.Kind.NOTE, "clsName:" + clsName);
        messager.printMessage(Diagnostic.Kind.NOTE, "simpleName:" + simpleName);
        messager.printMessage(Diagnostic.Kind.NOTE, "value:" + value);
    }
    return false;
}
  • Rebuild之后(value就是R.id.tv的值)

注解所在的包名、类名、控件名称都有了就可以开始生成找ID的代码了;也就是生成一个Java文件然后实现找ID的代码

6.一个Java类的结构是从上到下一般是由 包名、导包、类名、变量、方法等组成,所以我们生成类时也需要这样来生成
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
    //获取所有使用到注解的节点
    Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(BindView.class);
    //遍历所有的类节点
    for (Element element : elements) {
        //类的的包名
        String packageName = elementUtils.getPackageOf(element).getQualifiedName().toString();
        //类名
        String clsName = element.getEnclosingElement().getSimpleName().toString();
        String simpleName = element.getSimpleName().toString();
        int value = element.getAnnotation(BindView.class).value();
        try {
            createJavaFile(packageName, clsName, simpleName, value);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    return false;
}
/**
 * 生成java文件
 *
 * @param packageName
 * @param clsName
 * @throws IOException
 */
private void createJavaFile(String packageName, String clsName, String simpleName, int value)
        throws IOException {
    String className = clsName + "$$ViewBinding";
    JavaFileObject sourceFile = filer.createSourceFile(packageName + "." + className);
    Writer writer = sourceFile.openWriter();
    writer.write("package " + packageName + ";\n");
    writer.write("import android.view.View;\n");
    writer.write("public class  " + className + "{\n");
    writer.write("public " + className + "(" + clsName + " target){\n");
    writer.write("this(target,target.getWindow().getDecorView());\n}\n");
    writer.write("public " + className + "(" + clsName + " target, View view){\n");
    writer.write("target." + simpleName + "=view.findViewById(" + value + ");\n }");
    writer.write("}");
    writer.close();
}
  • 再次Rebuild之后就可以看到生成的文件了,在samplebuild\generated\ap_generated_sources\debug\out\com\azhon\sample\

四、注解和自动生成找ID的代码都已经生成好了,所以最后一步就是去使用了;那么现在就来看看要怎么使用

  • 在把findview模块创建出来,在这里实现调用逻辑;并创建一个FindView类
  • 通过菜单File —> New Module —> 选择Java Library创建即可
public class FindView {
    
    public static void bind(Activity activity) {
        Constructor constructor = findBindingConstructorForClass(activity.getClass());
        if (constructor != null) {
            try {
                //实例画APT生成的类 即会自动找id
                constructor.newInstance(activity);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * 根据当前类名找到APT生成的类
     *
     * @param cls
     * @return
     */
    public static Constructor findBindingConstructorForClass(Class<?> cls) {
        String clsName = cls.getName();
        try {
            Class<?> bindingClass = cls.getClassLoader().loadClass(clsName + "$$ViewBinding");
            return bindingClass.getConstructor(cls);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }
}
  • APT生成的类是我们自定义的规则Activity类名+$$ViewBinding,所以可以通过类加载机制加载到每个Activity对应生成的类
  • 通过反射实例化类的构造方法传入对应的参数,这样就调用到了找ID的代码
1.sample模块依赖FindView模块
implementation project(path: ':findview')
  • 然后使用即可,与ButterKnife使用是一致的
public class MainActivity extends AppCompatActivity {

    @BindView(R.id.tv)
    TextView textView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        FindView.bind(this);
        textView.setText("成功设置了文本");
    }
}
效果图


Profile Android代码打trace android写代码工具_android_03

FindViewDemo下载地址