说在前面
很想写一篇关于路由的文章,但是由于一些原因一直没有去完成,直接使用了Arouter作为路由方案,进行了我的组件化改造之路。最近经过一些鞭打,我发现我并不能完整的表达出来,所以我还是决定再去了解了解它的原理,博文内容仅代表个人意见,并且由于本人知识有限,如有错误,敬请指正!谢谢!
Arouter
在进行分析之前,我们还是对Arouter进行一个介绍:
Arouter 是阿里团队在2016年开源的一个路由方案,可以帮助我们对项目进行组件化改造算是一个比较成熟的路由方案。它的通信是采用的路由+接口下沉的方式实现的。
大概原理
利用注解、和注解处理工具(APT)
在编译期间通过APT动态生成含有类路由信息的映射文件,在运行期间通过固定的包名对映射文件进行加载(并且在这里Arouter进行了优化,也就是分组管理、按需加载;实际上是先加载了root文件,然后按需加载group文件),将所有路由文件都存放到本地仓库中,当用户需要时可以通过路由地址在仓库中找到需要的类信息。然后实现项目中的跳转与服务获取。
注解类型
- @Route : 用于描述路径,类被该注解标识才能被自动生成路由信息加入到路由表中。
- @AutoWired : 用于标识参数,在该页面被路由打开时,自动赋值传递的参数值
- @Interceptor : 拦截器
其它东西咱在后面出现了再介绍,免得翻到前面来看!
生成路由文件的过程
说到路由表的生成那么离不开一个东西 RouteProcessor 它是一个注解处理器,负责生成路由文件。
它会对使用了 @Route 注解的类进行一系列的操作,然后调用JavaPoet的API动态生成一些Java类(包含被注解类的信息)。
具体的操作是在RouteProcessor.process()方法中进行的。现在我们来分析这个方法:
- 获取路由元素
它是怎么获取的呢?咱浅说一下(深了咱也不会)
就是说process()方法会接收到两个参数
annotations 和 roundEnv 两个参数;annotation是我们当前处理的一个注解,roundEnv(JavaRoundEnvironment)是当前的运行时环境。
然后roundEnv有个方法叫 getElementsAnnotatedWith()。我们就是利用这个方法去获取的路由元素。
- 创建路由元信息
RouteMeta就是我们所说的路由元信息,也就是在之后我们通过路径从本地仓库中获取的就是这个里面的信息。
我们根据上面获取到的路由元素填充创建一个RouteMeta,这个RouteMeta实际上就与我们通过使用 @Route 注解 标注的一个Activity/Fragment/Provider相对应了。
咱来看一下这个内部包含了哪些东西(图来自网络)
然后我们简单的介绍一下上面相关的字段的含义。
路径类型 :其实就是说明该RouteMeta是与Activity/Fragment/Provider中的哪一种绑定的。
路线原始类型 :被@Route 标识了类的Class信息。
终点 :类的Class,也就是说我们要跳转的Class
路径/路线组 :就是我们在@Route(path =" 1**/2**")中设置的路径,其中如果我们没有指定group 那么1** 默认为group也就是路线组。(这里就是我们前面说的Arouter对路由的一个优化 分组管理 的基础)。
优先级 :拦截器才会用到。
标志 :是否生成路由文档,默认不生成。
参数类型 :就是我们跳转时携带的参数的类型;在内部有一个枚举值,不同的值对应不同的类型。
路线名称 :略
注入配置 :当元素类型为 Activity 时,RouteProcessor 会遍历获取 Activity 的子元素,也就是 Activity 的成员变量,把它们放入注入配置 RouteMeta 的 injectConfig 中,当我们配置了要生成路由文档时,RouteProcessor 就会把 injectConfig 写入到文档中。
- 将路由信息进行分组
这个就是我们前面说的分组管理的一个真正操作的地方了。它的就是按照我们指定的group的值进行分组的。并且在后面加载的时候也是一个group的单位进行加载的,如果组内没有页面被调用那么整个组都不会被加载,但是如果有一个被调用,那么整个组都会被加载。好了回到正题->前面说了这里的分组是按照前前面提供的group的值进行的,其实呢就是根据值的不同分类将值相同的RouteMeta放入了同一个Set中。这里的实现原理是维护了一个hashmap>。
string的值就是group
private Map<String, Set<RouteMeta>> groupMap = new HashMap<>();
- 生成路由文件
这里就是JavaPoet的主战场了,它会根据group的不同生成不同的group文件,每个group文件内维护着一个类似map的结构用来存储我们之前分组好的RouteMeta。当然这里不止会生成group路由文件,还有root路由文件、Provider路由文件。就是这些文件构建了我们的路由表。后期物流中心 LogisticsCenter就是利用这些文件来填充的本地仓库 Warehouse。
这里我们分别给出group和provider的路由文件生成的一个伪代码:
Group :
public class ARouter$$Group$$a implements IRouteGroup {
@Override
public void loadInto(Map<String, RouteMeta> atlas) {
atlas.put("/a/b", RouteMeta.build(RouteType.ACTIVITY, SecondActivity.class, "/a/b", "a", null, -1, -2147483648));
}
}
Provider : 有两种情况
第一种情况是实现Iprovider接口的类,它这个生成路由文件的方式就和其它的大差不差,来看看
public class ARouter$$Providers$$module1 implements IProviderGroup {
@Override
public void loadInto(Map<String, RouteMeta> providers) {
providers.put("com.example.module1.HelloService", RouteMeta.build(RouteType.PROVIDER, HelloService.class, "/module1/hello", "module1", null, -1, -2147483648));
}
}
也就是说让实现类的路径作为了Key。
但是Arouter还支持一种 通过接口发现路由的方式,就是一个接口继承了Iprovider,然后一个实现类实现了这个接口,我们也来看看
public class ARouter$$Providers$$module1 implements IProviderGroup {
@Override
public void loadInto(Map<String, RouteMeta> providers) {
providers.put("com.example.module1.IUserService", RouteMeta.build(RouteType.PROVIDER, UserServiceImpl1.class, "/u/1", "u", null, -1, -2147483648));
}
}
这时候Key就不是实现类的路径了,而是接口的路径!
那么在实际使用过程中,我们就只需要给出接口就能找到服务,实现解耦。
整个路由文件的生成我们就讲到这里了,可能不是很详细,也需要大家自己去翻看资源和动手才能真正理解吧
给大家留几个问题吧,也是通过上面的学习能够感悟到的,答案在文章末尾会给出
1.在同一个moudel中有两个相同path的activity的情况可以存在吗,如果可以那么当利用path的时候获取的是哪一个activity呢?
2.如果在不同的moudel中允许存在吗?
3.那接口呢?就是说如果通过接口的方式发现服务,并且这个接口有不同的实现,会怎么样?
路由的加载
路由的加载发生在运行时,为了提高性能,Arouter在这也有一些优化方案。比如说当我们不使用插件的情况下,我们是按需进行加载的,其实就是在初始化加载的时候我们只是加载了各个模块的root文件(root文件管理着group文件),当我们需要某一个页面但是不存在时,我们才会去进行加载;还有一种方案就是利用插件,这个的具体原理应该是字节码插桩(这个我不懂,咱有机会再补充吧)
然后我们再稍微细一点讲讲这个过程;我们把不利用插件的方式叫做从dex文件中读取收集路由。
- 从dex文件中读取路由
我们前面说过 LogisticsCenter 会根据我们生成路由文件填充本地仓库,其实就是在这个时候。
具体就是在LogisticsCenter 确定没有使用插件后,它会使用ClassUtils去读dex文件,找到base apk的路径,然后从apk中读取类信息,会根据特定的包命,找到我们生成的文件的类名,然后将这些类名加入到一个列表中,返回给LogisticsCenter 。(这里特定的包名就是我们之前利用JavaPoet生成的类文件的存储包名)然后LogisticsCenter会把这些类名保存在本地(这里利用的是SheradPrefrence)以至于下次加载的时候如果app的版本未改变,则从本地缓存中加载类名,否则还是利用ClassUtils去读取,最后会根据它们的后缀判断该类是 IRouteRoot 、IInterceptorGroup 还是 IProviderGroup ,然后根据不同的类把类文件的内容加载到索引中。
前面问题答案
- 在同一个模块中可以存在两个path相同的activity 但是在使用该path调用的时候,我们会使用到字母表排在前面的哪个类(activity)只是因为我们默认都是按照字母表排序进行流程,当我们在分组哪的时候,你会发现我们用的是set的数据结构,所以排在后面的相同的path根本就不能分到组里
- 不同模块是不能的,APT是分模块进行编译的,所以在根据我们给的group会在两个模块都生成相同名称的group文件,因为我们在合并到dex文件时出现两个相同的group文件系统不知道该怎么办,从而产生冲突。
- 接口是可以的,排在字母表后面的会覆盖掉排在字符表前面的,这是因为我们在生成路由文件时,Provider是利用的map,当Key相同时,后面来的会覆盖掉前面的。所以当我们利用接口发现服务的方式注入时,我们获取的是字母表排在后面的哪个实现类的实例。
参考
参考1
参考2
参考3