共计 10518 个字符,预计需要花费 27 分钟才能阅读完成。
TheRouter
是一个 Kotlin 编写,用于 Android 模块化开发的一整套解决方案框架。
Github 我的项目地址与应用文档详见 https://github.com/HuolalaTech/hll-wp-therouter-android。
TheRouter 外围性能具备如下能力:
- 页面导航跳转能力(Navigator)
- 跨模块依赖注入能力(ServiceProvider)
- 单模块主动初始化能力(FlowTaskExecutor)
- 动态化能力(ActionManager)
- 模块 AAR/ 源码依赖一键切换脚本
一、为什么要应用 TheRouter
路由是现如今挪动端开发中必不可少的性能,尤其是企业级 APP,能够用于将 Intent
页面跳转的强依赖关系解耦,同时缩小跨团队开发的相互依赖问题。
对于大型 APP 开发,根本都会选用模块化 (或组件化) 形式开发,对于模块间解耦要求更高。TheRouter
是一整套齐全面向模块化开发的解决方案,不仅能反对惯例的模块依赖解耦、页面跳转,同时提供了模块化过程中常见问题的解决办法。例如:完满解决了模块化开发后因为组件内无奈获取 Application
生命周期与业务流程,造成每次初始化与关联依赖调用都须要跨模块批改代码的问题。
1.1 TheRouter 四大能力
Navigator:
- 反对
Activity
和Fragment
- 反对
Path
与页面多对一关系或一对一关系,可用于解决多端 path 对立问题 - 页面
Path
反对正则表达式申明 - 反对
json
格局路由表导出 - 反对动静下发
json
路由表,降级任意页面为 H5 - 反对任意
object
跨模块传递(无需序列化,且能保障对象类型) - 反对页面跳转拦挡解决
- 反对自定义页面参数解析形式(例如将
json
解析为对象) - 反对应用路由跳转到第三方 SDK 中的
Activity
(Fragment
)
ServiceProvider:
- 反对跨模块依赖注入
- 反对自定义注入项的创立规定,依赖注入可自定义参数
- 反对自定义服务拦挡,单模块
mock
调试 - 反对注入对象缓存,屡次注入 只会 new 一次对象
FlowTaskExecutor:
- 反对单模块独立初始化
- 反对懒加载初始化
- 独立初始化容许多任务依赖(参考
Gradle Task
) - 反对编译期循环援用检测
- 反对自定义业务初始化机会,能够用于解决隐衷合规问题
ActionManager:
- 反对全局回调配置
- 反对优先级响应与中断响应
- 反对记录调用门路,解决调试期观察者模式无奈追踪
Observable
的问题
注: FlowTaskExecutor
、ActionManager
后续会作为可选能力,提供 可插拔
或独自应用
的选项(预计 10 月份提供)。
二、路由计划
目前现有的路由基本上集中于两种能力的实现:页面跳转、跨模块调用,核心技术计划大体上如图:
- 开发阶段,对要应用路由的落地页或被调用办法增加注解标识。
- 编译期解析注解,生成一系列中间代码,待调用。
- 利用启动后调用中间代码实现路由的筹备动作。大部分路由会额定通过
Gradle Transform
,在编译期做一次聚合,以晋升运行时筹备路由表的效率。 - 发动路由跳转时,实质上就是一次路由表遍历,通过 uri 获取到对应的落地页或办法对象,进行调用。
TheRouter
的页面跳转、跨模块调用也是如此,然而在设计上会有一些细节解决。
TheRouter
会在编译期依据注解生成 RouteMap__
结尾的类,这些类中记录了以后模块的所有路由信息,也就是以后模块的路由表。
在最顶层的 app
模块中,通过 Gradle
插件,将所有 aar、源码中的 RouteMap__
结尾的类对立集中到 TheRouterServiceProvideInjecter
类中。
后续利用启动后,初始化路由时只须要执行 TheRouterServiceProvideInjecter
类的办法,就能 没有任何反射 的加载到全副的路由表了。
加载当前的路由表会被保留到一个反对正则匹配的 Map
中,这也是 TheRouter
容许多个 path
对应同一个落地页的起因。每当产生页面跳转时,通过跳转时的 path
,去Map
中获取到对应的落地页信息,再失常调用 startActivity()
即可。
三、应用 TheRouter 页面跳转
3.1 申明路由项
如果一个页面(反对 Activity、Fragment)容许被路由关上,则须要应用注解 @Route
申明路由项,每个页面容许申明多个路由项,也就是一对多的能力,极大升高多端路由对立时的业务影响面。
参数释义
- path: 路由 path【必传】。
倡议是一个 url。path 内反对应用正则表达式(为了匹配效率,正则必须蕴含反双斜杠 \),容许多个 path 对应同一个 Activity(Fragment)。 - action: 自定义事件【可选】。
个别用来关上指标页面后做一个执行动作,例如自定义页面弹出广告弹窗。 - description: 页面形容【可选】。
会被记录到路由表中,不便前期排查的时候晓得每个 path 或 Activity 是什么业务。 -
params: 页面参数【可选】。
主动写入intent
中,容许写在路由表中动静下发批改默认值,或通过路由跳转时代码传入。@Route(path = "http://therouter.com/home", action = "action://scheme.com", description = "第二个页面", params = {"hello", "world"}) public class HomeActivity extends AppCompatActivity {}
3.2 发动页面跳转
传入的参数能够是 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();
3.3 路由表生成规定
如果两条路由的 path
、指标className
完全相同,则认为是同一条路由,不会思考参数是否雷同 。
路由表生成规定:编译期依照如下程序取 并集。
笼罩规定 :
依据如下程序,如果雷同,后者能够笼罩前者的路由表规定。
- 编译期解析注解生成路由表
- 首先取
业务模块 aar
中的路由表 - 再取 主
app module
代码中的路由表 -
最初取
assets/RouteMap.json
文件中申明的路由表。- 如果编译期没有这个文件,会生成一份默认路由表放在这个目录内;如果有,会将路由表合并。
- 路由表生成时可配置是否启用查看路由合法性,判断指标页面是否存在,(warning/error)级别。
-
运行时线上动静下发的路由表
- 路由表容许线上动静下发,将笼罩本地路由表,详见【3.4 动静路由表的设计与应用】
如果编译期没有这个文件,会生成一份默认路由表放在这个目录内;如果有,会将路由表合并,因而,对于没方法批改代码的第三方 SDK 外部,如果心愿通过路由关上,只须要手动在 RouteMap.json
文件中申明,就能通过路由关上了。
3.4 动静路由表的设计与应用
TheRouter
的路由表是动静增加的,我的项目每次编译后,会在 apk 内生成一份以后 APP 的全量路由表,默认门路为:/assets/therouter/routeMap.json
。这个路由表也能够后续通过近程下发的形式应用,例如远端能够针对不同的 APP 版本,下发不同的路由表达到配置目标。这样如果未来线上某些页面产生 Crash,能够通过将这个页面的落地页替换为 H5 的形式,长期解决这类问题。
有两种举荐的近程下发形式可供使用方抉择:
- 将打包零碎与配置零碎买通,每次新版本 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()}
}
});
3.5 高级用法
TheRouter 同时反对更多页面跳转能力,详情可参考我的项目文档【https://github.com/HuolalaTech/hll-wp-therouter-android/wiki/Navigator】:
- 为第三方库外面的页面增加路由表,达到对某些页面降级替换的目标;
- 提早路由跳转(从 Android 8 开始,不能在后盾启动页面);
- 跳转过程拦截器(总共四层,可依据理论需要应用);
- 跳转后果回调;
四、跨模块依赖注入 ServiceProvider 的设计
对于模块化开发中跨模块的调用,咱们举荐采纳 SOA(面向服务架构) 的设计形式,服务调用方与应用方齐全隔离,调用模块外的能力不须要关注能力的提供者是谁。ServiceProvider
的外围设计思维也是这样的,目前服务间的调用协定采纳接口的形式。当然,也能够兼容不通过接口下沉而是间接调用的状况。
具体到 Android 侧就是 AIDL 相似的设计,只是要比 AIDL 开发简略很多:
- 服务提供方负责提供服务,不须要关怀调用方是谁会在何时调用本人。
- 服务的应用方只关注服务自身,不须要关怀这个服务是谁提供的,只须要只能服务能提供哪些能力即可。
例如下面的图片:拉拉须要应用录音的服务,小货则向外提供一个录音的服务,由 TheRouter
的ServiceProvider
负责撮合。
4.1 服务应用方:拉拉
她无需关怀,IRecordService
这个接口服务是谁提供的,他只须要晓得本人须要应用这样的一个服务就行了。
注:如果没有提供服务的提供方,TheRouter.get()
可能返回null
TheRouter.get(IRecordService::class.java)?.doRecord()
4.2 服务提供方:小货
服务提供方须要申明一个提供服务的办法,用 @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}
五、单模块主动初始化能力 FlowTaskExecutor 的设计
后面讲过,TheRouter
是齐全面向模块化开发提供的一套解决方案。在模块化开发时,可能每个模块都有本人须要初始化的一些代码。以前的做法是把这些代码都在 Application
里申明,然而这样可能随着业务变动每次都须要批改 Application
所在模块。TheRouter
的单模块主动初始化能力就是为了解决这样的状况,能够只在以后模块申明初始化办法后,将会在业务场景时主动被调用。
每个心愿被主动初始化的办法,必须应用 public static
润饰,次要起因是这样子就能通过类名间接调用了。另外很多初始化代码都须要获取 Context
对象,所以咱们将 Context
作为初始化办法的默认参数,会主动传入Application
。其余的所在类名、办法名都没有限度,反正只有加上了 @FlowTask
注解,在编译期都能通过 APT 获取到。
5.1 FlowTaskExecutor 应用介绍
能够在以后模块中,任意类中申明一个任意办法名的办法,给办法增加上@FlowTask
的注解即可。
@FlowTask
注解参数阐明:
- taskName:以后初始化工作的工作名,必须全局惟一,倡议格局为:
moduleName_taskName
- dependsOn:参考
Gradle
Task,工作与工作之间可能会有依赖关系。如果当前任务须要依赖其余工作先初始化,则在这里申明依赖的工作名。能够同时依赖多个工作,用英文逗号分隔,空格可选,会被过滤:dependsOn = “mmkv, config, login”,默认为空,利用启动就被调用 - async:是否要在异步执行此工作,默认 false。
/**
* 将会在异步执行
*/
@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 两个工作都执行当前才会被执行");
}
5.2 内置初始化节点
应用这个能力,在路由外部默认反对了两个生命周期类工作,可在应用时间接援用
- TheRouterFlowTask.APP_ONCREATE:当 Application 的 onCreate()执行后初始化
- TheRouterFlowTask.APP_ONSPLASH:当利用的首个 Activity.onCreate()执行后初始化
同时,应用 TheRouter
的主动初始化依赖,也无需放心循环依赖造成的问题,框架会在编译期构建有向无环图,监测循环依赖状况,如果发现会在编译期间接报错,并且还会将产生循环援用的工作显示进去,用于排错。
5.3 实现原理
每个加了 @FlowTask
注解的办法,都会在编译期被解析,生成一个对应的 Task
对象,这个对象蕴含了初始化办法的相干信息,比方:是否异步执行、工作名、是否依赖其余工作先执行。
当所有 aar 都编译实现,生成好全副的 Task
当前,会在主 app 中通过 Gradle
插件进行聚合,在这时会将所有的 Task
做一次查看,通过构建 有向无环图
来避免 Task
产生循环援用的状况。
每次利用启动后,会在路由初始化时,将有向图中的全副Task
,依照依赖关系按程序加载。
六、动态化能力 ActionManager 的设计
Action
实质是一个全局的零碎回调,次要用于预埋的一系列操作,例如:弹窗、上传日志、清理缓存。
与 Android 零碎自带的播送告诉相似,你能够在任何中央申明动作与解决形式。并且所有 Action
都是能够被跟踪的,只有你违心,能够在日志中将所有的动作调用栈输入,以不便调试应用,这样在肯定水平上能够解决观察者模式带来的通病:无奈追踪 Observable
的问题。
6.1 Action 应用
申明一个 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()
6.2 高级用法
每个 Action
容许关联多个 ActionInterceptor
进行解决,多个 ActionInterceptor
之间能够自定义拦截器优先级,同时容许终止接下来的低优先级拦截器的执行。
最典型利用场景:首页可能会有多个弹窗,不同业务之间的弹窗是有优先级之分的,为了体验优化咱们必定不会在首页一次把所有弹窗全副弹出,能够通过 ActionInterceptor
为每个弹窗申明好优先级关系,假如需要是首页只能弹出 3 个弹窗,那么第三个弹窗处理完毕即可敞开以后事件,接下来的拦截器将不会被响应。
abstract class ActionInterceptor {abstract fun handle(context: Context, args: Bundle): Boolean
fun onFinish() {}
/**
* 数字越大,优先级越高
*/
open val priority: Int
get() = 5}
6.3 客户端动静响应应用场景
如果仅客户端应用,罕用的场景可能是:当用户执行某些操作(关上某个页面、H5 点击某个按钮、动静页面配置的点击事件)时,将会主动触发,执行预埋的 Action 逻辑。
如果与服务端链路买通,这个能力其实是须要整个公司的配合,比方有一套相似智慧大脑的计划,能够基于客户端过来的一些埋点数据,智能推断出用户下一步要做的事件,而后通过长连贯间接向客户端下发指令做某些事件。那么通过客户端预埋的页面跳转、弹窗、清缓存、退出登录等等操作,就能够通过服务端指令进行操作,则就是一套残缺的动态化计划。
七、一键切换源码与 AAR
7.1 模块化反对的 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
的目标了。
八、从其余路由迁徙至 TheRouter
8.1 迁徙工具一键迁徙
TheRouter
提供了图形化界面的迁徙工具,能够一键从其余路由迁徙到TheRouter
,目前仅反对ARouter
,其余路由框架迁徙也在开发中(GitHub 下载,70M 左右,请急躁期待):
- Mac OS 迁徙工具下载:https://github.com/HuolalaTech/hll-wp-therouter-android/wiki/uploads/file/TheRouterTransfer-Mac.zip
- Windows 迁徙工具下载:https://github.com/HuolalaTech/hll-wp-therouter-android/wiki/uploads/file/TheRouterTransfer-Windows.zip
如果我的项目中应用了 ARouter 的 IProvider.init()办法,可能须要手动解决初始化逻辑。
如下图:
8.2 与其余路由比照
性能 | TheRouter | ARouter | WMRouter |
---|---|---|---|
Fragment 路由 | ✔️ | ✔️ | ✔️ |
反对依赖注入 | ✔️ | ✔️ | ✔️ |
加载路由表 | 无运行时扫描 无反射 |
运行时扫描 dex 反射实例类 性能损耗大 |
运行时读文件 反射实例类 性能损耗中 |
注解正则表达式 | ✔️ | ✖️ | ✔️ |
Activity 指定拦截器 | ✔️(四大拦截器可依据业务定制) | ✖️ | ✔️ |
导出路由文档 | ✔️(路由文档反对增加正文形容) | ✔️ | ✖️ |
动静注册路由信息 | ✔️ | ✔️ | ✖️ |
APT 反对增量编译 | ✔️ | ✔️(开启文档生成则无奈增量编译) | ✖️ |
plugin 反对增量编译 | ✔️ | ✖️ | ✖️ |
多 Path 对应同一页面(低成本实现双端 path 对立) | ✔️ | ✖️ | ✖️ |
远端路由表下发 | ✔️ | ✖️ | ✖️ |
反对单模块独立初始化 | ✔️ | ✖️ | ✖️ |
反对应用路由关上第三方库页面 | ✔️ | ✖️ | ✖️ |
反对应用路由关上第三方库页面 | ✔️ | ✖️ | ✖️ |
对热修复反对(例如 tinker) | ✔️(未扭转的代码屡次构建无变动) | ✖️(屡次构建 apt 产物会发生变化,生成无意义补丁) | ✖️(屡次构建 apt 产物会发生变化,生成无意义补丁) |
九、总结
TheRouter
并不仅仅是一个玲珑灵便的路由库,而是一整套残缺的 Android
模块化解决方案,可能解决简直全副的模块化过程中会遇到的问题。
对于现有的路由框架,咱们也在最大限度反对平滑迁徙,目前已实现 ARouter
的一键迁徙工具,其余框架的迁徙仍在开发中。你也能够在 Github
issue
中提出需要,咱们评估后会尽快反对,也欢送任何人提供 Pull Requests
。
更多问题请拜访:具体沟通