当前位置: 首页 > news >正文

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*/@Overridepublic synchronized void init(ProcessingEnvironment processingEnv) {super.init(processingEnv);filer = processingEnv.getFiler();messager = processingEnv.getMessager();elementUtils = processingEnv.getElementUtils();typeUtils = processingEnv.getTypeUtils();}/*** 设置支持的版本** @return 这里用最新的就好*/@Overridepublic SourceVersion getSupportedSourceVersion() {return SourceVersion.latestSupported();}/*** 设置支持的注解类型** @return*/@Overridepublic 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*/@Overridepublic 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生成具体的代理类文件
 /*** 注解内部逻辑的实现* <p>* Element代表程序的一个元素,可以是package, class, interface, method.只在编译期存在* TypeElement:变量;TypeElement:类或者接口** @param annotations* @param roundEnv* @return*/@Overridepublic 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> {@Overridepublic 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;@Overrideprotected 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> {@Overridepublic 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) 

的技术来实现的。

完毕

http://www.lryc.cn/news/606671.html

相关文章:

  • 嵌入式系统教学范式演进:云端仿真平台如何重构温湿度监测实验教学
  • JavaScript语法树简介:AST/CST/词法/语法分析/ESTree/生成工具
  • 2025 腾讯广告算法大赛 Baseline 项目解析
  • gd32modbus从机移植
  • 烽火HG680-KX-海思MV320芯片-2+8G-安卓9.0-强刷卡刷固件包
  • 关税战火中的技术方舟:新西兰证券交易所的破局之道 ——从15%关税冲击到跨塔斯曼结算联盟,解码下一代交易基础设施
  • VSCode:通义灵码插件安装使用 -- 免费AI编程工具
  • 高端房产管理小程序
  • C++继承中虚函数调用时机问题及解决方案
  • Spring框架下的中医针灸系统实现
  • 使用uniapp开发小程序-【引入字体并全局使用】
  • 1.6万 Star 的流行容器云平台停止开源
  • GitHub 趋势日报 (2025年07月31日)
  • hadoop.yarn 带时间的LRU 延迟删除
  • 【实战】Dify从0到100进阶--插件开发(1)Github爬取插件
  • 【2025/08/01】GitHub 今日热门项目
  • 24 SAP CPI 调用SAP HTTP接口
  • R语言基础图像及部分调用函数
  • Dify API接口上传文件 postman配置
  • osloader!DoGlobalInitialization函数分析之HW_CURSOR--NTLDR源代码分析之设置光标
  • django操作orm整套
  • 学习设计模式《二十》——解释器模式
  • 如何使用Postman做接口测试
  • curl命令使用
  • 【机器学习与数据挖掘实战 | 医疗】案例20:基于交叉验证和LightGBM算法的糖尿病遗传风险预测
  • 机器学习②【字典特征提取、文本特征处理(TF-IDF)、数据标准化与归一化、特征降维】
  • 解决IDEA无法克隆GitHub上的工程的问题
  • 解决IDEA中MAVEN项目总是将LANGUAGE LEVEL重置的问题
  • SSL 剥离漏洞
  • 把上次做的图片的API改成国内版,让图片返回速度提升一个档次