编程进阶网编程进阶网
  • 基础组成体系
  • 程序编程原理
  • 异常和IO系统
  • 六大设计原则
  • 设计模式导读
  • 创建型设计模式
  • 结构型设计模式
  • 行为型设计模式
  • 设计模式案例
  • 面向对象思想
  • 基础入门
  • 高级进阶
  • JVM虚拟机
  • 数据集合
  • Java面试题
  • C语言入门
  • C综合案例
  • C标准库
  • C语言专栏
  • C++入门
  • C++综合案例
  • C++专栏
  • HTML
  • CSS
  • JavaScript
  • 前端专栏
  • Swift
  • iOS入门
  • 基础入门
  • 开源库解读
  • 性能优化
  • Framework
  • 方案设计
  • 媒体音视频
  • 硬件开发
  • Groovy
  • 常用工具
  • 大厂面试题
  • 综合案例
  • 网络底层
  • Https
  • 网络请求
  • 故障排查
  • 专栏
  • 数组
  • 链表
  • 栈
  • 队列
  • 树
  • 递归
  • 哈希
  • 排序
  • 查找
  • 字符串
  • 其他
  • Bash脚本
  • Linux入门
  • 嵌入式开发
  • 代码规范
  • Markdown
  • 开发理论
  • 开发工具
  • Git管理
  • 百宝箱
  • 开源协议
  • 技术招聘
  • 测试经验
  • 职场提升
  • 技术模版
  • 关于我
  • 目标清单
  • 学习框架
  • 育儿经验
  • 我的专栏
  • 底层能力
  • 读书心得
  • 随笔笔记
  • 职场思考
  • 中华历史
  • 经济学故事
  • 基础组成体系
  • 程序编程原理
  • 异常和IO系统
  • 六大设计原则
  • 设计模式导读
  • 创建型设计模式
  • 结构型设计模式
  • 行为型设计模式
  • 设计模式案例
  • 面向对象思想
  • 基础入门
  • 高级进阶
  • JVM虚拟机
  • 数据集合
  • Java面试题
  • C语言入门
  • C综合案例
  • C标准库
  • C语言专栏
  • C++入门
  • C++综合案例
  • C++专栏
  • HTML
  • CSS
  • JavaScript
  • 前端专栏
  • Swift
  • iOS入门
  • 基础入门
  • 开源库解读
  • 性能优化
  • Framework
  • 方案设计
  • 媒体音视频
  • 硬件开发
  • Groovy
  • 常用工具
  • 大厂面试题
  • 综合案例
  • 网络底层
  • Https
  • 网络请求
  • 故障排查
  • 专栏
  • 数组
  • 链表
  • 栈
  • 队列
  • 树
  • 递归
  • 哈希
  • 排序
  • 查找
  • 字符串
  • 其他
  • Bash脚本
  • Linux入门
  • 嵌入式开发
  • 代码规范
  • Markdown
  • 开发理论
  • 开发工具
  • Git管理
  • 百宝箱
  • 开源协议
  • 技术招聘
  • 测试经验
  • 职场提升
  • 技术模版
  • 关于我
  • 目标清单
  • 学习框架
  • 育儿经验
  • 我的专栏
  • 底层能力
  • 读书心得
  • 随笔笔记
  • 职场思考
  • 中华历史
  • 经济学故事
  • 1.1App启动流程梳理
  • 1.2ActivityThread分析
  • 1.3Context设计思想
  • 2.1Activity基础介绍
  • 2.2Activity启动流程
  • 2.3Activity布局创建
  • 2.4Activity布局绘制
  • 2.5Service基础介绍
  • 2.6Service启动流程
  • 2.7Receiver广播基础
  • 2.8Receiver深入原理
  • 2.9ContentProvider分析
  • 2.10Fragment实践
  • 2.11Intent深入思考
  • 3.1Paint和Canvas
  • 3.2View的绘制基础
  • 3.3onMeasure流程设计
  • 3.4onLayout流程设计
  • 3.5onDraw流程设计
  • 3.6View工作原理
  • 3.7View刷新设计流程
  • 3.8自定义View控件
  • 3.9自定义ViewGroup控件
  • 4.1Handler基础使用
  • 4.2消息机制流程分析
  • 4.3Handler深度解析
  • 4.4Message深度理解
  • 4.5MessageQueue解析
  • 4.6Looper深度解析
  • 4.7理解Handler同步屏障
  • 4.8ThreadLocal分析
  • 4.9ThreadLocal场景
  • 5.1View事件设计思考
  • 5.2View滑动冲突处理
  • 5.3View事件源码分析
  • 5.4View事件总结案例
  • 6.1DecorView设计思想
  • 6.2视图的载体View
  • 6.3视图管理者Window
  • 6.4窗口管理服务WMS
  • 6.5布局解析者Inflater
  • 7.1AsyncTask深入介绍
  • 7.2HandlerThread设计
  • 7.3IntentService设计
  • 8.1IPC通信方式介绍
  • 8.2AIDL进程间通信
  • 8.7Binder通信机制设计
  • /zh/android/basic/9.1注解设计思想和原理.html
  • 9.2APT技术设计详解
  • 9.3APT多种案例实践

9.2APT技术设计详解

目录介绍

  • 01.什么是apt
  • 02.基本概念
    • 2.1 注解处理器
    • 2.2 抽象处理器
  • 03.android-apt被替代
  • 04.实际案例操作
    • 4.1 实现目标
    • 4.2 自定义注解
    • 4.3 创建注解处理器
    • 4.4 具体案例分享

01.什么是apt

  • 什么是apt
    • APT,就是Annotation Processing Tool的简称,叫做注解处理工具。就是可以在代码编译期间对注解进行处理,并且生成Java文件,减少手动的代码输入。注解我们平时用到的比较多的可能会是运行时注解,比如大名鼎鼎的retrofit就是用运行时注解,通过动态代理来生成网络请求。编译时注解平时开发中可能会涉及的比较少,但并不是说不常用,比如我们经常用的轮子Dagger2, ButterKnife, EventBus3 都在用。
  • 编译时注解。
    • 也有人叫它代码生成,其实他们还是有些区别的,在编译时对注解做处理,通过注解,获取必要信息,在项目中生成代码,运行时调用,和直接运行手写代码没有任何区别。而更准确的叫法:APT - Annotation Processing Tool
  • 大概原理
    • Java API 已经提供了扫描源码并解析注解的框架,开发者可以通过继承 AbstractProcessor 类来实现自己的注解解析逻辑。APT 的原理就是在注解了某些代码元素(如字段、函数、类等)后,在编译时编译器会检查 AbstractProcessor 的子类,并且自动调用其 process() 方法,然后将添加了指定注解的所有代码元素作为参数传递给该方法,开发者再根据注解元素在编译期输出对应的 Java 代码

02.基本概念

2.1 注解处理器

  • 注解处理器是一个在javac中的,用来编译时扫描和处理的注解的工具。你可以为特定的注解,注册你自己的注解处理器。
  • 注解处理器可以生成Java代码,这些生成的Java代码会组成 .java 文件,但不能修改已经存在的Java类(即不能向已有的类中添加方法)。而这些生成的Java文件,会同时与其他普通的手写Java源代码一起被javac编译。

2.2 抽象处理器

  • 每一个注解处理器都要继承于AbstractProcessor,如下所示:
    public class MyProcessor extends AbstractProcessor{
        @Override
        public synchronized void init(ProcessingEnvironment processingEnvironment){
            super.init(processingEnvironment);
        }
    
        @Override
        public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment){
            return false;
        }
    
        @Override
        public Set<String> getSupportedAnnotationTypes(){
            return super.getSupportedAnnotationTypes();
        }
    
        @Override
        public SourceVersion getSupportedSourceVersion(){
            return super.getSupportedSourceVersion();
        }
    }
  • 这几个方法如下
    • init(ProcessingEnvironment processingEnvironment): 每一个注解处理器类都必须有一个空的构造函数。然而,这里有一个特殊的init()方法,它会被注解处理工具调用,并输入ProcessingEnviroment参数。ProcessingEnviroment提供很多有用的工具类Elements,Types和Filer。后面我们将看到详细的内容。
    • process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment): 这相当于每个处理器的主函数main()。你在这里写你的扫描、评估和处理注解的代码,以及生成Java文件。输入参数RoundEnviroment,可以让你查询出包含特定注解的被注解元素。后面我们将看到详细的内容。
    • getSupportedAnnotationTypes(): 这里你必须指定,这个注解处理器是注册给哪个注解的。注意,它的返回值是一个字符串的集合,包含本处理器想要处理的注解类型的合法全称。换句话说,你在这里定义你的注解处理器注册到哪些注解上。
    • getSupportedSourceVersion(): 用来指定你使用的Java版本。通常这里返回SourceVersion.latestSupported()。然而,如果你有足够的理由只支持Java 7的话,你也可以返回SourceVersion.RELEASE_7。我推荐你使用前者。

03.android-apt被替代

  • annotationProcessor替代者
    • Android Gradle 插件 2.2 版本的发布,之前 android-apt 作者在官网发表声明证实了后续将不会继续维护 android-apt,并推荐大家使用 Android 官方插件提供的相同能力。也就是说,大约三年前推出的 android-apt 即将告别开发者,退出历史舞台,Android Gradle 插件提供了名为 annotationProcessor 的功能来完全代替 android-apt。
  • annotationProcessor和apt区别?
    • Android 官方的 annotationProcessor 同时支持 javac 和 jack 编译方式,而 android-apt 只支持 javac 方式。当然,目前 android-apt 在 Android Gradle 插件 2.2 版本上面仍然可以正常运行,如果你没有想支持 jack 编译方式的话,可以继续使用 android-apt。
  • 什么是jack编译方式?
    • Jack (Java Android Compiler Kit)是新的Android 编译工具,从Android 6.0 开始加入,替换原有的编译工具,例如javac, ProGuard, jarjar和 dx。它主要负责将java代码编译成dex包,并支持代码压缩,混淆等。
  • Jack工具的主要优势
    • 完全开放源码,源码均在AOSP中,合作伙伴可贡献源码
    • 加快编译源码,Jack 提供特殊的配置,减少编译时间:pre-dexing, 增量编译和Jack编译服务器.
    • 支持代码压缩,混淆,重打包和multidex,不在使用额外单独的包,例如ProGuard。

04.实际案例操作

4.1 实现目标

  • 通过一个栗子来进行分析。平时一般启动Activity都是这样通过startActivity或者startActivityForResult等等balabala。今天我们通过给Activity添加一个注解@RouteAnnotation(name = "RouteName_Activity"),然后通过注解来启动Activity,AnnocationRouter.getSingleton().navigat("RouteName_Activity")。

4.2 自定义注解

  • 定义注解RouteAnnotation,作用对象就是类,作用范围就是编译时。然后就是接受一个参数,也就是Activity的别名。
    /**
     * <pre>
     *     @author 杨充
     *     blog  : https://github.com/yangchong211
     *     time  : 2018/05/21
     *     desc  : 定义注解RouteAnnotation,作用对象就是类,作用范围就是编译时
     *     revise: 接受一个参数,也就是Activity的别名
     * </pre>
     */
    @Target(ElementType.TYPE)
    @Retention(RetentionPolicy.CLASS)
    public @interface RouteAnnotation {
        String name();
    }

4.3 创建注解处理器

  • 注解处理器一般会是一个项目比较底层的模块,因此我们创建一个Java Library,annotationprocess模块,要依赖前面的注解。自定义的处理器需要继承AbstractProcessor,需要自己实现process方法。
    • 注意,这里是创建Java Library工程
    import javax.annotation.processing.AbstractProcessor;
    
    public class AnnotationProcessor extends AbstractProcessor {
    
        private Filer mFiler;
    
        @Override
        public synchronized void init(ProcessingEnvironment processingEnvironment) {
            super.init(processingEnvironment);
            mFiler = processingEnvironment.getFiler();
        }
    
        @Override
        public SourceVersion getSupportedSourceVersion() {
            //getSupportedSourceVersion()方法返回 Java 版本
            return SourceVersion.latestSupported();
        }
    
        @Override
        public Set<String> getSupportedAnnotationTypes() {
            //getSupportedAnnotationTypes()方法返回要处理的注解的集合
            LinkedHashSet<String> types = new LinkedHashSet<>();
            types.add(RouteAnnotation.class.getCanonicalName());
            return types;
        }
    
        @Override
        public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
            HashMap<String, String> nameMap = new HashMap<>();
    
            Set<? extends Element> annotationElements = roundEnvironment.getElementsAnnotatedWith(RouteAnnotation.class);
    
            for (Element element : annotationElements) {
                RouteAnnotation annotation = element.getAnnotation(RouteAnnotation.class);
                String name = annotation.name();
                nameMap.put(name, element.getSimpleName().toString());
                //nameMap.put(element.getSimpleName().toString(), name);//MainActiviy-RouteName_MainActivity
            }
    
            //generate Java File
            generateJavaFile(nameMap);
    
            return true;
        }
    
        private void generateJavaFile(Map<String, String> nameMap) {
            //generate constructor
            MethodSpec.Builder constructorBuidler = MethodSpec.constructorBuilder()
                    .addModifiers(Modifier.PUBLIC)
                    .addStatement("routeMap = new $T<>()", HashMap.class);
            for (String key : nameMap.keySet()) {
                String name = nameMap.get(key);
                constructorBuidler.addStatement("routeMap.put(\"$N\", \"$N\")", key, name);
            }
            MethodSpec constructorName = constructorBuidler.build();
    
            //generate getActivityRouteName method
            MethodSpec routeName = MethodSpec.methodBuilder("getActivityName")
                    .addModifiers(Modifier.PUBLIC)
                    .returns(String.class)
                    .addParameter(String.class, "routeName")
                    .beginControlFlow("if (null != routeMap && !routeMap.isEmpty())")
                    .addStatement("return (String)routeMap.get(routeName)")
                    .endControlFlow()
                    .addStatement("return \"\"")
                    .build();
    
            //generate class
            TypeSpec typeSpec = TypeSpec.classBuilder("AnnotationRoute$Finder")
                    .addModifiers(Modifier.PUBLIC)
                    .addMethod(constructorName)
                    .addMethod(routeName)
                    .addSuperinterface(Provider.class)
                    .addField(HashMap.class, "routeMap", Modifier.PRIVATE)
                    .build();
    
    
            JavaFile javaFile = JavaFile.builder("com.ycbjie.video.annotaioncompiletest", typeSpec).build();
            try {
                javaFile.writeTo(mFiler);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
  • 重点就是process方法,方法里面主要工作就是生成Java文件。我们具体看下步骤:
    • 1.roundEnvironment.getElementsAnnotatedWith(RouteAnnotation.class)拿到所有RouteAnnotation注解标注的类
    • 2.循环取出注解的name属性和被标注的类名并缓存,其实就是:
    • put("RouteName_ActivityTwo", "ActivityTwo");
    • 3.通过javapoet库生成Java类,javapoet是square公司良心出品,让我们脱离手动凭借字符串来生成Java类的痛苦,可以通过各种姿势来生成Java类,这里不多做介绍,有需要的可以看官方文档,很详细。
    • 4.最后通过JavaFile.builder("包名",TypeSpec)生成Java文件,包名可以随意取,最后生成的文件都是在主程序模块app.build.generated.source.apt目录下

03.Element

  • Element也是APT的重点之一,所有通过注解取得元素都将以Element类型等待处理,也可以理解为Element的子类类型与自定义注解时用到的@Target是有对应关系的。
    • Element的官方注释:Represents a program element such as a package, class, or method. Each element represents a static, language-level construct (and not, for example, a runtime construct of the virtual machine).
    • 表示一个程序元素,比如包、类或者方法。
  • Element的子类有,不同Element的信息获取方式不同。
    ExecutableElement
    表示某个类或接口的方法、构造方法或初始化程序(静态或实例),包括注释类型元素。
    对应@Target(ElementType.METHOD) @Target(ElementType.CONSTRUCTOR)
    PackageElement;
    表示一个包程序元素。提供对有关包极其成员的信息访问。
    对应@Target(ElementType.PACKAGE)
    TypeElement;
    表示一个类或接口程序元素。提供对有关类型极其成员的信息访问。
    对应@Target(ElementType.TYPE)
    注意:枚举类型是一种类,而注解类型是一种接口。
    TypeParameterElement;
    表示一般类、接口、方法或构造方法元素的类型参数。
    对应@Target(ElementType.PARAMETER)
    VariableElement;
    表示一个字段、enum常量、方法或构造方法参数、局部变量或异常参数。
    对应@Target(ElementType.LOCAL_VARIABLE)
  • 例如:取得所有修饰了@XXX的元素。
    for (Element element : roundEnv.getElementsAnnotatedWith(XXX.class)){
    	//OnceClick.class是@Target(METHOD)
    	//则该element是可以强转为表示方法的ExecutableElement
    	ExecutableElement method = (ExecutableElement)element;
    	//如果需要用到其他类型的Element,则不可以直接强转,需要通过下面方法转换
    	//但有例外情况,我们稍后列举
    	TypeElement classElement = (TypeElement) element
                    .getEnclosingElement();
    }

04.修饰方法的注解和ExecutableElement

  • 当你有一个注解是以@Target(ElementType.METHOD)定义时,表示该注解只能修饰方法。那么这个时候你为了生成代码,而需要获取一些基本信息:包名、类名、方法名、参数类型、返回值。
    • 代码如下所示
      //xxx.class 以 @Target(ElementType.METHOD)修饰
      for (Element element : roundEnv.getElementsAnnotatedWith(xxx.class)) {
      	//对于Element直接强转
          ExecutableElement executableElement = (ExecutableElement) element;
          
          //非对应的Element,通过getEnclosingElement转换获取
          TypeElement classElement = (TypeElement) element
                      .getEnclosingElement();
                      
          //当(ExecutableElement) element成立时,使用(PackageElement) element
          //            .getEnclosingElement();将报错。
          //需要使用elementUtils来获取
          Elements elementUtils = processingEnv.getElementUtils();
          PackageElement packageElement = elementUtils.getPackageOf(classElement);
      	
      	//全类名
          String fullClassName = classElement.getQualifiedName().toString();
          //类名
          String className = classElement.getSimpleName().toString();
          //包名
          String packageName = packageElement.getQualifiedName().toString();
          //方法名
          String methodName = executableElement.getSimpleName().toString();
      
      	//取得方法参数列表
      	List<? extends VariableElement> methodParameters = executableElement.getParameters();
      	//参数类型列表
      	List<String> types = new ArrayList<>();
          for (VariableElement variableElement : methodParameters) {
              TypeMirror methodParameterType = variableElement.asType();
              if (methodParameterType instanceof TypeVariable) {
                  TypeVariable typeVariable = (TypeVariable) methodParameterType;
                  methodParameterType = typeVariable.getUpperBound();
                  
              }
              //参数名
              String parameterName = variableElement.getSimpleName().toString();
              //参数类型
              String parameteKind = methodParameterType.toString();
              types.add(methodParameterType.toString());
          }
      }

05.修饰属性、类成员的注解和VariableElement

  • 当你有一个注解是以@Target(ElementType.FIELD)定义时,表示该注解只能修饰属性、类成员。那么这个时候你为了生成代码,而需要获取一些基本信息:包名、类名、类成员类型、类成员名。
    • 代码如下所示
      for (Element element : roundEnv.getElementsAnnotatedWith(xxx.class)) {
      	//ElementType.FIELD注解可以直接强转VariableElement
      	VariableElement variableElement = (VariableElement) element;
      	
          TypeElement classElement = (TypeElement) element
                  .getEnclosingElement();
          PackageElement packageElement = elementUtils.getPackageOf(classElement);
          //类名
          String className = classElement.getSimpleName().toString();
          //包名
          String packageName = packageElement.getQualifiedName().toString();
          //类成员名
          String variableName = variableElement.getSimpleName().toString();
          
          //类成员类型
          TypeMirror typeMirror = variableElement.asType();
          String type = typeMirror.toString();
      }

06.修饰类的注解和TypeElement

  • 当你有一个注解是以@Target(ElementType.TYPE)定义时,表示该注解只能修饰类、接口、枚举。那么这个时候你为了生成代码,而需要获取一些基本信息:包名、类名、全类名、父类。
    • 代码如下所示
      //遍历项目中所有的xxx注解
      for (Element element : roundEnv.getElementsAnnotatedWith(xxx.class)) {
      	//ElementType.TYPE注解可以直接强转TypeElement
          TypeElement classElement = (TypeElement) element;
          
          PackageElement packageElement = (PackageElement) element
                      .getEnclosingElement();
                      
          //全类名
          String fullClassName = classElement.getQualifiedName().toString();
          //类名
          String className = classElement.getSimpleName().toString();
          //包名
          String packageName = packageElement.getQualifiedName().toString();
      	//父类名
      	String superClassName = classElement.getSuperclass().toString();
      }
贡献者: yangchong211
上一篇
/zh/android/basic/9.1注解设计思想和原理.html
下一篇
9.3APT多种案例实践