Android-Router路由框架浅析

Android-Router路由框架浅析

前言:最近在思考项目 Module 解耦问题的时候,粗略了解了一下路由框架,其中最著名的又属阿里开源的 ARouter 了。但在实操的时候发现路由框架并没有我一开始想象的「能完全实现 Module 间解耦」那么美好,有着其本身的局限性,所以在本文对 ARouter 的基本原理和局限性做一个很浅显的个人总结。


1. 什么是ARouter

ARouter 主要用于针对 Activity 跳转之间的解耦,本质上它提供了一种通过 String 类型的「路径 Path」对应到 Activity 的路由表。其核心是通过 APT 在编译时自动检索添加了 @Route(path="/XXX/XXX") 注解的 Activity,并以对应的 Path 为 Key 自动生成了一个 Map,然后在运行时根据 Map 存储的路由信息跳转。


2. ARouter的基本原理

ARouter 要求一个 Activity 的路径 Path 必须包括至少两级,例如:/main/sub,将 main 称为主路径,表示某一组 Activity 路由信息统一的主路径,sub 称为子路径,表示具体到某一个 Activity 的精确路由。一个主路径可以包括多个子路径,例如:/main/sub, /main/sub2

当项目很庞大或 Activity 的数量很多时,各个需要路由的 Module 下就会产生很大的 Map,因此 ARouter 做了一个优化:分段懒加载,也即运行时不会立即将所有路由信息都加载进内存,而是在发起一个路由请求时,先读取缓存,如果缓存没有,再对目标的主路径下的路由信息做懒加载。

2.1 编译时处理

ARouter 在编译时会通过 APT 生成两个表:

(1)对每个添加了 ARouter 依赖的 Module,检索 Module 内所有添加了 @Route 注解的 Activity,并生成每个具体的 Path 对应 Activity 的表 atlas

  • 假设 module_splash 下有一个 ADSActivity 添加了路由,则编译时会生成一个类似以下结构的表:
1
2
3
4
5
6
public class EaseRouter_Group_splash implements IRouteRoot {
@Override
public void loadInto(Map<String, RouteMeta> atlas) {
atlas.put("/splash/ads", RouteMeta.build(RouteMeta.Type.ACTIVITY, ADSActivity.class, "/splash/ads", "splash"));
}
}
  • 假设 module_user 下 LoginActivity 和 RegisterActivity 都添加了路由,则编译时同样会生成一个类似以下结构的表:
1
2
3
4
5
6
7
public class EaseRouter_Group_user implements IRouteRoot {
@Override
public void loadInto(Map<String, RouteMeta> atlas) {
atlas.put("/user/login", RouteMeta.build(RouteMeta.Type.ACTIVITY, LoginActivity.class, "/user/login", "user"));
atlas.put("/user/register", RouteMeta.build(RouteMeta.Type.ACTIVITY, RegisterActivity.class, "/user/register", "user"));
}
}

(2)对所有 Router,生成一个主路径对应每个实际路由表 atlas 的表:

1
2
3
4
5
6
7
public class EaseRouter_Root_app implements IRouteRoot {
@Override
public void loadInto(Map<String, Class<? extends IRouteGroup>> routes) {
routes.put("slpash", EaseRouter_Group_splash.class);
routes.put("user", EaseRouter_Group_user.class);
}
}

实际上,所有和路由相关的表都,都是通过一个实现了 IRouteRoot 接口的类存储的。

2.2 运行时加载

ARouter 会在运行时收到路由请求后再做懒加载:

(1)初始化 ARouter.init(); 时,ARouter 会开启一个子线程扫描 apk 中的所有 dex 文件,遍历当前包名下所有实现了 IRouteRoot 接口的类,并存进一个 className 集合中。

(2)通过 ARouter.getInstance().build("/XXX/XXX").navigation(); 请求路由到指定 Path 对应的 Activity。

(3)尝试从缓存中读取对应的 Activity,如果命中缓存的路由信息,则直接定位并启动目标 Activity。

(4)如果未命中缓存,说明该路由对应所在的整个路由表都没有加载。假设目标路由为:/user/register,则 EaseRouter_Group_user 加载 atlas 时,会同时把当前主路径,也即 user 下的所有路由信息都加载,所以如果找不到 /user/register,就说明整个 user 都没有加载,则根据目标路由的主路径 user 加载所有 user 下的路由信息。

(5)将所有主路径为 user 的路由信息加载后,就能通过 routes.get("user"); 获取到 EaseRouter_Group_user,然后再通过 atlas.get("/user/register") 获取到 RegisterActivity


3. ARouter的局限性

上文很简单的解释了一下 ARouter 的原理,作为一个路由框架,ARouter 最大的用途就是把多个 Module 间的 Activity 跳转解耦,发起跳转时不再需要关注实际的 Activity,只需要一个 String 类型的路由即可,从代码编写上确实看起来是解耦了,但实际上 ARouter 并没有解决依赖的问题。

对于 Android 来说,Gradle 在构建 app 时是有优化的,假如一个 AModule 没有被别的 Module 依赖,或者虽然 AModule 被 BModule 依赖了,但 BMdoule 又没有被依赖,类似于 JVM 中的「引用链是断开的」,那 Gradle 在编译时就会忽略掉这些 Module,以减小最后生成的 APK 包大小以及简化资源。

Gradle 这个优化本是出于减小包大小考虑的,本质上是好的,但对于想要实现 Module 间解耦来说就是一个阻碍了,因为那些我们希望解耦而去掉依赖的 Module,根本没有被打包进最后的 APK 中,那无论是采用路由、还是别的方式都无法访问。例如:

  • app 主 Module 下有个 MainActivity

  • module_user 下有 LoginActivityRegisterActivity

  • app 主 Module 和 module_user 都添加了 ARouter 依赖,并且 Activity 都添加了 @Route 路由信息

  • app 主 Module 并不依赖 module_user,两个 Module 都是独立的

  • 编译打包,Build 日志中,ARouter 输出确实找到了 LoginActivity 和 RegisterActivity 的路由:

    1
    2
    Note: ARouter::Compiler >>> Found activity route: priv.luis.user.LoginActivity <<<
    Note: ARouter::Compiler >>> Found activity route: priv.luis.user.RegisterActivity <<<
  • 但是打包完再反解后发现,module_user 没有被打进 APK 内,MainActivity 请求路由到 LoginActivity 时 ARouter 也报错:

    1
    2
    ARouter::There is no route match the path [/user/login]
    ARouter::There is no route match the path [/user/register]

也就是说,想要使用路由框架,首先也要确保 Module 能正确被打包进去,那方案只有两种:

  1. app 主 Module 添加所有业务子 Module 的依赖。
  2. 新建一个中介 Module,中介 Module 添加所有业务子 Module 的依赖,然后 app 主 Module 再添加这个中介 Module 的依赖,除了中介 Module 外,其他 Module 都互不依赖。这样 app 主 Module 并没有显式依赖各个业务子 Module,在开发时可以避免逻辑耦合的情况,而且实际上所有业务子 Module 是间接被 app 主 Module 依赖了,所以也能确保被打包进 APK 中,这也是常用的做法。

所以实际上路由框架并没有从根源上解决依赖的问题,从这个角度来看,ARouter 最大的好处或许其实在于其使用 APT 自动生成了路由信息的录入和查找相关代码,并且对查找路由做了分段懒加载等内存优化,在业务开发上可以节省一些编码时间,但 ARouter 最基本的使用也需要开发人员对相关 Gradle 配置、路由配置等稍有熟悉后才可上手(当然这个过程其实已经有傻瓜式文档了,使用门槛很低),对于整个 App 的路由信息,也需要专门管理,以避免路由信息混乱、重复、不规范等问题,当然这些确实都能通过良好的管理方式优化。但实际上我个人对使用 String 来管理路由的做法反倒是略有排斥的,至于是否真的使得代码变得更「内聚」、「解耦」、以及「高级」,和每个人的开发习惯有关,就见仁见智了。

附录[参考指导]