android APT技术

1,背景

对于注解 的使用,想必大家都不陌生,它出现在我们的源码中,以及大部分框架中,比如ButterKnifeArouterRetrofit,但它们是有区别的,其中前2个是编译时注解,最后一个是运行时注解。我们都知道用注解可以简化代码,但大多数同学对其中的原理和实现方式还是很陌生的,下面就带大家入门编译时注解。

2,结合实例讲解

以仿写ButterKnife为例,内容如下:

  • 创建自定义注解@interface
  • 创建并注册注解处理器AbstractProcesso,生成处理注解逻辑的.java文件;
  • 封装一个供外部调用的API,用的是反射技术,具体来说就是调用第二步中生成的代码中的方法;
  • 在项目中使用,比如ActivityFragmentAdapter

APT实现方案

这里按功能职责,分多个module来处理,具体如下:

  • annotation:自定义注解(java lib)
  • processor:注解处理器AbstractProcessor (java lib)
  • inject_api: 供外部调用的API (android lib)
  • app:项目使用 android lib)
步骤一:创建自定义注解

创建一个Java Library,名称为annotation,作用是保存所有注解。

步骤二:实现注解处理器AbstractProcessor

创建一个Java Library,名称为processor,作用是扫描、解析、处理注解。

processor的Gradle配置如下:

创建自己的Processor来实现注解处理器:

复制代码
//通过 AutoService 将 Processor 声明到 META-INF 中
@AutoService(Processor.class)
public class ButterKnifeProcessor extends AbstractProcessor {
    /**
     * 生成文件的工具类
     */
    private Filer filer;
    /**
     * 打印信息
     */
    private Messager messager;
    /**
     * 元素相关
     */
    private Elements elementUtils;
    private Types typeUtils;
    /**
     * 存放被注解标记的所有变量,按类来划分
     */
    private Map<String, ProxyInfo> proxyInfoMap = new HashMap<>();


    /**
     * 一些初始化操作,获取一些有用的系统工具类
     *
     * @param processingEnv
     */
    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
        filer = processingEnv.getFiler();
        messager = processingEnv.getMessager();
        elementUtils = processingEnv.getElementUtils();
        typeUtils = processingEnv.getTypeUtils();
    }


    /**
     * 设置支持的版本
     *
     * @return 这里用最新的就好
     */
    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }


    /**
     * 设置支持的注解类型
     *
     * @return
     */
    @Override
    public Set<String> getSupportedAnnotationTypes() {
        //添加支持的注解
        HashSet<String> set = new HashSet<>();
        set.add(BindView.class.getCanonicalName());
        return set;
    }

    /**
     * 注解内部逻辑的实现
     * <p>
     * Element代表程序的一个元素,可以是package, class, interface, method.只在编译期存在
     * TypeElement:变量;TypeElement:类或者接口
     *
     * @param annotations
     * @param roundEnv
     * @return
     */
    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        messager.printMessage(Diagnostic.Kind.NOTE, "annotations size--->" + annotations.size());
        //1、获取要处理的注解的元素的集合
        Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(BindView.class);

        //process()方法会调用3次,只有第一次有效,第2,3次调用的话生成.java文件会发生异常
        if (elements == null || elements.size() < 1) {
            return true;
        }

        //2、按类来划分注解元素,因为每个使用注解的类都会生成相应的代理类
        for (Element element : elements) {
            checkAnnotationValid(element, BindView.class);

            //获取被注解的成员变量
            //这里被注解的类型只能是变量,所以可以直接强转
            VariableElement variableElement = (VariableElement) element;
            //获取该元素的父元素,这里是父类
            TypeElement typeElement = (TypeElement) variableElement.getEnclosingElement();
            //获取全类名
            String className = typeElement.getQualifiedName().toString();
            //获取被注解元素的包名
            String packageName = elementUtils.getPackageOf(element).getQualifiedName().toString();
            //获取注解的参数
            int resourceId = element.getAnnotation(BindView.class).value();

            //生成ProxyInfo对象
            //一个类里面的注解都在一个ProxyInfo中处理
            ProxyInfo proxyInfo = proxyInfoMap.get(className);
            if (proxyInfo == null) {
                proxyInfo = new ProxyInfo(typeElement, packageName);
                proxyInfoMap.put(className, proxyInfo);
            }
            proxyInfo.viewVariableElement.put(resourceId, variableElement);
        }

        //3、生成注解逻辑处理类
        for (String key : proxyInfoMap.keySet()) {
            ProxyInfo proxyInfo = proxyInfoMap.get(key);
            JavaFile javaFile = JavaFile.builder(proxyInfo.packageName, proxyInfo.generateProxyClass())
                    //在文件头部添加注释
                    .addFileComment("auto generateProxyClass code,can not modify")
                    .build();
            try {
                javaFile.writeTo(filer);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return true;
    }


    /**
     * 检查注解是否可用
     *
     * @param annotatedElement
     * @param clazz
     * @return
     */
    private boolean checkAnnotationValid(Element annotatedElement, Class clazz) {
        if (annotatedElement.getKind() != ElementKind.FIELD) {
            messager.printMessage(Diagnostic.Kind.NOTE, "%s must be declared on field.", annotatedElement);
            return false;
        }
        if (annotatedElement.getModifiers().contains(PRIVATE)) {
            messager.printMessage(Diagnostic.Kind.NOTE, "%s() can not be private.", annotatedElement);
            return false;
        }

        if (!isView(annotatedElement.asType())) {
            return false;
        }
        return true;
    }

    //递归判断android.view.View是不是其父类
    private boolean isView(TypeMirror type) {
        List<? extends TypeMirror> supers = typeUtils.directSupertypes(type);
        if (supers.size() == 0) {
            return false;
        }
        for (TypeMirror superType : supers) {
            if (superType.toString().equals("android.view.View") || isView(superType)) {
                return true;
            }
        }
        return false;
    }
}

下面开始这个Processor类的详解:

  • 继承AbstractProcessor,需要重写process()方法,这里是处理注解内部逻辑的,也是本文的关键点之一;

  • 此外还需要实现几个简单的方法init ()getSupportedSourceVersion()getSupportedAnnotationTypes()

  • getSupportedSourceVersion():设置支持的版本,一般用最新的就好;

  • getSupportedAnnotationTypes():添加支持的注解类型,可以是单个/多个,用Set存储;

  • init ():一些初始化操作,获取一些有用的系统工具类,比如生成文件、打印信息、处理元素等;

@AutoService(Processor.class),生成 META-INF 信息;这里使用 Google 的 AutoService 来创建 META-INF,并将被注解的文件声明到 META-INF 中,这里是ButterKnifeProcessor

下面是META-INF 的文件目录:

文件内容com.zx.processor.ButterKnifeProcessor,这就是我们自定义的注解处理器ButterKnifeProcessor的路径。

接下来讲解最关键的process()方法

它的工作内容是扫描处理注解,最终生成.java文件,这个文件里面就是注解的业务逻辑。

这里主要分为3步:

  • 1、通过getElementsAnnotatedWith()获取要处理的注解的元素的集合,换句话说,找到所有Class中被@BindView注解标记的变量;

  • 2、遍历第一步中的元素集合,由于这个注解可能会在多个类中使用,所以我们以类名为单元划分注解 。具体说,新建一个ProxyInfo对象去保存一个类里面的所有被注解的元素;用proxyInfoMap去保存所有的ProxyInfo;大概是这个样子Map<String, ProxyInfo> proxyInfoMap = new HashMap<>();

  • 3、在ProxyInfo中为每个使用了@BindView注解的类生成一个代理类;

  • 4、遍历proxyInfoMap,通过ProxyInfoJavaFile生成具体的代理类文件

    /**
    * 注解内部逻辑的实现
    *


    * Element代表程序的一个元素,可以是package, class, interface, method.只在编译期存在
    * TypeElement:变量;TypeElement:类或者接口
    *
    * @param annotations
    * @param roundEnv
    * @return
    */
    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
    messager.printMessage(Diagnostic.Kind.NOTE, "annotations size--->" + annotations.size());
    //1、获取要处理的注解的元素的集合
    Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(BindView.class);

    复制代码
          //process()方法会调用3次,只有第一次有效,第2,3次调用的话生成.java文件会发生异常
          if (elements == null || elements.size() < 1) {
              return true;
          }
    
          //2、按类来划分注解元素,因为每个使用注解的类都会生成相应的代理类
          for (Element element : elements) {
              checkAnnotationValid(element, BindView.class);
    
              //获取被注解的成员变量
              //这里被注解的类型只能是变量,所以可以直接强转
              VariableElement variableElement = (VariableElement) element;
              //获取该元素的父元素,这里是父类
              TypeElement typeElement = (TypeElement) variableElement.getEnclosingElement();
              //获取全类名
              String className = typeElement.getQualifiedName().toString();
              //获取被注解元素的包名
              String packageName = elementUtils.getPackageOf(element).getQualifiedName().toString();
              //获取注解的参数
              int resourceId = element.getAnnotation(BindView.class).value();
    
              //生成ProxyInfo对象
              //一个类里面的注解都在一个ProxyInfo中处理
              ProxyInfo proxyInfo = proxyInfoMap.get(className);
              if (proxyInfo == null) {
                  proxyInfo = new ProxyInfo(typeElement, packageName);
                  proxyInfoMap.put(className, proxyInfo);
              }
              proxyInfo.viewVariableElement.put(resourceId, variableElement);
          }
    
          //3、生成注解逻辑处理类
          for (String key : proxyInfoMap.keySet()) {
              ProxyInfo proxyInfo = proxyInfoMap.get(key);
              JavaFile javaFile = JavaFile.builder(proxyInfo.packageName, proxyInfo.generateProxyClass())
                      .addFileComment("auto generateProxyClass code,can not modify")
                      .build();
              try {
                  javaFile.writeTo(filer);
              } catch (IOException e) {
                  e.printStackTrace();
              }
          }
          return true;
      }

具体来说,可以按如下分类:

  • PackageElement 一般代表Package
  • TypeElement 一般代表代表类
  • VariableElement 一般代表成员变量
  • ExecutableElement 一般代表类中的方法

通过javapoet生成注解逻辑代码的具体步骤如下:

复制代码
 /**
     * 通过javapoet API生成代理类
     * @return
     */
    public TypeSpec generateProxyClass() {
        //代理类实现的接口
        ClassName viewInjector = ClassName.get("com.zx.inject_api", "IViewInjector");
        //原始的注解类
        ClassName className = ClassName.get(typeElement);
        //  泛型接口,implements IViewInjector<MainActivity>
        ParameterizedTypeName parameterizedTypeName = ParameterizedTypeName.get(viewInjector, className);

        //生成接口的实现方法inject()
        MethodSpec.Builder bindBuilder = MethodSpec.methodBuilder("inject")
                .addModifiers(Modifier.PUBLIC)
                .addAnnotation(Override.class) //添加方法注解
                .addParameter(className, "target")
                .addParameter(Object.class, "source");

        for (int id : viewVariableElement.keySet()) {
            VariableElement element = viewVariableElement.get(id);
            String fieldName = element.getSimpleName().toString();
            bindBuilder.addStatement(" if (source instanceof android.app.Activity){target.$L = ((android.app.Activity) source).findViewById( $L);}" +
                    "else{target.$L = ((android.view.View)source).findViewById($L);}", fieldName, id, fieldName, id);
        }

        MethodSpec bindMethodSpec = bindBuilder.build();

        //创建类
        TypeSpec typeSpec = TypeSpec.classBuilder(proxyClassName)
                .addModifiers(Modifier.PUBLIC)
                .addSuperinterface(parameterizedTypeName) //实现接口
                .addMethod(bindMethodSpec)
                .build();

        return typeSpec;
    }

这里通过javapoet的API去生成代理类的代码,大概就是TypeSpec生成类,MethodSpec生成方法。这里以MainActivity举例,具体生成处理注解的代理类如下:

复制代码
package com.zx.compileannotation;

import com.zx.inject_api.IViewInjector;

import java.lang.Object;
import java.lang.Override;

public class MainActivity_ViewBinding implements IViewInjector<MainActivity> {
    @Override
    public void inject(MainActivity target, Object source) {
        if (source instanceof android.app.Activity) {
            target.textView2 = ((android.app.Activity) source).findViewById(2131230920);
        } else {
            target.textView2 = ((android.view.View) source).findViewById(2131230920);
        }
        ;
        if (source instanceof android.app.Activity) {
            target.recyclerView = ((android.app.Activity) source).findViewById(2131230849);
        } else {
            target.recyclerView = ((android.view.View) source).findViewById(2131230849);
        }
        ;
        if (source instanceof android.app.Activity) {
            target.textView = ((android.app.Activity) source).findViewById(2131230919);
        } else {
            target.textView = ((android.view.View) source).findViewById(2131230919);
        }
        ;
    }
}

注意:这里只是以MainActivity的代理类,实际上每个用到注解的类都会生成一个类似的代理类

到这一步就会为每一个使用注解的类生成对应的代理类,生成的代理类路径如下,

步骤三:封装一个供外部调用的API

我们知道在processor中已经生成了处理注解逻辑的代理类,那接下来就是调用了。首先我们要知道代理类是在编译器动态生成的,而且会有多个,所以我们只能通过反射找到这个类,然后调用它的方法

复制代码
/**
     * 根据使用注解的类和约定的命名规则,通过反射找到动态生成的代理类(处理注解逻辑)
     * @param object 调用类对象
     */
    private static IViewInjector findProxyActivity(Object object) {
        String proxyClassName = object.getClass().getName() + PROXY;
        Log.e(TAG, "findProxyActivity: "+proxyClassName);
        Class<?> proxyClass = null;
        try {
            proxyClass = Class.forName(proxyClassName);
//            Constructor<?> constructor = proxyClass.getConstructor(object.getClass());
            return (IViewInjector) proxyClass.newInstance();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

我们都知道在使用findViewById()的时候分为2种情况,在Activity中可以直接使用;而在Fragment、Adapter中则是view. findViewById(),所以在下面的接口方法中多设计了一个Object参数,如果是Activity传入它本身;如果是Fragment、Adapter则传入View,

复制代码
public interface IViewInjector<T> {
    /**
     * 通过source.findViewById()
     *
     * @param target 泛型参数,调用类 activity、fragment等
     * @param source Activity、View
     */
    void inject(T target, Object source);
}

最后提供2个供外部activity、fragment等调用的方法,

复制代码
 /**
     * Activity调用
     */
    public static void bind(Activity activity) {
        findProxyActivity(activity).inject(activity, activity);
    }

    /**
     * Fragment、Adapter调用
     *
     * @param object
     * @param view
     */
    public static void bind(Object object, View view) {
        findProxyActivity(object).inject(object, view);
    }

而运行时的入口为,

复制代码
public class MainActivity extends AppCompatActivity {

    @BindView(R.id.tv1)
    TextView textView;
    @BindView(R.id.tv2)
    TextView textView2;

    @BindView(R.id.rv)
    RecyclerView recyclerView;


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        //运行时入口
        ButterKnife.bind(this);

        textView2.setText("this is Activity  compile annotation");

        addFragment();
        initRv();
    }

而ButterKnife.java的代码如下,

复制代码
package com.zx.inject_api;

import android.app.Activity;
import android.util.Log;
import android.view.View;


/**
 * @author 周旭
 * @company 伊柯夫
 * @e-mail 374952705@qq.com
 * @time 2019/11/18
 * @descripe
 */


public class ButterKnife {

    private static final String TAG = "ButterKnife";
    public static final String PROXY = "_ViewBinding";

    /**
     * Activity调用
     */
    public static void bind(Activity activity) {
        findProxyActivity(activity).inject(activity, activity);
    }

    /**
     * Fragment、Adapter调用
     *
     * @param object
     * @param view
     */
    public static void bind(Object object, View view) {
        //先找到对应的类,再调用对应的接口,和我们平时写代码无异,只是类时编译时生成的
        findProxyActivity(object).inject(object, view);
    }


    /**
     * 根据使用注解的类和约定的命名规则,通过反射找到动态生成的代理类(处理注解逻辑)
     *
     * @param object 调用类对象
     */
    private static IViewInjector findProxyActivity(Object object) {
        String proxyClassName = object.getClass().getName() + PROXY;
        Log.e(TAG, "findProxyActivity: " + proxyClassName);
        Class<?> proxyClass = null;
        try {
            proxyClass = Class.forName(proxyClassName);
//            Constructor<?> constructor = proxyClass.getConstructor(object.getClass());
            return (IViewInjector) proxyClass.newInstance();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }
}

通过上面注解,调用到注解生成的文件MainActivity_ViewBinding.java中,内容如下,

复制代码
// auto generateProxyClass code,can not modify
package com.zx.compileannotation;

import com.zx.inject_api.IViewInjector;
import java.lang.Object;
import java.lang.Override;

public class MainActivity_ViewBinding implements IViewInjector<MainActivity> {
  @Override
  public void inject(MainActivity target, Object source) {
     if (source instanceof android.app.Activity){target.textView2 = ((android.app.Activity) source).findViewById( 2131230920);}else{target.textView2 = ((android.view.View)source).findViewById(2131230920);};
     if (source instanceof android.app.Activity){target.recyclerView = ((android.app.Activity) source).findViewById( 2131230849);}else{target.recyclerView = ((android.view.View)source).findViewById(2131230849);};
     if (source instanceof android.app.Activity){target.textView = ((android.app.Activity) source).findViewById( 2131230919);}else{target.textView = ((android.view.View)source).findViewById(2131230919);};
  }
}

代码就是刚process()写进入的,

复制代码
    /**
     * 通过javapoet API生成代理类
     * @return
     */
    public TypeSpec generateProxyClass() {
        //代理类实现的接口
        ClassName viewInjector = ClassName.get("com.zx.inject_api", "IViewInjector");
        //类
//        ClassName className = ClassName.bestGuess(simpleClassName);
        ClassName className = ClassName.get(typeElement);
        // 类型变量
//        TypeVariableName tTypeVariable = TypeVariableName.get("T");

        //  泛型接口,implements IViewInjector<MainActivity>
        ParameterizedTypeName parameterizedTypeName = ParameterizedTypeName.get(viewInjector, className);


        //生成构造方法
        MethodSpec.Builder constructorBuilder = MethodSpec.constructorBuilder()
                .addModifiers(Modifier.PUBLIC)
                .addParameter(className, "target")
                .addStatement("this.target = target");


        //生成接口的实现方法inject()
        MethodSpec.Builder bindBuilder = MethodSpec.methodBuilder("inject")
                .addModifiers(Modifier.PUBLIC)
                .addAnnotation(Override.class) //添加方法注解
                .addParameter(className, "target")
                .addParameter(Object.class, "source");

        for (int id : viewVariableElement.keySet()) {
            VariableElement element = viewVariableElement.get(id);
            String fieldName = element.getSimpleName().toString();
            bindBuilder.addStatement(" if (source instanceof android.app.Activity){target.$L = ((android.app.Activity) source).findViewById( $L);}" +
                    "else{target.$L = ((android.view.View)source).findViewById($L);}", fieldName, id, fieldName, id);
        }

        MethodSpec bindMethodSpec = bindBuilder.build();
//        MethodSpec constructorMethodSpec = constructorBuilder.build();

        //创建类
        TypeSpec typeSpec = TypeSpec.classBuilder(proxyClassName)
                .addModifiers(Modifier.PUBLIC)
                .addSuperinterface(parameterizedTypeName) //实现接口
//                .addTypeVariable(tTypeVariable)
//                .addMethod(constructorMethodSpec)
                .addMethod(bindMethodSpec) //添加类中的方法
//                .addField(className, "target", Modifier.PRIVATE)
                .build();

        return typeSpec;
    }

本质就是运行时调用编译时生成的文件,而编译生成的文件是通过

复制代码
@AutoService(Processor.class) 

的技术来实现的。

完毕

相关推荐
sun00770011 小时前
android ndk编译valgrind
android
AI视觉网奇12 小时前
android studio 断点无效
android·ide·android studio
jiaxi的天空12 小时前
android studio gradle 访问不了
android·ide·android studio
No Silver Bullet13 小时前
android组包时会把从maven私服获取的包下载到本地吗
android
catchadmin13 小时前
PHP serialize 序列化完全指南
android·开发语言·php
tangweiguo0305198715 小时前
Kable使用指南:Android BLE开发的现代化解决方案
android·kotlin
00后程序员张17 小时前
iOS App 混淆与资源保护:iOS配置文件加密、ipa文件安全、代码与多媒体资源防护全流程指南
android·安全·ios·小程序·uni-app·cocoa·iphone
柳岸风19 小时前
Android Studio Meerkat | 2024.3.1 Gradle Tasks不展示
android·ide·android studio
编程乐学19 小时前
安卓原创--基于 Android 开发的菜单管理系统
android
whatever who cares21 小时前
android中ViewModel 和 onSaveInstanceState 的最佳使用方法
android