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
5javaCompileOptions {
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
34private 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
2TypeElement 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
7public 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"));
}
}