ARouter解析(前)- 注解、注解处理器、JavaPoet

580 阅读12分钟

前言

本来想直接洋洋洒洒开始对ARouter进行一顿分析,但是发现如果读者没有掌握好前置的知识点,在解释编译期原理时读起来会云里雾里。

那么我就打算在我们正式接触ARouter原理之前,我先开一篇介绍注解、注解处理器、JavaPoet的文章,接触这些知识是为了我们更好的在学习ARouter编译期原理时更加容易理解,以及甚至可以运用到我们的项目里面,对项目进行优化。

那么事不宜迟,开始吧!

1.注解

1.1 什么是注解?

什么是注解?首先先说注释,注释是面向开发者的,简单来说是给开发者看的,那么注解就是面向程序,是给程序看的,那问题来了,程序要怎么看?

程序要解析注解,主要通过两种方式来解析:

  1. 反射
  2. APT(Annotation Processing Tool) 即注解处理器

本篇里面会会通过简单的例子来教大家怎么通过反射与APT来解析注解,接下来我们看看怎么声明注解~

但是首先在学习声明注解之前,首先要了解元注解是什么。

1.2 元注解

元注解其实也是一种注解,需要声明在注解上,负责向程序说明注解的基础信息(作用范围,时机等)。

哈哈,看到这里感觉太抽象了是吧,没关系,我们慢慢会理解上面那句话。

  • @Target:指定注解的作用范围
  • @Retention:指定注解的作用时机
  • @Inherited:被该注解修饰的注解,作用在某个类上,该类的此注解可以被子类继承
  • @Documented:给Javadoc配置的,但不常用

1.2.1 @Target

既然负责指定注解的作用范围,那么肯定有对应的值,我们通过一个表格理解一下,有哪些值以及值的作用

作用范围
ElementType.TYPE类、接口(包括注解类型)或枚举声明
ElementType.FIELD字段声明(包括枚举常量)
ElementType.METHOD方法声明
ElementType.PARAMETER形参声明
ElementType.CONSTRUCTOR构造函数声明
ElementType.LOCAL_VARIABLE局部变量声明
ElementType.ANNOTATION_TYPE注解类型声明
ElementType.PACKAGE包声明
ElementType.TYPE_PARAMETER类型参数声明
ElementType.TYPE_USE类型的使用
ElementType.MODULE模块声明

最后5个其实很少使用,我们围绕高频的几个来举例感受一下。

我用Retrofit一些经典作为例子,这里我默认大家都用过Retrofit,如果没用过的话我只能说,快!去!用!不用也看一下!

这里顺便安利我Retrofit的一篇文章:Retrofit原理全解析

  //一个Retrofit请求方法
  @GET("users/{user}/repos")
  Call<List<Repo>> listRepos(@Path("user") String user);

我们可以看到 ==@GET== 是声明在方法上的,而 ==@PATH== 是声明在参数上的,我再把它们的源码贴一下出来.

@Documented
@Target(METHOD)//@GET是作用在方法上的
@Retention(RUNTIME)
public @interface GET {
  String value() default "";
}

@Documented
@Target(PARAMETER)//@Query是作用在形参上的
@Retention(RUNTIME)
public @interface Query {
  boolean encoded() default false;
}

@Documented
@Retention(RetentionPolicy.CLASS)
@Target({ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD, ElementType.LOCAL_VARIABLE})//亦可以指定多个作用范围
public @interface LayoutRes {
}

通过例子之后,应该可以了解元注解的作用就是一个声明在注解上的注解了吧(禁止套娃)

通过上面的例子,大家应该很清晰的知道Target有什么用了,那么继续下一个。

1.2.2 @Retention

指定注解的作用时机,这个要怎么理解呢,不要着急,我们先看看它有哪些值吧

作用时机
RetentionPolicy.SOURCE指定注解只在源码阶段有用,编译器编译之后将会丢弃,这类型的注解一般用来做代码限制或者提示等。如Override用来提示方法重写了父类的方法,如果在没有重写父类方法的方法上面使用此注解,编译器会报错。
RetentionPolicy.CLASS指定注解将由编译器编译后记录在类文件中,但在运行时虚拟机不会保留。Android中经常用到一些方法参数的限制中,如即将学习的ARouter中的@Route,另外此值为@Retention默认的参数。
RetentionPolicy.RUNTIME指定注解会被编译器记录在类文件中,并在运行时虚拟机会保留,因此它们可以在程序运行时通过反射读取。

这里三个值对应的是源码、编译时、运行时三个阶段,亦意味着我们的注解到底保留到什么阶段,比如说运行时是读取不到声明着SOURCE的注解,声明这个元注解需要清晰的知道自己的注解是什么阶段使用。

另外@Inherited和@Documented因为不常用加上篇幅问题就不在这里展开说了,有兴趣的童鞋可以去搜索一下对应的使用方式。

1.3 声明注解

ARouter的注解基本都是编译解,Retrofit的注解基本上都是运行时,我们可以通过这两个经典的框架来理解注解使用的场景与用法。

1.3.1 运行时注解

Retrofit是如何声明一个@Post注解的?

@Target(METHOD)//表明Post是声明在方法上的一个注解
@Retention(RUNTIME)//表明该注解是运行时注解,可在运行时通过反射解析
public @interface POST {
  //声明一个接口地址/路径的值
  String value() default "";
}

接下来我们通过该注解声明一个Post请求。

 interface Api{
    //POST中的字符串,其实就是上面value字段
    @POST("users/xxx/repos")
    Call<List<Repo>> listRepos();
 }

这样我们就为该方法添加上@Post注解,并且设置好将要访问的域名了,接着我们看看Retrofit运行时,怎么获得这些注解信息。

1.3.2 通过反射获取注解信息

关于Retrofit解析的过程我不会详细的列出,有兴趣的小伙伴可以点击我上面Retrofit的源码分析连接去看,下面只会分析关键逻辑。

   //解析一个方法上的注解
   void parseAnnotations (Method method) {
      //Retorfit通过动态代理的回调获得了Method对象,亦就是我们上面申明的listRepos方法本身.
      //Method对象包含着一个方法相关的信息.
      this.method = method;
      //通过该方法获取该方法上的注解数组
      Annotation[] methodAnnotations = method.getAnnotations();
      //遍历该数组,获取我们的POST注解
      for (Annotation annotation : methodAnnotations) {
          //通过类型判断获取注解
          if (annotation instanceof POST) {
            //强转类型,获取Post中的value值,最后获得需要请求的地址或路径
            String requestPath= ((POST) annotation).value();
            //生成请求(伪代码)
            buildRequest(requestPath);
          }
      }
    }

说实话通过反射解析注解可以说是没难度。

但是重点还是体现在编程维度有点不一样了,我们一般通过代码层面实现的东西居然通过了注解去实现,对比起通过代码声明,这种方式反而更加直观优雅,我希望说到这里对大家对于编程维度的扩展有一定帮助,在日常编程中可以寻找不同方式去实现自己的功能。

1.3.3 编译时注解

其实运行时注解并不是我重点想说的,因为我们系列的文章是围绕ARouter的,而在ARouter的几个注解都是编译时注解。

而且编译时注解我相信大部分开发者都很少亲自去写与解析,我们很少去关注编译时会发生什么,我们只关注运行时发生了什么,但是通过学习ARouter之后你会发现,ARouter通过编译时解析路由注解对路由信息进行预处理,将原本应该在运行时做的工作,放在了编译时完成,这意味着什么?

想象一下你的项目有几百个页面,但是编译时预处理意味着我们不需要在运行时扫描整个项目,不需要浪费性能遍历每个类的注解和处理注解中的信息,这就是性能优化!

而怎么声明编译时注解,我觉得已经不用举例了,但是目前大家肯定有疑问:

  1. 所以怎么在编译时解析注解?
  2. ARouter又是怎么做预处理的?

以上2个问题就是接下来准备展开解释的,首先我们解析第一个问题,那么事不宜迟我们进入下一章

2.注解处理器

2.1 什么是注解处理器?

APT(Annotation Processing Tool) ,是一种注解处理工具,用来在编译期扫描和处理注解。

APT 的原理是在注解了某些代码元素(如字段、函数、类等)后,在编译时编译器会检查 AbstractProcessor 的子类,并且自动调用其 process() 方法,然后将添加了指定注解的所有代码元素作为参数传递给该方法,开发者再根据注解元素在编译期做相应的事情。

听起来,其实思路跟用反射一样,那么我们接下来只需要学习如何声明与使用它就可以达到我们的目的了。

2.2 声明一个注解处理器

2.2.1 首先我们声明一个Java Library

QQ图片20230426213746.png 这里我的AndroidStudio版本比较高,所以旧版的同学可能有点不一样哈。

2.2.2 导入相关依赖

在新建的JavaLibrary中,我们通过其build.gradle,导入一下注解处理器相关依赖

dependencies {
    annotationProcessor 'com.google.auto.service:auto-service:1.0-rc7'
    compileOnly 'com.google.auto.service:auto-service-annotations:1.0-rc7'
}

因为我们需要在编译期注册我们的注解处理器,这样才会执行到我们的注解处理器,虽然有几种注册方式,但是我们接下来使用AutoService来进行自动注册,其他注册方式有兴趣可以自行百度。

2.2.3 声明注解处理器类

//声明AutoService,自动注册编译器中
@AutoService(Processor.class)
public class DemoProcessor extends AbstractProcessor { //继承AbstractProcessor
    //文件工具
    Filer mFiler;
    //处理 TypeMirror 的工具类,用于取类信息
    Types types;
    //处理 Element 的工具类,用于获取程序的元素,例如包、类、方法。
    Elements elementUtils;
    //类似Log的工具
    Messager messager;
    //初始化
    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
        mFiler = processingEnv.getFiler();
        types = processingEnv.getTypeUtils();
        elementUtils = processingEnv.getElementUtils();
        messager= processingEnv.getMessager();
        
    }
    
     /**
     * 设置支持的源版本,默认为RELEASE_6
     * 两种方式设置版本:
     * 1. 此处返回指定版本
     * 2. 类上面设置SupportedSourceVersion注解,并传入版本号
     */
    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }
    
     /**
     * 设置支持的注解类型,默认为空集合(都不支持)
     * 两种方式设置注解集合:
     * 1. 此处返回支持的注解集合
     * 2. 类上面设置SupportedAnnotationTypes注解,并传入需要支持的注解
     */
    @Override
    public Set<String> getSupportedAnnotationTypes() {
        Set<String> types = new HashSet<>();
        //添加对应注解的完整类名
        typs.add(Router.class.getCanonicalName())
        return types;
    }
    
    //抽象方法,实现注解处理器的具体逻辑。获取被注解的元素,生成 Java 源代码等信息。
    //返回值表示注解是否由当前 Processor 处理。如果返回 true,则这些注解由此注解来处理,后续其它的 Processor 无需再处理它们;
    //如果返回false,则这些注解未在此 Processor 中处理并,那么后续 Processor 可以继续处理它们,类似责任链设计的一种模式
    @Override public boolean process(Set<? extends TypeElement> set, RoundEnvironment env) {
    //获取项目中声明了Router注解的类
    Set<? extends Element> elements = env.getElementsAnnotatedWith(Router.class);
    //遍历每个元素,获取Element元素,下面会有个表格解释Element有哪些,我们目前知道该Element指的是一个类对象就可以了。
    for (Element element : elements) {
        //获取类上声明的Router注解
        Router annotation = element.getAnnotation(Router.class);
        //拿到注解之后就可以拿里面的信息啦~
        //do something...
    }
    return false;
}
}

来简单了解一下Element的类型~

类型
TypeElement代表该元素是一个类或者接口
VariableElement代表该元素是字段、方法入参、枚举常量
PackageElement代表该元素是包级别
ExecutableElement代表方法,构造方法
TypeParamentElement代表类型、接口、方法入参的泛型参数

以上就是我们的声明方式了,重点知道以下几点:

  1. 怎么新建一个注解处理器Java Library
  2. 知道怎么将注解处理器注册到编译器中
  3. 知道怎么声明一个注解处理器类及关键方法与对象有什么用

2.2.4 将声明的注解处理器依赖到我们的项目中

在我们项目中,我们将直接处理器依赖进来

dependencies {
    //Kotlin使用Kpt依赖
    kpt project(':demo_compiler')
    //Java使用(没记错的话是这个)
    annotationProcessor project(':demo_compiler')
}

然后Build一下项目!我们写的注解处理器就跑起来了~我们就学会了怎么解析编译时注解了。

3.JavaPoet简单介绍

我们知道了ARouter是通过编译器注解处理器解析注解的,但是解析了之后它做了什么呢?

ARouter生成了一个Java文件,将一个一个解析出来的Route对象分门别类的扔进Map中。

听到这里肯定大写一个卧槽?至少我当时了解之后是卧槽的,生成Java文件?那意味着需要生成类,成员变量,方法等等等等,这怎么做?用文件流输出字符串吗?格式怎么办?

不用担心,已经有工具替我们做这件事了,接下来我们就学习使用这个工具就可以了,它就是 JavaPoet!

3.1 JavaPoet是啥?

JavaPoet 是 square 开源的 Java 代码生成框架,可以很方便地通过其提供的 API 来生成指定格式(修饰符、返回值、参数、函数体等)的代码。

接下来,我们就简单学习一下JavaPoet是怎么使用的!

3.2 使用JavaPoet生成Java类!

比如说我们要通过JavaPoet生成以下内容

class Hello {
  
  void sayHello(){
      System.out.println("丢雷楼妹");
  }
    
}

我们通过JavaPoet可以这么写

//声明一个方法
MethodSpec main =
    MethodSpec.methodBuilder("sayHello")//声明方法名
    .addModifiers(Modifier.PUBLIC)//声明修饰符
    .returns(void.class)//返回值
    .addStatement("$T.out.println($S)", System.class, "Hello, JavaPoet!")//声明内容
    .build();

//声明一个类
TypeSpec helloWorld = TypeSpec.classBuilder("Hello")
    .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
    .addMethod(main)//添加方法
    .build();


JavaFile javaFile = JavaFile.builder("com.example.helloworld", helloWorld)
    .build();
    
//输出文件
javaFile.writeTo(System.out);

这样就搞掂了~

但是这里捏,我就不详细的介绍这个JavaPoet怎么用了,但是这个是真的是黑科技不说笑~

想学习的小伙伴就点击JavaPoet文档,去学习吧!

以上就是ARouter原理解析系列的前瞻!

接下来开坑ARouter从编译期到运行期的原理是什么?感兴趣的小伙伴请关注我!关注我!关注我!并且点赞!点赞!点赞!

Jetpack系列文章:

ViewModel探索(1)

ViewModel探索(2)- SavedState

LiveData原理解析

Lifecycle原理剖析