手把手教你写Router框架入门篇
前言
本文代码在Github上面,可以自行查看,代码还是挺简单的。
最近项目在组件化,模块内的跳转使用了Router框架,通信暂时还有用的接口,没想到好的方法。关于组件化和Router框架是什么这里不作介绍请自行Google。
Router框架的好处可以解耦,还有在Push的时候可能会比较方便,在后台直接配置Activity的名称硬编码并不好,还有一个是一般现在的客户端,App里面的模块都有Web版的,使用Router框架的话,在跳转的过程中,如果本地模块没有可以去自动做些处理,使用浏览器打开对应的模块的。
这篇文章着重说一下编译时候的注解处理器,其实注解的使用在Router框架中的占比非常小,主要的处理还是在Router库里面。由于我自己并不熟悉编译时的注解的写法,所以开篇的文章先写一下注解处理器的使用。
不使用注解处理器的基本Router框架的写法
路由框架,顾名思义就是通过一个路由地址可以跳转到对应的页面去。这里的页面只针对Activity,Fragment其实没必要,个人感觉Fragment最好通过Activity的参数处理。
为了对应起Activity和路由地址,我们需要有一个表来进行记录,这里路由地址最好制定好相关的协议,我是希望一个路由地址大概如下:
1 | scheme://module/界面/params |
路由里面支持参数对于Push或者Web跳转到相应的界面是非常方便的。
一个不使用注解处理器的路由框架,需要自己手动把每个界面和相应的Activity对应起来。这个处理过程可以在Application里面做的。
大致步骤:
1.定义接口,让调用着可以去注册。
2.写RouterManager类,去实现跳转。
首先定义一个接口如下:
1 | public interface IRoute { |
使用Map将路由地址和Activity关联起来。
然后完成RouterManager大概如下所示。
1 | public class RouterManager { |
使用者只需要在Application里面调用init方法即可。
1 | public class RouterApplication extends Application { |
相应的代码在Github上面。
使用注解处理器自动完成initRouter过程
上面的框架已经基本可以使用了,就是Application里面的注册比较麻烦,Activity比较少还好,多的话就比较麻烦了。下面就说一下使用注解处理器自动完成这个过程。
一些基本概念
首先这里讨论的不是在运行时通过反射去调用的注解(不过在编写API的过程中,看想要调用者怎么使用,可能会用到一些反射的),而是在编译时,扫描字节码文件,根据相应的注解生成特定的代码。
注解处理器:一个在javac中,用来编译时扫描和处理的注解的工具。你可以为特定的注解(你自己写的注解啦)注册你自己的注解处理器。
AbstractProcessor
每一个处理器都是继承于AbstractProcessor。我们可以继承并复写一些里面的方法,这些方法java虚拟机会自动调用的。
1 | public class MyProcessor extends AbstractProcessor { |
init(ProcessingEnvironment processingEnv)
这个方法会被注解处理工具调用,并传入ProcessingEnvironment
参数,这个参数携带啦很多有用的信息后面会用到。process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv)
处理函数,我们需要在这个函数里面写处理特定注解的代码,并生成相应的java文件,RoundEnvironment
通过这个参数我们可以拿到带有特定注解的元素(类,字段,方法啦)。getSupportedAnnotationTypes
指定当前的注解处理器处理哪些注解,从返回值可以看到是返回Set
的,那就是一个注解处理器可以处理多个注解的。getSupportedSourceVersion
指定你使用的java版本,通常这里直接返回SourceVersion.latestSupported()
,可以看这个方法的具体实现。
在java 7中,可以使用注解来代替最后的两个方法。
1 |
不过还是建议使用复写的方式吧。
注册你的处理器
在你提供的jar包中,需要有特定的文件在META-INF/services中,文件名是javax.annotation.processing.Processor
内容是处理器的路径,多个的就没行一个啦。类似下面。
处理器的编写
其实编写编译时注解处理器,首先要知道我们要干什么,上面一开始就把我们要干什么事情分析的很清楚了,我们需要写一个类,这个类去实现IRoute
接口,在initRouter
方法里面去实现关联Activity和URL。
注意
这个类实现完了,我们怎么调用,这个要想明白先。
- 1.让使用者手动调用,因为这个类在MakeProject的时候是生成了的,并且类名我们都写死了,所以可以让使用者手动调用,这就会比较麻烦一些,我们只生成一个类,还好,如果生成的多了,使用者可能会崩溃。
- 2.在提供对外使用的API中使用反射调用,原因和上面一样,类名是死的,就算不是死的,我们也知道类名的生成规则的。这就是上面提到的可能会用到一些反射的原因。
编写编译时注解的library是有一定套路的,一般编写这种库,会建三个module,一个是只存放注解的库,一个是注解处理库(这个只是处理注解,并不会增加apk的大小啦),一个是提供对外使用的API库,前面已经说到,其实这种库,注解处理器的作用很小的,只是提供某一个功能,其余的99%的功能都是对外使用的API库做的(就是说可以只有API库,其余的需要编译时注解处理的工作可以手动处理)。这个很重要,要记住。一般项目划分如下。
注解库实现
注解库只专注提供注解给API库和注解处理器库使用,本身并不做其他的操作。这里我们只需要一个注解即可,这个注解是使用在Activity上面的,就是类(继承自Activity的类)上面,所以这里就很简单啦。
1 | package com.ai.router.anno; |
是不是觉得很简单(当然,这篇文章只是个入门篇,其实就算是只使用一个注解类,应该也不会这么简单的使用一个参数啦,这个其实跟你的路由框架的架构设计有关的,你的路由库准备怎样设计使用的路由,要不要使用scheme,要不要二级path,这个都是需要提前想好整个大体的框架,然后画个图,自己好好研究,写个库哪这么简单😢)。
注解处理库实现
其实代码也特别少,这里先贴出代码,然后慢慢讲解的。
1 | package com.ai.router.compiler; |
详细分析
1.init
前面说啦,ProcessingEnvironment
会携带一些有用的东西,我们后面需要用到,这里就把这些对象取出来,放在一个单独的类里面方便随时调用。UtilManager
的实现如下,很简单。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
30public class UtilManager {
/**
* 一个用来处理TypeMirror的工具类
*/
private Types typeUtils;
/**
* 一个用来处理Element的工具类
*/
private Elements elementUtils;
/**
* 正如这个名字所示,使用Filer你可以创建文件
*/
private Filer filer;
/**
* 日志相关的辅助类
*/
private Messager messager;
private static UtilManager mgr = new UtilManager();
public void init(ProcessingEnvironment environment) {
setTypeUtils(environment.getTypeUtils());
setElementUtils(environment.getElementUtils());
setFiler(environment.getFiler());
setMessager(environment.getMessager());
}
private UtilManager() {
}
}上面的注释也很清晰了,具体的怎么用,要到用到的时候才能理解。比如我们可以通过Elements.getTypeElement(“android.app.Activity”)得到Activity的type element,这个非常有作用,后面就会看到。
还可以通过Messager.printMessage()方法输出一些我们想要的信息。
在注解处理的过程,源码的每个部分都是特定的Element。
如下:1
2
3
4
5
6
7
8
9package com.example; // PackageElement
public class Foo { // TypeElement
private int a; // VariableElement
private Foo other; // VariableElement
public Foo () {} // ExecuteableElement
public void setA ( // ExecuteableElement
int newA // TypeElement
) {}
}2.process
首先,我们要得到被Route
注解的Activity的Element集合,显然我们规定了Route是作用于类上面的,即上面的TypeElement
,还有不仅是作用于类,而且是Activity的子类上面。Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(Route.class);
这句代码即是获取所有被Route
注解的Element
的。if (!Utils.checkTypeValid(element)) continue;
这个是检测被Route
修饰的Element
是不是类(TypeElement
),并且是不是Activity
的字类啦。检测的方法这里就不分析啦,GitHub上面的代码有注释,可以去看看。1
2
3TypeElement typeElement = (TypeElement) element;
Route route = typeElement.getAnnotation(Route.class);
targetInfos.add(new TargetInfo(typeElement, route.value()));这三句代码,比较重要,我们来分析一下,一个被
Route
注释的类表示一个需要被添加到路由表里面的数据。这里使用List记录起来。typeElement
就是那个Activity的字类,route.value()
就是Activity的路由地址。3.generateCode生成java文件
我们其实已经知道我们最终需要的文件是什么样子的啦。1
2
3
4
5
6
7
8
9package com.ai.router.impl;
public class AppRouter implements IRoute {
public void initRouter(Map<String, Class<? extends Activity>> routers) {
routers.put("router://activity/main2", Main2Activity.class);
routers.put("router://activity/main3", Main3Activity.class);
routers.put("router://activity/main", MainActivity.class);
}
}然后写相应的代码即可,这里生成java文件,我使用的是square的开源项目javapoet关于它的用法这里就不说了,这个不重要,可以自行搜索用法。
4.Router API库编写
上面一直说要实现的接口,其实定义在这个库里面的。1
2
3public interface IRoute {
void initRouter(Map<String, Class<? extends Activity>> routers);
}主要的功能实现类。
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
55package com.ai.router;
public class RouterManager {
private static volatile RouterManager sManager;
private Map<String, Class<? extends Activity>> mTables;
private String mSchemeprefix;
private RouterManager() {
mTables = new HashMap<>();
}
public static RouterManager getManager() {
if (sManager == null) {
synchronized (RouterManager.class) {
if (sManager == null) {
sManager = new RouterManager();
}
}
}
return sManager;
}
public void init() {
try {
String className = "com.ai.router.impl.AppRouter";
Class<?> moduleRouteTable = Class.forName(className);
Constructor constructor = moduleRouteTable.getConstructor();
IRoute instance = (IRoute) constructor.newInstance();
instance.initRouter(mTables);
} catch (Exception e) {
e.printStackTrace();
}
}
public void setSetSchemeprefix(String setSchemeprefix) {
this.mSchemeprefix = setSchemeprefix;
}
public void openResult(Context context, String path) {
if (!TextUtils.isEmpty(mSchemeprefix)) {
// router://activity/main
path = mSchemeprefix + "://" + path;
}
try {
Class aClass = mTables.get(path);
Intent intent = new Intent(context, aClass);
if (!(context instanceof Activity)) {
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
}
context.startActivity(intent);
} catch (Exception e) {
e.printStackTrace();
}
}
}这里就提供两个最简单的功能
init
注册路由框架,init使用的是反射,拿到AppRouter
,它实现了IRoute,调用就很简单了,直接调用initRouter方法即可。当然这个方法需要使用者在Application里面去调用。openResult
打开对应的Activity,代码很简单,一看即懂。5.使用
Application里面1
2
3
4
5
6
7
8
9
10
11
12public class RouterApplication extends Application {
public void onCreate() {
super.onCreate();
initRouter();
}
private void initRouter() {
RouterManager manager = RouterManager.getManager();
manager.setSetSchemeprefix("router");
manager.init();
}
}打开对应的页面:
1
`RouterManager.getManager().openResult(this, "activity/main");`
以上,一个最简单的使用编译时注解的Router框架就完成了,这篇文章着重讲了编译时注解的使用。一个Router框架不会这么简单的啦。