0%

一个简单的路由

前言

在页面跳转的过程中,使用路由已不是什么新鞋事,市面上已经有很多库可以做这件事,比如 ARouter, WMRouter,Router等等
这些路由框架,都是使用 APT 的原理来实现的。通过之前的 注解处理器APT初探SugarAdapter 注解处理器分析 ,我们对 APT 的使用有了一些认识。

因此,这里我们简单尝试一下,实现一个史上最最简单的路由。通过这个过程我们可以了解一下,实现一个真正好用的路由,需要考虑哪些问题,又有哪些难点需要去处理。

史上最简单路由的诞生

目标

作为史上最简单的路由,我们的目标很简单,用路由的方式实现 Activity 的跳转

1
2
3
  mJump.setOnClickListener(v ->
QRouterApi.go(this, "/activity/second")
);
1
2
3
4
@QRouter("/activity/second")
public class SecondActivity extends AppCompatActivity {
....onCreate .... stuff ...
}

上述点击事件,可以正确跳转到 “/activity/second” 注解所在的 SecondActivity(假设我们现在无法直接依赖 SecondActivity.class,😎😎)

下面就一步一步走,看看如何实现这个简单的小目标。

路由表生成

注解定义

1
2
3
4
5
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.TYPE)
public @interface QRouter {
String value();
}

首先,为路由定义一个注解 QRouter,只接受一个参数,也就是路由地址。

注解处理

关于注解处理器的使用细节这里就不再赘述,注解处理器APT初探 已经有过深入分析了。这里直接分析看看 process 如何实现。

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
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
mMessager.printMessage(Diagnostic.Kind.NOTE, "processing....");


Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(QRouter.class);
for (Element element : elements) {
if (element instanceof TypeElement) {


if (mQRouterCreatorProxy == null) {
mQRouterCreatorProxy = new QRouterCreatorProxy(mElementUtils, (TypeElement) element);
}

QRouter routerAnnotation = element.getAnnotation(QRouter.class);
String router = routerAnnotation.value();
String target = ((TypeElement) element).getQualifiedName().toString();

mQRouterCreatorProxy.putElement(router, target);

} else {
mMessager.printMessage(Diagnostic.Kind.ERROR, "annotation on mistake type, the type should be class");
}
}

generateRouterTable();

return true;
}

这里还是沿袭之前 BingView 的思路,用代理类 QRouterCreatorProxy 处理所有的关键信息,processor 将我们需要的信息,即

1
2
3
 String router = routerAnnotation.value();
String target = ((TypeElement) element).getQualifiedName().toString();
mQRouterCreatorProxy.putElement(router, target);

注解的路由地址和路由所在的目标类,通过 mQRouterCreatorProxy 做一次聚合。这样代理类包含了所有的路由信息,接下来就是通过这些信息,生成一些代码。

那么要生成什么样的代码呢?想想我们的目标,通过路由打开 Activity 。那么我们很容易想到的一种做法,就是建立一个 路由和 Activity 的对应关系,然后通过这个关系找到对应的 Activity 。下面就按照这个思路来生成代码。

生成代码

1
2
3
4
5
6
7
8
9
10
private void generateRouterTable() {
TypeSpec typeSpec = mQRouterCreatorProxy.genCode();
String packageName = mQRouterCreatorProxy.getPackageName();
JavaFile javaFile = JavaFile.builder(packageName, typeSpec).build();
try {
javaFile.writeTo(mFiler);
} catch (IOException e) {
e.printStackTrace();
}
}
  • gencode 的实现
1
2
3
4
5
6
7
8
9
10
11
public class QRouterCreatorProxy {
private static final String Q_ROUTER_CLASS_NAME = "QRouterTable";

public TypeSpec genCode() {
TypeSpec typeSpec = TypeSpec.classBuilder(Q_ROUTER_CLASS_NAME)
.addModifiers(Modifier.FINAL, Modifier.PUBLIC)
.addMethod(genRouterTable())
.build();
return typeSpec;
}
}

这里首先会生成一个名为 QRouterTable 的类,是 final 且 public 类型的,并且还包括一个方法,继续看

  • genRouterTable 实现
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
public class QRouterCreatorProxy {

private HashMap<String, String> elements = new HashMap<>();

public void putElement(String router, String targetClass) {
elements.put(router, targetClass);
}

/**
* @return HashMap<String, String>
*/
private MethodSpec genRouterTable() {
/*
```Map<String, String>```
*/
ParameterizedTypeName inputMapTypeOfGroup = ParameterizedTypeName.get(
ClassName.get(Map.class),
ClassName.get(String.class),
ClassName.get(String.class)
);

MethodSpec.Builder builder = MethodSpec.methodBuilder("map")
.addModifiers(Modifier.PUBLIC, Modifier.FINAL)
.returns(inputMapTypeOfGroup);

builder.addCode("\n");
builder.addStatement("$T<$T,$T> tables=new $T()", HashMap.class, String.class, String.class, HashMap.class);
builder.addCode("\n");

for (String key : elements.keySet()) {
String target = elements.get(key);
String code = "tables.put(\"" + key + "\",\"" + target + "\");\n";
builder.addCode(code);
}
builder.addCode("return tables;\n");
return builder.build();
}
}

elements 这个散列表里存储的就是路由关系信息。

  • 首先会创建方法声明,是一个 名为 map 返回值为 HashMap<String, String>,final 且 public 类型的方法。
  • 然后,在方法体里通过 addStatement 初始化一个 HashMap<String, String> ,接下来就开始遍历 elements 这个 hashmap ,把之前存储的路由信息,转换成代码的方式进行存储。
  • 最后,返回第二步创建的 HashMap<String, String> 实例即可。

最终我们要生成的代码如下 :

1
2
3
4
5
6
7
8
9
10
public final class QRouterTable {
public final Map<String, String> map() {

HashMap<String,String> tables=new HashMap();

tables.put("/activity/simple","com.engineer.aptlite.SimpleActivity");
tables.put("/activity/second","com.engineer.aptlite.SecondActivity");
return tables;
}
}

小结

至此,我们已经生成了路由表(这里就是一个包含路由和目标 Class 关系的 HashMap)。可以看到,使用 javapoet 生成代码和我们手动写代码的思路是一样的,只不过手写的时候每一个子母都是我们敲出来的(ide 辅助生成,其实也可以理解为手动敲出来的代码吧 🤔🤔),javapoet 通过某些特定的 api 可以帮助我们生成这些代码。本质上都是写文件的过程。javapoet 之于java 程序员,就如同 输入法之于铅笔、钢笔或毛笔吧。

路由 API

路由表已经生成了,那么我们改如何使用这张路由表呢? 想想我们的目标,通过路由打开 Activity,路由表里存着路由和目标 Class 的对应关系,那么事情就很简单了。

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
public class QRouterApi {

public static void go(Activity activity, String route) {
try {
long begin = System.currentTimeMillis();
String log;

Class tableClass = Class.forName("com.engineer.aptlite.QRouterTable");
Method methodMap = tableClass.getMethod("map", null);
HashMap<String, String> tables = (HashMap<String, String>) methodMap.invoke(tableClass.newInstance(), null);

System.err.println("tables===" + tables);

String target = tables.get(route);

log = String.format(Locale.CHINA, "use %f to find the target", 1000f * (System.currentTimeMillis() - begin));
System.err.println(log);

if (!TextUtils.isEmpty(target)) {
Class targetClass = Class.forName(target);
Intent intent = new Intent(activity, targetClass);
activity.startActivity(intent);

log = String.format(Locale.CHINA, "use %f to finish", 1000f * (System.currentTimeMillis() - begin));
System.err.println(log);
}
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
.... other execption stuff...
}
}

这个静态方法 go(activity,route) 的核心

  • Class tableClass = Class.forName(“com.engineer.aptlite.QRouterTable”); 加载路由表
  • 反射获取 hashmap 的实例
  • String target = tables.get(route); 从 hashmap 中找到路由地址对应的 Class
  • Class targetClass = Class.forName(target); 加载这个 Class
  • 创建 Intent 实例,然后 startActivity(intent) 实现 Activity 的跳转。

以上实现,为史上最简单、最丑陋且最低效路由跳转 Activity 的方法,看完之后,请立刻遗忘 😳😳

这样,我们通过调用

1
QRouterApi.go(this, "/activity/second")

就可以打开 @QRouter(“/activity/second”) 这个注解所在的 Activity 了。有兴趣的同学可以参考源码,简单体验一下效果。

槽点

在我们思考什么是高效路由之前,首先简单总结一下这个史上最简单路由有哪些问题(以目标为导向来对比)

  • go 方法多次使用发射,效率堪忧。目标 Class 其实是可以直接获取到,在编译期在 HashMap 中存储的时候,可以直接保存 Class,而不是 Class 路径的字符串。
  • 无法支持 startActivityForResult 的情景
  • 没有很好的容错机制

一个高效的路由应该是什么样子的

看完上面的实现,我们可以看到,如果只是为了简单纯粹的解耦,解决类依赖的问题,那么路由的实现其实是比较简单的。但实际情况中,并不是这样。一个高效的路由框架需要考虑的点其实很多。

  • 支持数据通信

支持数据通信,其实是一件即可以很简单,又会很复杂的事情。路由地址本质上是一个字符串,对于基本的数据类型 int,float,char 以及 String ,我们可以把需要传递的参数直接写在路由地址上,或 path 或 query中。最终通过解析 URL 都是可以解析到的。但是对于对象来说,就比较难了。Android 中传递对象采用的 Intent 携带 bundle 的机制。路由地址仅仅是一个字符串,是无法直接完成对象传递的。那么是否有其他的方法呢?再有对于基础数据类型,如果区分参数和实际地址呢?

  • 高效准确,不能比原生的跳转机制差

这个是必须的,使用路由,少不了路由匹配的过程,但并不能因此,导致页面跳转时间变长,让用户体验变得糟糕。

  • 有很好的容错机制,或者说是降级策略

路由地址写错了,或着是目标路由因为某些原因不存在了,是否可以有很好的降级策略,而不是简单的报错或无响应。

  • 路由跳转有拦截机制或优先级机制

在多人协作的开发中,路由地址同名是不太好避免的(一种可行的方案是把所有路由地址定义在一个公共的地方),对于已然无法改动的代码,是否可以通过添加 Interceptor(类似 OKHTTP 的思路) 或定义优先级的方式,解决有冲突的路由;或者因某些特殊业务的需求,可以更改路由原始的跳转行为。

  • 支持组件化(即多 module 的情形)

组件化开发已经成为大型项目的标准,路由的 APT 实现是否可以完美应对多 module 情形下路由表的正确创建及查找解析。

  • 编译期生成代码,不要太耗时

随着使用 APT 的库越来越多,在编译期要做的事情也越多越多,那么是否可以保证在编译期生成代码的过程,可以高效快速的完成(比如增量编译),保证开发人员的效率不受影响。

更多。。。

以上这些点,应该是一个合格的路由应该支持的基础功能(纯属个人观点,😎😎😎😎)。ARouter 在 GitHub 已经有 1 万+ 的 star,说明他还是很受开发者认可的。它的实现是否满足了上述要求,以及它又有哪些独有的特点,我们可以带着这些疑问去学习一下他的实现。

需求推动技术

很多时候,都是需求推动技术进步,没有完美的框架,很多时候都是需要基于特定的业务实施特定的方案,因此。路由也是这样。

总结

回归到本质,在注解处理器的 process 方法中,我们通过

1
Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(QRouter.class);

这里的 Element 包含着这个注解所在类的全部信息,包括他所在类以及他的父类等各种信息,有了这些信息我们可以做很多事情。

就比如这里的 QRouter 注解,把这个注解打在 Activity 上,并在编译期生成路由表,而在运行时通过路由表里的信息,在特定的 API 里封装的 startActivity 这种类似模板的代码,对外暴露了更轻量级的 API,使得页面的跳转实现变得似乎简单了许多。
API.go 一行简单的代码简化了原本的复杂实现,对开发者来说更加友好了。

路由的实现,在某些方面还是非常依赖具体业务规范的。没有完美的路由,只有合适的路由。

加个鸡腿呗.