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) 

的技术来实现的。

完毕

相关推荐
我是好小孩34 分钟前
Android-侧边导航栏的使用
android·gitee
吗喽对你问好35 分钟前
安卓基础布局核心知识点整理
android·gitee
安卓开发者37 分钟前
Android Material Components 全面解析:打造现代化 Material Design 应用
android
教程分享大师1 小时前
带root权限_中国移动创维DT541_S905L3融合机器改机顶盒刷机教程 当贝纯净版安卓9.0系统线刷包 刷机包
android
wuk9981 小时前
Android:UI:Drawable:View/ImageView与Drawable
android·ui
whysqwhw2 小时前
Kotlin 中作用域函数 let、with、run、also、apply 的核心使用指南
android
旋风菠萝3 小时前
设计模式---单例
android·java·开发语言
whysqwhw4 小时前
Android Jetpack 中 ViewModel 的全面解析
android
2501_916007477 小时前
iPhone查看App日志和系统崩溃日志的完整实用指南
android·ios·小程序·https·uni-app·iphone·webview
稻草人不怕疼7 小时前
Android 渲染机制小结
android