Android 如何生成路由映射文件

01.前沿介绍

  • 在Activity类上加上@Router注解之后,便可通过apt来生成对应的路由表,那么这节就来讲述一下如何通过apt来生成路由表。
  • APT是Annotation Processing Tool的简称,即注解处理工具。它是在编译期对代码中指定的注解进行解析,然后做一些其他处理(如通过javapoet生成新的Java文件)。
  • 整个生成映射文件,大都是仿照阿里ARouter路由器。这里学习使用!

02.定义注解处理器

  • 用来在编译期扫描加入@Route注解的类,然后做处理。这也是apt最核心的一步,新建RouterProcessor 继承自 AbstractProcessor,然后实现process方法。在项目编译期会执行RouterProcessor的process()方法,我们便可以在这个方法里处理Route注解了。

  • 此时我们需要为RouterProcessor指明它需要处理什么注解,这里引入一个google开源的自动注册工具AutoService,如下依赖:

    1
    implementation 'com.google.auto.service:auto-service:1.0-rc3'
  • 这个工具可以通过添加注解来为RouterProcessor指定它需要的配置,如下所示

    1
    2
    3
    4
    5
    @AutoService(Processor.class)
    public class RouterProcessor extends AbstractProcessor {

    //...
    }
  • 比较完整的RouterProcessor注解处理器配置如下

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    @AutoService(Processor.class)
    /**
    处理器接收的参数 替代 {@link AbstractProcessor#getSupportedOptions()} 函数
    */
    @SupportedOptions(RouterConstants.ARGUMENTS_NAME)
    public class RouterProcessor extends AbstractProcessor {


    /**
    * key:组名 value:类名
    */
    private Map<String, String> rootMap = new TreeMap<>();
    /**
    * 分组 key:组名 value:对应组的路由信息
    */
    private Map<String, List<RouteMeta>> groupMap = new HashMap<>();

    private Elements elementUtils;
    private Filer filer;
    private Types typeUtils;
    private RouterLog log;
    private String moduleName;

    /**
    * 初始化方法
    * @param processingEnvironment 获取信息
    */
    @Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {
    super.init(processingEnvironment);
    //节点工具类 (类、函数、属性都是节点)
    elementUtils = processingEnvironment.getElementUtils();
    //文件生成器 类/资源
    filer = processingEnvironment.getFiler();
    Messager messager = processingEnvironment.getMessager();
    log = RouterLog.newLog(messager);
    Map<String, String> options = processingEnvironment.getOptions();
    //type(类信息)工具类
    typeUtils = processingEnvironment.getTypeUtils();

    //参数是模块名 为了防止多模块/组件化开发的时候 生成相同的编译文件 xx$$ROOT$$文件
    if (!RouterUtils.isEmpty(options)) {
    moduleName = options.get(RouterConstants.ARGUMENTS_NAME);
    }
    if (RouterUtils.isEmpty(moduleName)) {
    throw new EmptyException("RouterProcessor Not set processor moduleName option !");
    }
    log.i("RouterProcessor init RouterProcessor " + moduleName + " success !");
    }

    /**
    * 所有的注解处理都是从这个方法开始的,你可以理解为,当APT找到所有需要处理的注解后,会回调这个方法,
    * 你可以通过这个方法的参数,拿到你所需要的信息。
    *
    * 参数Set<? extends TypeElement> annotations:将返回所有由该Processor处理,并待处理的Annotations。
    * (属于该Processor处理的注解,但并未被使用,不存在与这个集合里)
    *
    * 参数 RoundEnvironment roundEnv :表示当前或是之前的运行环境,可以通过该对象查找找到的注解。
    * @param annotations annotations
    * @param roundEnv roundEnv
    * @return 返回值 表示这组 annotations 是否被这个 Processor 接受,
    * 如果接受true后续子的 Processor不会再对这个Annotations进行处理
    */
    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
    if (!RouterUtils.isEmpty(annotations)) {
    log.i("RouterProcessor Router 注解的节点集合");
    //被 Router 注解的节点集合
    Set<? extends Element> rootElements = roundEnv.getElementsAnnotatedWith(Router.class);
    if (!RouterUtils.isEmpty(rootElements)) {
    processorRouter(rootElements);
    }
    return true;
    }
    return false;
    }
    }

03.如何拿到module名称

  • 通过@SupportedOptions(Constant.ARGUMENTS_NAME)拿到每个module的名字,用来生成对应module下存放路由信息的类文件名。在这之前,我们需要在module的gradle下配置如下

    1
    2
    3
    4
    5
    javaCompileOptions {
    annotationProcessorOptions {
    arguments = [moduleName: project.getName()]
    }
    }
  • Constant.ARGUMENTS_NAME便是每个module的名字。

    • @SupportedAnnotationTypes(Constant.ANNOTATION_TYPE_ROUTE)指定了需要处理的注解的路径地址,在此就是Router.class的路径地址。
    • RouterProcessor中我们实现了init方法,拿到log apt日志输出工具用以输出apt日志信息,并通过以下代码得到上面提到的每个module配置的moduleName
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    @SupportedOptions(RouterConstants.ARGUMENTS_NAME)
    public class RouterProcessor extends AbstractProcessor {

    private String moduleName;

    @Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {
    super.init(processingEnvironment);
    Map<String, String> options = processingEnvironment.getOptions();
    //参数是模块名 为了防止多模块/组件化开发的时候 生成相同的编译文件 xx$$ROOT$$文件
    if (!RouterUtils.isEmpty(options)) {
    moduleName = options.get(RouterConstants.ARGUMENTS_NAME);
    }
    if (RouterUtils.isEmpty(moduleName)) {
    throw new EmptyException("RouterProcessor Not set processor moduleName option !");
    }
    log.i("RouterProcessor init RouterProcessor " + moduleName + " success !");
    }
    }

04.process处理方法

  • 这里在process()里生成文件用javapoet,这是squareup公司开源的一个库,通过调用它的api,可以很方便的生成java文件,在含有注解处理器(demo中apt相关的代码实现都在easy-compiler module中)的module中引入依赖如下:

    1
    implementation 'com.squareup:javapoet:1.10.0'
  • 在process()方法里有如下代码

    • 如果接受true后续子的 Processor不会再对这个Annotations进行处理,也就是Router注解
    • set就是扫描得到的支持处理注解的节点集合,然后得到rootElements,即被@Router注解的节点集合,此时就可以调用 processorRoute(rootElements)方法去生成文件了。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
    if (!RouterUtils.isEmpty(annotations)) {
    log.i("RouterProcessor Router 注解的节点集合");
    //被 Router 注解的节点集合
    Set<? extends Element> rootElements = roundEnv.getElementsAnnotatedWith(Router.class);
    if (!RouterUtils.isEmpty(rootElements)) {
    processorRouter(rootElements);
    }
    return true;
    }
    return false;
    }
  • 看一下processorRouter(rootElements)方法

    • 大概流程如下所示,代码注释都非常详细
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    private void processorRouter(Set<? extends Element> rootElements) {
    //首先获取activity信息
    TypeElement activity = elementUtils.getTypeElement(RouterConstants.ACTIVITY);
    //获取service信息
    TypeElement service = elementUtils.getTypeElement(RouterConstants.I_SERVICE);
    //被 Router 注解的节点集合
    for (Element element : rootElements) {
    RouteMeta routeMeta;
    //类信息
    TypeMirror typeMirror = element.asType();
    log.i("RouterProcessor Route class:" + typeMirror.toString());
    //获取注解
    Router route = element.getAnnotation(Router.class);
    if (typeUtils.isSubtype(typeMirror, activity.asType())) {
    routeMeta = new RouteMeta(RouteMeta.Type.ACTIVITY, route, element);
    } else if (typeUtils.isSubtype(typeMirror, service.asType())) {
    routeMeta = new RouteMeta(RouteMeta.Type.I_SERVICE, route, element);
    } else {
    throw new RuntimeException("RouterProcessor Just support Activity or IService Route: " + element);
    }
    //检查是否配置 group 如果没有配置 则从path截取出组名
    categories(routeMeta);
    }

    //获取
    TypeElement iRouteGroup = elementUtils.getTypeElement(RouterConstants.I_ROUTE_GROUP);
    TypeElement iRouteRoot = elementUtils.getTypeElement(RouterConstants.I_ROUTE_ROOT);

    //生成Group记录分组表
    generatedGroup(iRouteGroup);

    //生成Root类 作用:记录<分组,对应的Group类>
    generatedRoot(iRouteRoot, iRouteGroup);
    }
  • 生成代码逻辑

    • 提到过生成的root文件和group文件分别实现了IRouteRoot和IRouteGroup接口,就是通过下面这两行文件代码拿到IRootGroup和IRootRoot的字节码信息,然后传入generatedGroup(iRouteGroup)和generatedRoot(iRouteRoot, iRouteGroup)方法,这两个方法内部会通过javapoet api生成java文件,并实现这两个接口。
    1
    2
    TypeElement iRouteGroup = elementUtils.getTypeElement(RouterConstants.I_ROUTE_GROUP);
    TypeElement iRouteRoot = elementUtils.getTypeElement(RouterConstants.I_ROUTE_ROOT);
  • 然后看看如何生成代码的,这里只看看生成Group记录分组表的代码

    • ParameterizedTypeName是创建参数类型的api,ParameterSpec是创建参数的实现,MethodSpec是函数的生成实现等等。最后,当参数、方法、类信息都准备好了之后,调用JavaFileapi生成类文件。JavaFile的builder ()方法传入了PACKAGE_OF_GENERATE_FILE变量,这个就是指定生成的类文件的目录,方便我们在app进程启动的时候去遍历拿到这些类文件。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    /**
    * 生成Root类 作用:记录<分组,对应的Group类>
    * @param iRouteRoot iRouteRoot
    * @param iRouteGroup iRouteGroup
    */
    private void generatedRoot(TypeElement iRouteRoot, TypeElement iRouteGroup) {
    //创建参数类型 Map<String,Class<? extends IRouteGroup>> routes>
    //Wildcard 通配符
    ParameterizedTypeName parameterizedTypeName = ParameterizedTypeName.get(
    ClassName.get(Map.class),
    ClassName.get(String.class),
    ParameterizedTypeName.get(
    ClassName.get(Class.class),
    WildcardTypeName.subtypeOf(ClassName.get(iRouteGroup))
    ));
    //参数 Map<String,Class<? extends IRouteGroup>> routes> routes
    ParameterSpec parameter = ParameterSpec.builder(parameterizedTypeName, "routes").build();
    //函数 public void loadInfo(Map<String,Class<? extends IRouteGroup>> routes> routes)
    MethodSpec.Builder methodBuilder = MethodSpec.methodBuilder(RouterConstants.METHOD_LOAD_INTO)
    .addModifiers(Modifier.PUBLIC)
    .addAnnotation(Override.class)
    .addParameter(parameter);
    //函数体
    for (Map.Entry<String, String> entry : rootMap.entrySet()) {
    methodBuilder.addStatement("routes.put($S, $T.class)", entry.getKey(), ClassName.get(RouterConstants.PACKAGE_OF_GENERATE_FILE, entry.getValue()));
    }
    //生成$Root$类
    String className = RouterConstants.NAME_OF_ROOT + moduleName;
    TypeSpec typeSpec = TypeSpec.classBuilder(className)
    .addSuperinterface(ClassName.get(iRouteRoot))
    .addModifiers(Modifier.PUBLIC)
    .addMethod(methodBuilder.build())
    .build();
    //指定路径:com.ycbjie.api.router.routes
    JavaFile.Builder builder = JavaFile.builder(RouterConstants.PACKAGE_OF_GENERATE_FILE, typeSpec);
    JavaFile build = builder.build();
    try {
    build.writeTo(filer);
    log.i("Generated RouteRoot:" + RouterConstants.PACKAGE_OF_GENERATE_FILE + "." + className);
    } catch (IOException e) {
    e.printStackTrace();
    }
    }

05.生成的文件

  • 如下所示

    1
    2
    3
    4
    5
    6
    7
    public class ARouter_Group_main implements IRouteGroup {
    @Override
    public void loadInto(Map<String, RouteMeta> atlas) {
    atlas.put("/main/FiveActivity",RouteMeta.build(RouteMeta.Type.ACTIVITY,FiveActivity.class,"/main/FiveActivity","main"));
    atlas.put("/main/SixActivity",RouteMeta.build(RouteMeta.Type.ACTIVITY,SixActivity.class,"/main/SixActivity","main"));
    }
    }