TheRouter
是一个 Kotlin 编写,用于 Android 模块化开发的一整套解决方案框架。
Github 项目地址与使用文档详见 https://github.com/HuolalaTech/hll-wp-therouter-android。
TheRouter 核心功能具备如下能力:
路由是现如今移动端开发中必不可少的功能,尤其是企业级APP,可以用于将Intent
页面跳转的强依赖关系解耦,同时减少跨团队开发的互相依赖问题。
对于大型 APP 开发,基本都会选用模块化(或组件化)方式开发,对于模块间解耦要求更高。 TheRouter
是一整套完全面向模块化开发的解决方案,不仅能支持常规的模块依赖解耦、页面跳转,同时提供了模块化过程中常见问题的解决办法。例如:完美解决了模块化开发后由于组件内无法获取 Application
生命周期与业务流程,造成每次初始化与关联依赖调用都需要跨模块修改代码的问题。
Navigator:
Activity
和 Fragment
Path
与页面多对一关系或一对一关系,可用于解决多端path统一问题Path
支持正则表达式声明json
格式路由表导出json
路由表,降级任意页面为H5object
跨模块传递(无需序列化,且能保证对象类型)json
解析为对象)Activity
(Fragment
)ServiceProvider:
mock
调试FlowTaskExecutor:
Gradle Task
)ActionManager:
Observable
的问题注: FlowTaskExecutor
、ActionManager
后续会作为可选能力,提供可插拔
或单独使用
的选项(预计10月份提供)。
目前现有的路由基本上集中于两种能力的实现:页面跳转、跨模块调用,核心技术方案大体上如图:
Gradle Transform
,在编译期做一次聚合,以提升运行时准备路由表的效率。TheRouter
的页面跳转、跨模块调用也是如此,但是在设计上会有一些细节处理。
TheRouter
会在编译期根据注解生成 RouteMap__
开头的类,这些类中记录了当前模块的所有路由信息,也就是当前模块的路由表。
在最顶层的app
模块中,通过Gradle
插件,将所有aar、源码中的RouteMap__
开头的类统一集中到TheRouterServiceProvideInjecter
类中。
后续应用启动后,初始化路由时只需要执行TheRouterServiceProvideInjecter
类的方法,就能没有任何反射的加载到全部的路由表了。
加载以后的路由表会被保存到一个支持正则匹配的 Map
中,这也是TheRouter
允许多个path
对应同一个落地页的原因。每当发生页面跳转时,通过跳转时的path
,去Map
中获取到对应的落地页信息,再正常调用startActivity()
即可。
如果一个页面(支持 Activity、Fragment)允许被路由打开,则需要使用注解 @Route
声明路由项,每个页面允许声明多个路由项,也就是一对多的能力,极大降低多端路由统一时的业务影响面。
参数释义
intent
中,允许写在路由表中动态下发修改默认值,或通过路由跳转时代码传入。 @Route(path = "http://therouter.com/home", action = "action://scheme.com",
description = "第二个页面", params = {"hello", "world"})
public class HomeActivity extends AppCompatActivity {
}
传入的参数可以是 String
和8种基本数据类型、也可以是Bundle
、Serializable
、
Parcelable
对象,跟 Intent
传值规则一致。
同时也支持为本次跳转的 Intent
添加Flag/Uri/ClipData/identifier
等业务特殊参数。
// 传入参数可以通过注解 @Autowired 解析成任意类型,如果是对象建议传json
// context 参数如果不传或传 null,会自动使用 application 替换
TheRouter.build("http://therouter.com/home")
.withInt("key1", 12345678)
.withString("key2", "参数")
.withBoolean("key3", false)
.withSerializable("key4", object)
.withObject("object", any) // 这个方法可以传递任意对象,但是接收的地方对象类型需自行保证一致,否则会强转异常
.navigation(context);
// 如果传入 requestCode,默认使用startActivityForResult启动Activity
.navigation(context, 123);
// 如果要打开的是fragment,需要使用
.createFragment();
如果两条路由的path
、目标className
完全相同,则认为是同一条路由,不会考虑参数是否相同。
路由表生成规则:编译期按照如下顺序取并集。
覆盖规则:
根据如下顺序,如果相同,后者可以覆盖前者的路由表规则。
业务模块 aar
中的路由表app module
代码中的路由表assets/RouteMap.json
文件中声明的路由表。
如果编译期没有这个文件,会生成一份默认路由表放在这个目录内;如果有,会将路由表合并,因此,对于没办法修改代码的第三方SDK内部,如果希望通过路由打开,只需要手动在RouteMap.json
文件中声明,就能通过路由打开了。
TheRouter
的路由表是动态添加的,项目每次编译后,会在 apk 内生成一份当前 APP 的全量路由表,默认路径为:/assets/therouter/routeMap.json
。这个路由表也可以后续通过远程下发的方式使用,例如远端可以针对不同的APP版本,下发不同的路由表达到配置目的。这样如果将来线上某些页面发生Crash,可以通过将这个页面的落地页替换为H5的方式,临时解决这类问题。
有两种推荐的远程下发方式可供使用方选择:
assets/
目录中的配置文件上传到配置系统,下发给对应版本APP 。优点在于全自动不会出错。TheRouter
会自动用最新下发的路由项覆盖包内的路由项。优点在于精确,且流量资源占用小。注:一旦你设置了自定义的InitTask
,原框架内路由表初始化任务将不再执行,你需要自己处理找不到路由表时的兜底逻辑,一种建议的处理方式见如下代码。
// 此代码 必须 在 Application.super.onCreate() 之前调用
RouteMap.setInitTask(new RouterMapInitTask() {
/**
* 此方法执行在异步
*/
@Override
public void asyncInitRouteMap() {
// 此处为纯业务逻辑,每家公司远端配置方案可能都不一样
// 不建议每次都请求网络,否则请求网络的过程中,路由表是空的,可能造成APP无法跳转页面
// 最好是优先加载本地,然后开异步线程加载远端配置
String json = Connfig.doHttp("routeMap");
// 建议加一个判断,如果远端配置拉取失败,使用包内配置做兜底方案,否则可能造成路由表异常
if (!TextUtils.isEmpty(json)) {
List<RouteItem> list = new Gson().fromJson(json, new TypeToken<List<RouteItem>>() {
}.getType());
// 建议远端下发路由表差异部分,用远端包覆盖本地更合理
RouteMap.addRouteMap(list);
} else {
// 在异步执行TheRouter内部兜底路由表
initRouteMap()
}
}
});
TheRouter同时支持更多页面跳转能力,详情可参考项目文档【https://therouter.cn/docs/2022/08/28/01】:
对于模块化开发中跨模块的调用,我们推荐采用 SOA(面向服务架构) 的设计方式,服务调用方与使用方完全隔离,调用模块外的能力不需要关注能力的提供者是谁。
ServiceProvider
的核心设计思想也是这样的,目前服务间的调用协议采用接口的方式。当然,也可以兼容不通过接口下沉而是直接调用的情况。
具体到 Android 侧就是 AIDL 类似的设计,只是要比AIDL开发简单很多:
例如上面的图片:拉拉需要使用录音的服务,小货则向外提供一个录音的服务,由TheRouter
的ServiceProvider
负责撮合。
她无需关心,IRecordService
这个接口服务是谁提供的,他只需要知道自己需要使用这样的一个服务就行了。
注:如果没有提供服务的提供方,TheRouter.get()
可能返回null
TheRouter.get(IRecordService::class.java)?.doRecord()
服务提供方需要声明一个提供服务的方法,用@ServiceProvider
注解标记。
java
,必须是 public static
修饰kotlin
,建议写成 top level 的函数/**
* 方法名不限定,任意名字都行
* 返回值必须是服务接口名,如果是实现了服务的子类,需要加上returnType限定(例如下面代码)
* 方法必须加上 public static 修饰,否则编译期就会报错
*/
@ServiceProvider
public static IRecordService test() {
return new IRecordService() {
@Override
public void doRecord() {
String str = "执行录制逻辑";
}
};
}
// 也可以直接返回对象,然后标注这个方法的服名是什么
@ServiceProvider(returnType = IRecordService.class)
public static RecordServiceImpl test() {
// xxx
}
前面讲过,TheRouter
是完全面向模块化开发提供的一套解决方案。在模块化开发时,可能每个模块都有自己需要初始化的一些代码。以前的做法是把这些代码都在Application
里声明,但是这样可能随着业务变动每次都需要修改Application
所在模块。TheRouter
的单模块自动初始化能力就是为了解决这样的情况,可以只在当前模块声明初始化方法后,将会在业务场景时自动被调用。
每个希望被自动初始化的方法,必须使用public static
修饰,主要原因是这样子就能通过类名直接调用了。另外很多初始化代码都需要获取Context
对象,所以我们将Context
作为初始化方法的默认参数,会自动传入Application
。其他的所在类名、方法名都没有限制,反正只要加上了 @FlowTask
注解,在编译期都能通过 APT 获取到。
可以在当前模块中,任意类中声明一个任意方法名的方法,给方法添加上@FlowTask
的注解即可。
@FlowTask
注解参数说明:
moduleName_taskName
Gradle
Task,任务与任务之间可能会有依赖关系。如果当前任务需要依赖其他任务先初始化,则在这里声明依赖的任务名。可以同时依赖多个任务,用英文逗号分隔,空格可选,会被过滤:dependsOn = “mmkv, config, login”,默认为空,应用启动就被调用。注:dependsOn
只能保证task
是在依赖任务之后执行,由于内部逻辑都是在异步线程操作,取决于线程切换,并不一定是立刻执行。/**
* 将会在异步执行
*/
@FlowTask(taskName = "mmkv_init", dependsOn = TheRouterFlowTask.APP_ONCREATE, async = true)
public static void test2(Context context) {
System.out.println("异步=========Application onCreate后执行");
}
@FlowTask(taskName = "app1")
public static void test3(Context context) {
System.out.println("main线程=========应用启动就会执行");
}
/**
* 将会在主线程初始化
*/
@FlowTask(taskName = "test", dependsOn = "mmkv,app1")
public static void test3(Context context) {
System.out.println("main线程=========在app1和mmkv两个任务都执行以后才会被执行");
}
使用这个能力,在路由内部默认支持了两个生命周期类任务,可在使用时直接引用
同时,使用TheRouter
的自动初始化依赖,也无需担心循环依赖造成的问题,框架会在编译期构建有向无环图,监测循环依赖情况,如果发现会在编译期直接报错,并且还会将发生循环引用的任务显示出来,用于排错。
每个加了 @FlowTask
注解的方法,都会在编译期被解析,生成一个对应的 Task
对象,这个对象包含了初始化方法的相关信息,比如:是否异步执行、任务名、是否依赖其他任务先执行。
当所有aar都编译完成,生成好全部的 Task
以后,会在主 app 中通过Gradle
插件进行聚合,在这时会将所有的 Task
做一次检查,通过构建有向无环图
来防止 Task
发生循环引用的情况。
每次应用启动后,会在路由初始化时,将有向图中的全部Task
,按照依赖关系按顺序加载。
Action
本质是一个全局的系统回调,主要用于预埋的一系列操作,例如:弹窗、上传日志、清理缓存。
与 Android 系统自带的广播通知类似,你可以在任何地方声明动作与处理方式。并且所有Action
都是可以被跟踪的,只要你愿意,可以在日志中将所有的动作调用栈输出,以方便调试使用,这样在一定程度上可以解决观察者模式带来的通病:无法追踪Observable
的问题。
声明一个 Action:
// action建议遵循一定的格式
const val ACTION = "therouter://action/xxx"
@FlowTask(taskName="action_demo")
fun init(context: Context) =
TheRouter.addActionInterceptor(ACTION, object: ActionInterceptor() {
override fun handle(context: Context, args: Bundle): Boolean {
// do something
return false
}
})
执行一个 Action:
// action建议遵循一定的格式
const val ACTION = "therouter://action/xxx"
// 如果执行了一个没有被声明的Action,则不会有任何动作
TheRouter.build(ACTION).action()
每个Action
允许关联多个 ActionInterceptor
进行处理,多个ActionInterceptor
之间可以自定义拦截器优先级,同时允许终止接下来的低优先级拦截器的执行。
最典型应用场景:首页可能会有多个弹窗,不同业务之间的弹窗是有优先级之分的,为了体验优化我们肯定不会在首页一次把所有弹窗全部弹出,可以通过ActionInterceptor
为每个弹窗声明好优先级关系,假设需求是首页只能弹出3个弹窗,那么第三个弹窗处理完毕即可关闭当前事件,接下来的拦截器将不会被响应。
abstract class ActionInterceptor {
abstract fun handle(context: Context, args: Bundle): Boolean
fun onFinish() {}
/**
* 数字越大,优先级越高
*/
open val priority: Int
get() = 5
}
如果仅客户端使用,常用的场景可能是:当用户执行某些操作(打开某个页面、H5点击某个按钮、动态页面配置的点击事件)时,将会自动触发,执行预埋的 Action 逻辑。
如果与服务端链路打通,这个能力其实是需要整个公司的配合,比如有一套类似智慧大脑的方案,可以基于客户端过去的一些埋点数据,智能推断出用户下一步要做的事情,然后通过长连接直接向客户端下发指令做某些事情。那么通过客户端预埋的页面跳转、弹窗、清缓存、退出登录等等操作,就可以通过服务端指令进行操作,则就是一套完整的动态化方案。
在模块化开发过程中,如果没有采用分仓,或采用了分仓但依然使用 git-submodule
的方式开发,应该都会遇到一个问题。如果集成包采用源码编译,构建时间实在太久,大大降低开发调试效率;如果采用aar依赖编译,对于底层模块修改了代码,每次都要重新构建aar,在上层模块修改版本号以后,才能继续整包构建编译,也极大影响开发效率。
TheRouter
中提供了一个 Gradle
脚本,只需要在开发本地的local.properties
文件中声明要参与编译的module
,其他未声明的默认使用aar编译,这样就能灵活切换源码与aar,并且不会影响其他人,如下节选代码可供参考使用:
/**
* 如果工程中有源码,则依赖源码,否则依赖aar
*/
def moduleApi(String compileStr, Closure configureClosure) {
String[] temp = compileStr.split(":")
String group = temp[0]
String artifactid = temp[1]
String version = temp[2]
Set<String> includeModule = new HashSet<>()
rootProject.getAllprojects().each {
if (it != rootProject) includeModule.add(it.name)
}
if (includeModule.contains(artifactid)) {
println(project.name + "源码依赖:===project(\":$artifactid\")")
projects.project.dependencies.add("api", project(':' + artifactid), configureClosure)
// projects.project.configurations { compile.exclude group: group, module: artifactid }
} else {
println(project.name + "依赖:=======$group:$artifactid:$version")
projects.project.dependencies.add("api", "$group:$artifactid:$version", configureClosure)
}
}
在实际使用时,可以完全使用moduleApi
替换掉原有的api
。当然, implementation
也可以有一个对应的moduleImplementation
,这样只需要注释或解注释setting.gradle
文件内的include
语句就可以达到切换源码、aar
的目的了。
TheRouter
提供了图形化界面的迁移工具,可以一键从其他路由迁移到TheRouter
,目前仅支持ARouter
,其他路由框架迁移也在开发中(GitHub下载,70M左右,请耐心等待):
如果项目中使用了ARouter的IProvider.init()方法,可能需要手动处理初始化逻辑。
如下图:
功能 | TheRouter | ARouter | WMRouter |
---|---|---|---|
Fragment路由 | ✔️ | ✔️ | ✔️ |
支持依赖注入 | ✔️ | ✔️ | ✔️ |
加载路由表 | 无运行时扫描 无反射 |
运行时扫描dex(新版本改为反射) 反射实例类 性能损耗大 |
运行时读文件 反射实例类 性能损耗中 |
注解正则表达式 | ✔️ | ✖️ | ✔️ |
Activity指定拦截器 | ✔️(四大拦截器可根据业务定制) | ✖️ | ✔️ |
导出路由文档 | ✔️(路由文档支持添加注释描述) | ✔️ | ✖️ |
动态注册路由信息 | ✔️ | ✖️ | ✖️ |
APT支持增量编译 | ✔️ | ✔️(开启文档生成则无法增量编译) | ✖️ |
plugin支持增量编译 | ✔️ | ✖️ | ✖️ |
支持KSP编译 | ✔️ | ✖️ | ✖️ |
支持 AGP8 编译 | ✔️ | Gradle7.3以后无法使用 | Gradle7.3以后无法使用 |
多 Path 对应同一页面(低成本实现双端path统一) | ✔️ | ✖️ | ✖️ |
远端路由表下发 | ✔️ | ✖️ | ✖️ |
支持单模块独立初始化 | ✔️ | ✖️ | ✖️ |
支持使用路由打开第三方SDK页面 | ✔️ | ✖️ | ✖️ |
对热修复支持(例如tinker) | ✔️(未改变的代码多次构建无变动) | ✖️(多次构建apt产物会发生变化,生成无意义补丁) | ✖️(多次构建apt产物会发生变化,生成无意义补丁) |
TheRouter
并不仅仅是一个小巧灵活的路由库,而是一整套完整的 Android
模块化解决方案,能够解决几乎全部的模块化过程中会遇到的问题。
对于现有的路由框架,我们也在最大限度支持平滑迁移,目前已完成ARouter
的一键迁移工具,其他框架的迁移仍在开发中。你也可以在Github
issue
中提出需求,我们评估后会尽快支持,也欢迎任何人提供 Pull Requests
。
更多问题请加群沟通: