没错,货拉拉开源的路由库 —— TheRouter 是我写的
大约在17年底到18年初的时候,我经常会讲一些当时做模块化开发的心得和踩坑历程。比如这几篇都是那时候写的:《Android 模块化平台设计》、《优雅移除模块间耦合》、《企业级 Android 模块化平台设计建议》。
但后来我慢慢不讲这些了,因为我发现做模块化,虽然我们能总结出来一套较为通用的解决方案,但很难通过几次短短的技术分享就跟别人讲清楚。并且很容易让人产生误解:我们是小公司,不需要做模块化。再加上因为当时是基于公司已有的基础建设,和制度的一些限制,并不能对外开源一套较为完善的模块化方案,开源一套完整的模块化方案这个种子就一直埋下了。
说回 TheRouter
这个名字,其实熟悉我的都知道,之前写过一个开源类 MVP 框架,叫TheMVP
,基本上成为了一种将Activity
看做 P 层架构的行业规范。后来被支付宝使用了,也在 设置-关于-版权信息 里面能查到,直到前几天我去反编译的时候,都还看到BaseActivity
用的是我的代码。
The 代表了一种唯一性,表示有这个就够了。
TheRouter
也是一样,我相信用过TheRouter
以后你才会真正意识到,现在的企业级Android
模块化应该怎么玩。
为什么要使用 TheRouter
路由是现如今 Android 开发中必不可少的功能,尤其是企业级APP,可以用于将Intent
页面跳转的强依赖关系解耦,同时减少跨团队开发的互相依赖问题。
对于大型 APP 开发,基本都会选用模块化(或组件化)方式开发,对于模块间解耦要求更高。 TheRouter
是一整套完全面向模块化开发的解决方案,不仅能支持常规的模块依赖解耦、页面跳转,同时提供了模块化过程中常见问题的解决办法。例如:完美解决了模块化开发后组件内无法获取 Application
生命周期与业务流程,造成每次初始化与关联依赖调用都需要跨模块修改代码的问题。
不过为什么要用,说到底,还是用ARouter
用的太头疼了。
- 一个是死板,所有路由都是写死的,但凡想灵活一点,把线上
Crash
的页面降级成H5临时解决,都得改一大堆代码还很多限制性。 - 另一个就是效率,不管是编译时长还是启动耗时,这俩问题都一直不解决。某个厂的开源项目都这样,作者们该晋升的晋升,该转岗的转岗,剩下的躺平不管,毕竟修修补补这事不占KPI,没法述职啊。没办法,自己来吧,谁让我们还有启动耗时指标的。
- 再就是遇到的一个坑,在用
tinker
下发补丁的时候,发现同一个分支打出来的包,ARouter
和Butterknife
的产物包代码都不一样,直接增大了补丁体积。 - 当然,还有很多差异,看这个表格吧。
功能 | TheRouter | ARouter | WMRouter |
---|---|---|---|
Fragment路由 | ✔️ | ✔️ | ✔️ |
支持依赖注入 | ✔️ | ✔️ | ✔️ |
加载路由表 | 无运行时扫描 无反射 |
运行时扫描dex 反射实例类 性能损耗大 |
运行时读文件 反射实例类 性能损耗中 |
注解正则表达式 | ✔️ | ✖️ | ✔️ |
Activity指定拦截器 | ✔️(四大拦截器可根据业务定制) | ✖️ | ✔️ |
导出路由文档 | ✔️(路由文档支持添加注释描述) | ✔️ | ✖️ |
动态注册路由信息 | ✔️ | ✔️ | ✖️ |
APT支持增量编译 | ✔️ | ✔️(开启文档生成则无法增量编译) | ✖️ |
plugin支持增量编译 | ✔️ | ✖️ | ✖️ |
多 Path 对应同一页面(低成本实现双端path统一) | ✔️ | ✖️ | ✖️ |
远端路由表下发 | ✔️ | ✖️ | ✖️ |
支持单模块独立初始化 | ✔️ | ✖️ | ✖️ |
支持使用路由打开第三方库页面 | ✔️ | ✖️ | ✖️ |
支持使用路由打开第三方库页面 | ✔️ | ✖️ | ✖️ |
对热修复支持(例如tinker) | ✔️(未改变的代码多次构建无变动) | ✖️(多次构建apt产物会发生变化,生成无意义补丁) | ✖️(多次构建apt产物会发生变化,生成无意义补丁) |
动态页面路由能力
其实单纯的页面路由,没什么好说的,基本上所有人都是这么做的。APT编译期生成一个描述类,gradle
插件聚合所有的描述类,应用启动的时候再加载描述类,就这么一个流程。TheRouter
文档里面写的非常详细了,这里主要讲讲路由在现代APP中要怎么用。
TheRouter
从设计阶段,考虑的就是APP动态化能力。所以既能支持第三方SDK
的路由跳转,也能支持插件化的开发形态,又能处理H5Hybrid
、Flutter
混合的这种项目,反正路由表都是可以随便添加。
那,真正用处最多的是通过动态下发,提升客户端容灾能力。
比如在线上,某些页面或者核心下单交易流程因为客户端开发疏忽,造成无法使用的情况,可以通过路由将对应页面降级为H5或者小程序,保证线上APP
依然是可用的状态。
有两种推荐的远程下发方式可供使用方选择:
- 将打包系统与配置系统打通,每次新版本APP打包后自动将
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
中下发路由表的格式:
[
{
"path": "https://kymjs.com/therouter/test",
"className": "com.therouter.app.autoinit.TestActivity",
"action": "",
"description": "",
"params": {
"key":"value"
}
},
......
]
单模块自动初始化能力
其实,做模块化最麻烦的两个点,第一个是依赖解耦,第二个应该就是独立模块的初始化问题了。再加上现在对于隐私合规问题越查越严,各种权限都必须在隐私弹窗授权以后才能使用,使得模块独立更难,动不动就得改到Application
壳工程。
TheRouter 的单模块自动初始化能力就是为了解决这样的情况,可以只在当前模块声明初始化方法后,将会在业务场景时自动被调用。
类似于 Gradle
的 Task,你也可以声明自己的初始化 Task,然后声明的时候提供好需要依赖的其他 Task,这样只要依赖的那个 Task 没有初始化,你的任务就不会被初始化。直到依赖的那个 Task 初始化完成,你的任务才会被自动调用。
/**
* 将会在异步执行
*/
@FlowTask(taskName = "mmkv_init", dependsOn = TheRouterFlowTask.APP_ONCREATE, async = true)
public static void test2(Context context) {
System.out.println("异步=========Application onCreate后执行");
}
@FlowTask 注解参数说明:
- taskName:当前初始化任务的任务名,必须全局唯一,建议格式为:moduleName_taskName
- dependsOn:参考Gradle Task,任务与任务之间可能会有依赖关系。如果当前任务需要依赖其他任务先初始化,则在这里声明依赖的任务名。可以同时依赖多个任务,用英文逗号分隔,空格可选,会被过滤:dependsOn = "mmkv, config, login",默认为空,应用启动就被调用
- async:是否要在异步执行此任务,默认false。
内置初始化节点
使用这个能力,在路由内部默认支持了两个生命周期类任务,可在使用时直接引用
- TheRouterFlowTask.APP_ONCREATE:当Application的onCreate()执行后初始化
- TheRouterFlowTask.APP_ONSPLASH:当应用的首个Activity.onCreate()执行后初始化
同时,使用TheRouter
的自动初始化依赖,也无需担心循环依赖造成的问题,框架会在编译期构建有向无环图,监测循环依赖情况,如果发现会在编译期直接报错,并且还会将发生循环引用的任务显示出来,用于排错。
动态化能力
还有一个,动态化能力。这个能力其实是需要整个项目公司的配合,比如有一套类似智慧大脑的方案,可以基于客户端过去的一些埋点数据,智能推断出用户下一步要做的事情,然后通过长连接直接向客户端下发指令做某些事情。
不过抛开后端的能力,单独靠客户端也是可以使用的。
Action
本质是一个全局的系统回调,主要用于预埋的一系列操作,例如:弹窗、上传日志、清理缓存。
与 Android 系统自带的广播通知类似,你可以在任何地方声明动作与处理方式。并且所有Action
都是可以被跟踪的,只要你愿意,可以在日志中将所有的动作调用栈输出,以方便调试使用。
当用户执行某些操作(打开某个页面、H5点击某个按钮、动态页面配置的点击事件)时,将会自动触发,执行预埋的 Action 逻辑。
但还是强烈推荐,将端上数据与服务端链路打通,根据客户端不同的用户行为,交由后端分析,进而推测出用户下一步动作,提前执行下发逻辑交给客户端执行,则是一套完整的动态化方案。
模块化支持,Gradle脚本一键切换源码引用
在模块化开发过程中,如果没有采用分仓,或采用了分仓但依然使用 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
的目的了。
具体的使用方法,可以看我这篇文章里面讲的【源码与aar互斥】的实现:https://xiaozhuanlan.com/topic/2735849061
什么年代了,还在用 ARouter?
支持从 ARouter 一键迁移!
没错,什么年代了,还在用ARouter?
对于这种已有的存量路由框架,当然也是提供了一键迁移的图形化工具。
为了写这个工具我也是废了好大的劲,特意学了一遍JavaFX怎么用,然后打了一个Mac产物、一个Windows产物。
不禁感叹:Java的跨平台才是真正的跨平台啊。
注:传到了GitHub,可能有点慢,耐心等待