보충:
오픈 소스 창고 주소: github.com/HuolalaTech…
맞아요, 라라의 오픈소스 라우팅 라이브러리 - TheRouter는 제가 작성했습니다
2017년 말부터 2018년 초까지 저는 종종 모듈 개발 경험과 구덩이에 들어가는 과정에 대해 이야기하곤 했습니다. 예를 들어, " Android 모듈식 플랫폼 설계 ", " 모듈 간의 커플링을 우아하게 제거 ", " 엔터프라이즈 Android 모듈식 플랫폼 설계 제안 " 과 같은 기사가 당시 작성되었습니다 .
그러나 모듈화에서는 보다 일반적인 솔루션을 요약할 수 있지만 몇 가지 짧은 기술 공유를 통해 다른 사람에게 명확하게 설명하기가 어렵다는 것을 알았기 때문에 천천히 이에 대해 이야기하는 것을 중단했습니다. 그리고 오해하기 쉽습니다. 우리는 작은 회사이고 모듈화를 할 필요가 없습니다. 또한, 회사의 기존 인프라와 시스템의 몇 가지 제한 사항을 기반으로 했기 때문에 상대적으로 완전한 모듈식 솔루션을 외부 세계에 공개하는 것이 불가능했고, 완전한 모듈식 솔루션 오픈 소스 의 씨앗 이 심어졌습니다.
라우터로 돌아가기
사실 이 이름은 저를 잘 아시는 분들은 아실 겁니다.. 예전에 오픈 소스 MVP 프레임워크를 작성한 적이 TheMVP
있는데, 기본적으로 Activity
P-layer 아키텍처로 간주되는 업계 표준이 되었습니다. BaseActivity
나중에 알리페이에서 사용하던데 설정-저작권 정보에서 확인할 수 있는데, 며칠 전에 디컴파일 하러 가보기 전까지 는 제 코드가 사용된 걸 봤습니다 .
이것으로 충분함을 나타내는 고유성을 나타냅니다.
TheRouter
동일하게 사용하시면 현재의 엔터프라이즈급 모듈화가 TheRouter
어떻게 작동해야 하는지 실감하시리라 믿습니다 .Android
라우터를 사용하는 이유
라우팅은 오늘날 Android 개발, 특히 엔터프라이즈 수준 앱에서 없어서는 안될 기능으로, Intent
페이지 점프의 강력한 종속성을 분리하고 팀 간 개발의 상호 종속성 문제를 줄이는 데 사용할 수 있습니다.
대규모 APP 개발의 경우 기본적으로 모듈식(또는 컴포넌트화된) 방식을 개발에 사용하므로 모듈 간의 더 높은 디커플링이 필요합니다. TheRouter
모듈식 개발을 위한 완벽한 솔루션 세트로, 기존의 모듈 종속성 디커플링 및 페이지 점프를 지원할 뿐만 아니라 모듈화 프로세스의 일반적인 문제에 대한 솔루션을 제공합니다. 예를 들어, 모듈식 개발 후 구성 요소에서 Application
라이프 각 초기화 및 관련 종속성 호출에 대해 모듈 간에 코드를 수정해야 합니다.
그러나 그것을 사용하는 이유는 최종 분석에서 여전히 ARouter
사용하기에는 너무 골치 아픈 것입니다.
- 하나는 리지드(rigid), 모든 경로는 하드코딩(hard-coded)이지만, 유연하게
Crash
온라인 페이지를 일시적으로 H5로 다운그레이드하려면 많은 코드를 변경해야 하고 많은 제한이 있습니다. - 다른 하나는 효율성인데 컴파일 시간이든 시작 시간이든 이 두 가지 문제는 해결되지 않았습니다. 특정 공장의 오픈소스 프로젝트는 이런 식으로 작가는 승진, 이적해야 하고 나머지는 그대로 둔다. 결국 땜질은 KPI를 고려하지 않기 때문에 일을 보고할 방법이 없다. 시간이 많이 걸리는 지표를 시작하게 하는 혼자서 가자.
- 그러다가 핏(pit)이 발생
tinker
하는데, 패치를 사용할 때 같은 브랜치의 패키지가ARouter
제품Butterknife
패키지 코드와 다른 것을 발견하고, 이는 바로 패치의 크기를 증가시킨다. - 물론 많은 차이점이 있습니다. 이 표를 보세요.
기능 | 라우터 | AR라우터 | WMR라우터 |
---|---|---|---|
프래그먼트 라우팅 | ✔️ | ✔️ | ✔️ |
의존성 주입 지원 | ✔️ | ✔️ | ✔️ |
라우팅 테이블 로드 | 런타임 스캔 없음 반사 없음 |
런타임에 dex 리플렉션 인스턴스 클래스 를 스캔하면 성능 손실 이 큽니다. |
런타임에 파일을 읽는 것은 인스턴스 클래스의 성능 손실을 반영합니다. |
주석이 달린 정규식 | ✔️ | ✖️ | ✔️ |
활동 지정 인터셉터 | ✔️(四大拦截器可根据业务定制) | ✖️ | ✔️ |
导出路由文档 | ✔️(路由文档支持添加注释描述) | ✔️ | ✖️ |
动态注册路由信息 | ✔️ | ✔️ | ✖️ |
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互斥】的实现:xiaozhuanlan.com/topic/27358…
什么年代了,还在用 ARouter?
支持从 ARouter 一键迁移!
没错,什么年代了,还在用ARouter?
对于这种已有的存量路由框架,当然也是提供了一键迁移的图形化工具。
为了写这个工具我也是废了好大的劲,特意学了一遍JavaFX怎么用,然后打了一个Mac产物、一个Windows产物。
不禁感叹:Java的跨平台才是真正的跨平台啊。
注:传到了GitHub,可能有点慢,耐心等待
- Mac OS 迁移工具下载:github.com/HuolalaTech…
- Windows 마이그레이션 도구 다운로드: github.com/HuolalaTech…