前言
最近在对项目大刀阔斧的组件化改造,路由框架将是组件化开发中必不可少的,阿里的Arouter诚然是一个非常优秀的路由框架,但是Arouter确实也存在一些问题,比如初始化的时候会去扫描私有的dex,还有缓存机制,作为一个有追求的程序员,我们今天就来借鉴阿里Arouter的思想自己动手实现一个路由框架AwesomeRouter。我将从以下几个方面展开:
- 使用和原理
- 框架的整体架构
- 注解处理器APT处理自定义注解
- 优雅的生成路由Group类—JavaPoet的使用
- Activity的跳转实现
- 接口化通信IProvider的实现
使用和原理
首先我们先来看看如何使用和其中的原理
如何使用
1 | //定义注解,并设置path的值 |
可以看到用法基本和ARouter一样,接着我们看实现原理
实现原理
路由框架会在项目的编译期间扫描所有添加@Router注解的Activity类,然后将Router注解中的path地址和Activity.class文件一一对应保存在map中,我们直接看apt生成的路由Group类:
1 | public class AwesomeRouter$$Group$$user implements IRouteGroup { |
RouteMeta就是路由表的封装类,里面包含了路由的类型(包括Activity,Provider), 路由组地址,路由地址,目标class文件。可以看到路由地址和路由表的映射map就是在AwesomeRouter$$Group$$user
里面保存着,我们在跳转的时候就可以通过传入的路由地址的组地址,通过Class.forName方式找到AwesomeRouter$$Group$$user
class文件,并通过newInstance的方式得到该类的对象,再调用loadPath方法就能拿到map,然后再根据路由地址得到目标class文件,这样就能完成一次Activity跳转了,这个其实就是路由框架的核心逻辑所在。
整体架构
话不多说,直接上图:
可以看到主要分为三个moudle:
- router-annotation : java的Library,自定义注解所在的库,比如定义Router注解
- router-compiler:java的 Library,注解处理器所在的库,用于处理自定义注解,生成路由组代码
- router-api:Android的 Library,路由框架核心逻辑代码,之所以是Android的Library,是因为里面有activity跳转的逻辑
APT处理自定义注解
APT 全称Annotation Process Tool ,也就是注解处理器,主要作用就是在编译期间处理注解,生成我们期望的代码,例如ButterKnife,EventBus都用到了APT技术,下面我们就用APT技术生成路由组代码。
自定义注解Router
新建一个java Library,在其中新建Router注解
1 | (ElementType.TYPE) |
注解处理器的实现
- 通过Google的AutoService来注册注解处理器
- 指定注解处理器支持的java版本,参数选项,以及支持的注解类型
- 在init方法中获取操作Element工具类,type(类信息)工具类,以及文件生成器
- process方法是在扫描到支持的注解类型时会调用的,所以我们将处理注解的逻辑写在该方法中。
下面我们看具体代码,注释写的很清楚:
1 | # 抽象出来的BaseProcessor类 |
其中parseElements方法主要是根据被Router注解的elements,去生成path地址对应的RouteMeta对象:
1 | private void parseElements(Set<? extends Element> elements) throws IOException { |
JavaPoet生成路由Group类
我们接着看根据注解生成Group类的过程,其实生成类文件的过程,我们也是可以采用字符串拼接的方法,但是字符串拼接的方法容易出错且不是那么的高大上(能装逼),所以我们采用更加牛逼的方式JavaPoet来生成类文件。首先我们来熟悉下一些常用的类,以及字符串的格式化规则,然后再来实践生成Group类文件。
常用类
ClassName:用来包装一个类,例如ClassName.get(Map.class)
TypeName:类型,例如返回值类型void可以使用TypeName.VOID
ParameterizedTypeName:参数化类型,TypeName的子类,例如返回值Map
可以这样写 1
2
3
4
5TypeName inputMapTypeOfRoot = ParameterizedTypeName.get(
ClassName.get(Map.class), // Map
ClassName.get(String.class), // Map<String,
ClassName.get(RouteMeta.class) // Map<String, RouteMeta>
);ParametarSpec:用来创建参数,例如(Map
map),可以这样写:
1 | ParameterSpec rootParamSpec = ParameterSpec.builder(inputMapTypeOfRoot, "map").build(); |
- MethodSpec:用来创建方法
- TypeSpec:用来创建类
- FieldSpec:用来创建一个成员变量
字符串格式化规则
- $T: 类,接口,例如:$T , MainActivity
- $S: 字符串,例如:$S , “Hello”
- $N: 变量,例如:user.$N , name
- $L: 字面量,例如: int value = $L , 10
具体实现
这里有一个小技巧,我们可以先把我们需要生成的类先自己手动敲出来,这样我们在写javaPoet代码的时候就不会那么抽象了。
1 | private void createGroupFile(TypeElement groupType) throws IOException { |
Activity的跳转实现
Activity跳转的代码是这样的
1 | public void jumpLogin(View view) { |
首先AwesomeRouter的build方法主要是返回了一个Postcard对象,Postcard对象主要是封装的是activity跳转的参数以及动画,接着Postcard的navigation方法其实就是调用的AwesomeRouter的navigation方法,接下来我们主要看navigation方法,注释写的很清楚,应该看得懂:
1 | Object navigation(@NonNull final Context context, final PostCard postCard) { |
navigation方法是整个路由跳转的核心所在,也是本路由框架和ARouter的差异所在,ARouter是通过初始化的时候异步扫描私有dex,去找到路由group class的,这种方式会存在机型适配问题,并且拖慢启动速度,虽然在后来的ARouter版本中,ARouter改为了使用gradle插件去扫描IRouterRoot实现类,实现路由表自动注册,但是在项目比较庞大,分组过多的时候还是会拖慢启动速度,然而本框架的路由加载是懒加载,是在跳转的时候才会去加载路由表。可能有人会说,因为加载的时候调用了Class.forName方法了,可能会影响页面的跳转,这个可以不需要担心,因为调用一次Class.forName方法的耗时几乎很少,而且我们再其中加入了LRU缓存机制,这样可以避免每次都要去调用forName方法。
接口化通信IProvider的实现
IProvider的实现其实和Activity的跳转实现大同小异。在注解处理器中封装RouteMeta对象的时候,会根据被Router注解的类是Activity的子类还是IProvider的子类去设置RouteMeta的类型,接着在navigation方法中会去判断RouteMeta的类型,如果是PROVIDER类型的话,就会通过newInstance的方式返回目标class的对象,这样我们就可以实现跨模块通信了。