这篇文章是我在 2022【GIAC 寰球互联网架构大会】分享时所讲内容的文字版本,批改删减了演讲时的冗余语言,现凋谢给大家浏览,心愿能给买不到票加入分享的 开源实验室 读者带来帮忙。
大家好,明天跟大家分享的是一个开源路由TheRouter
的设计。
代码地址: https://github.com/HuolalaTech/hll-wp-therouter-android
先来看一下目录 咱们从三点,来讲述明天的主题:
- 别离是模块化的开始,如何通过路由去实现一个模块化。
- 而后再依据指标,去设计一个动态化的路由解决咱们的问题,以及在咱们的我的项目中,是如何实际的。
- 最初,往年的大环境大家应该都晓得,考虑一下如何在资源有些的状况下,推动工程的重构。
这里有三张我手机上APP的截图,别离是:货拉拉、今日头条、美团
他们基本上能够代表了现在市面上大部分APP的一个状态,在这四五年里,互联网公司大幅减少,而APP的业务性能也一直增多。
从技术角度再看一下:
这是我列出来的一个APP的通用架构,这张图根本能够笼罩现如今百分之八九十的 APP 架构。
- 首先最上层是各个业务层 比方说是像货拉拉的搬家、拉货、运大件这种。
- 接下来是各个业务模块 比方常见的像用户账户体系、而后可能有一些直播、音视频、领取这样的场景模块。
- 再往下就是一些功能性的组件:他们可能跟具体的业务性能相干,比方推送、IM、广告控件、这样的一系列性能组件。
- 最底层就是基础设施了: 就像数据上报 异样统计等等一系列的必要根底能力。
而后在侧面还有一些贯通整个APP的能力 像CICD 国际化 端智能 热修复等等。
从这张图咱们也能看呈现现在的APP是越来越简单 性能也越来越多
对于性能越来越多,越来越简单的APP架构,咱们最间接能想到的就是通过模块化,将不同的性能、不同的业务做独立拆分,分而治之,升高整个零碎的复杂度。毕竟越简略,逻辑越少的代码块,BUG就越少。
所以大型 APP 的开发,根本都会选用模块化开发,同时对于模块间解耦要求更高。
而说到模块化,咱们肯定须要一个路由去承载不同模块之间的通信。路由是现如今 Android 开发中必不可少的性能,尤其是企业级APP,能够用于将原生页面跳转的强依赖解耦,同时缩小跨团队开发的相互依赖问题。
比方UI层级的跳转、功能模块的联动调用,这是做模块化绕不开的两点。
实现这两点最罕用的方法也就是:别离将咱们以后的一个UI页面与一个uri关联,用Uri代替咱们的页面,
这个样子在跳转的时候就不须要强依赖UI页面去做匹配,而只须要通过一段字符串去匹配就行
那另一种就是通过接口下沉,将模块依赖改为协定依赖,这样 咱们在不同的模块之间调度的时候 只须要依赖一个最根底的协定或者说是接口 去实现就能够了
做完模块化当前,一个APP
的复杂度曾经被升高很多了。
然而有一个最大的问题,咱们通过模块化是没方法解决的。
也就是APP
依赖用户去被动的更新降级,用户不更新,那就是永远在用旧版本,
当年,也是为了解决这个问题,催生出了很多黑科技,比方Android
的插件化、热修复这种黑科技,最终这些科技最终也被验证是点歪了的技能树。
明天我跟大家讲讲另一种解决办法:
回到咱们明天的主题:动态化路由
前些天咱们开源了一套,在安卓下面的动态化路由叫 TheRouter
他是一整套咱们实现APP
动态化的设计方案。包含模块化、包含远端路由下发、包含后面方才我列出来的几个依赖用户降级而造成的一些问题,咱们都是通过他来解决的。
之所以叫TheRouter
因为 The
代表了一种唯一性,咱们在设计的时候就参考了全副现有的开源计划,汲取了大量优良实现,同时补齐了各个计划的毛病。咱们认为做挪动端的模块化,只须要看这一个就够了。
首先咱们来看一下行业内路由的设计方案,不论是页面跳转,还是跨模块调用,基本上都是
- 开发阶段,对要应用路由的落地页或被调用办法增加注解标识。
- 在编译期解析注解,生成一系列中间代码,期待调用。
- 利用启动后调用中间代码实现路由的筹备动作。大部分路由会额定通过
Gradle
Transform
,在编译期做一次聚合,以晋升运行时筹备路由表的效率。 - 发动路由跳转时,实质上就是一次路由表遍历,通过uri获取到对应的落地页或办法对象,进行调用。
跨模块调用也是相似,在开发时做标记,编译时生成中间代码,运行时通过中间代码调用跨模块办法。
TheRouter
的整体实现逻辑也是依照这个思路去做的,不过咱们对于各个细节的解决,有更好的解决办法。
这是另一个角度,跟行业路由的一些比照数据。
大家能够次要关注这几个点:
- 第一个点:
TheRouter
是齐全无运行时扫描,没有任何反射代码的框架。undefined当然因为援用了Gson
做json
解析,他外面应该是用了反射的,但这不在咱们探讨的范畴内,如果你违心咱们容许自定义json
解析框架,你能够换成其余的解析。 - 第二点是
TheRouter
对增量编译反对十分好,APT
、plugin
都能做到增量编译。undefined同时咱们外部也有一套基于最新KSP
的注解解决代码,KSP是kotlin
专门用于解决注解做的一套实现,咱们之前用的都是kapt
,然而kapt
只能解决Kotlin
类的注解,如果是Kotlin
跟Java
混合的工程,他还没方法解决,所以在他外部还包了一层Java
的apt
,碰到他解析不了的文件,就调用apt
去解析,所以他的处理速度是十分慢的。undefined而KSP是基于语法树剖析去做的,咱们晓得,所有的代码在编译之前,都会先通过语法树剖析,他就是在这一步顺带把剖析进去的词法返回,让咱们做一些本人的定制逻辑。所以KSP其实不仅仅能够做注解解决,还能够做一些定制的语法分析规定,相似lint
那种。 - 第三点:
TheRouter
应该是现如今所有路由里,惟一一个反对AGP8的。Gradle从7.X开始,内置了编译过程解决的相干办法,所以AGP间接在8.0删除了雷同性能的办法,这就造成大量基于TransformAPI
的库,在AGP8都没方法应用了。 - 最初一点也是咱们之前碰到的坑,在用
tinker
这类热修复框架的时候,因为路由编译的产物代码是无序的,所以每次编译都有可能产生扭转,就造成咱们的补丁包十分大。TheRouter
对这一点也做了非凡反对,只有你没有新增或改变路由相干的代码,编译产物代码就不会有任何变动。
接下来须要大家一起思考一下,一个路由 他真正须要具备的外围能力是哪些。我后面PPT列了一下,参考当初业内的一些通用的路由解决方案 它真正外围须要解决的问题就两个点:
- 一个是解耦UI跳转
- 一个是升高零碎依赖
咱们把这两个指标别离拆开。
在跳转方面,除了业界罕用的通过路由字符串映射页面UI之外,咱们还退出了动静参数注入。
也就是一个UI页面须要的默认参数能够通过路由表提前申明好,而路由表能够是远端下发的,那这些默认参数也能够是远端下发的,这就做到了线上默认字段的及时更新。
另一部分,升高依赖,除了罕用的SPI接口下沉,将模块性能依赖改为接口协议依赖之外,咱们还提供了业务节点的hook
,所有模块能够反向订阅所需的业务节点,并在业务产生时做本人的逻辑解决。
这一个能力最罕用的中央,比方咱们在做隐衷合规的时候,要求用户批准隐衷协定当前,能力做一些敏感API的调用。在以前的开发,这些调用都得要放到隐衷弹窗所在的模块内,当用户点批准按钮当前,再调用其余模块初始化办法。这种逻辑对模块化是十分好受的,因为增大了跨模块的沟通,如果团队特地大,不同团队负责不同模块的时候,这种沟通就很累了,假如初始化办法须要减少一个参数,还得额定解决。哪些能力是要一启动就调用的,哪些API是必须用户批准当前能力调用的,都得沟通分明。
而咱们做了业务节点订阅当前,就把这种依赖某个业务节点的性能,做成了订阅公布模式,你只须要申明初始化办法依赖用户批准隐衷协定就行了,在用户批准当前就会主动调用初始化办法。
另外,咱们还容许客户端创立一套基于规定引擎的触发与响应,能够全局动静智能解决用户操作。假如客户端此刻碰到什么意外状况,比方一个女性用户,在夜里十一二点打车,路上又在某些偏远点产生异样停留,客户端能够被动做一些咱们预置的事件,比方主动报警、语音或者视频主动分割咱们的客服。比方像往年iPhone14
的新性能,有个车祸检测,如果车翻了或者撞车了,主动帮你打救济电话。而咱们这一系列规定,都能够是动静响应的。
接下来看一下路由的设计细节
TheRouter
会在编译期依据注解生成 RouteMap__
结尾的类,这些类中记录了以后模块的所有路由信息,也就是以后模块的路由表。
在最顶层的app
模块中,通过Gradle
插件,将所有aar、源码中的RouteMap__
结尾的类对立集中到TheRouterServiceProvideInjecter
类中。
后续利用启动后,初始化路由时只须要执行TheRouterServiceProvideInjecter
类的办法,就能没有任何反射的加载到全副的路由表了。
加载当前的路由表会被保留到一个反对正则匹配的 Map
中,这也是TheRouter
容许多个path
对应同一个落地页的起因。每当产生页面跳转时,通过跳转时的path
,去Map
中获取到对应的落地页信息,再失常调用startActivity()
即可。
对于模块化开发中跨模块的调用,咱们举荐采纳 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 润饰,否则编译期就会报错 */@ServiceProviderpublic 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
注解的办法,都会在编译期被解析,生成一个对应的 Task
对象,这个对象蕴含了初始化办法的相干信息,比方:是否异步执行、工作名、是否依赖其余工作先执行。
当所有aar都编译实现,生成好全副的 Task
当前,会在主 app 中通过Gradle
插件进行聚合,在这时会将所有的 Task
做一次查看,通过构建有向无环图
来避免 Task
产生循环援用的状况。
每次利用启动后,会在路由初始化时,将有向图中的全副Task
,依照依赖关系按程序加载。
能够在以后模块中,任意类中申明一个任意办法名的办法,给办法增加上@FlowTask
的注解即可。
@FlowTask
注解参数阐明:
- taskName:以后初始化工作的工作名,必须全局惟一,倡议格局为:
moduleName_taskName
- dependsOn:参考
Gradle
Task,工作与工作之间可能会有依赖关系。如果当前任务须要依赖其余工作先初始化,则在这里申明依赖的工作名。能够同时依赖多个工作,用英文逗号分隔,空格可选,会被过滤:dependsOn = “mmkv, config, login”,默认为空,利用启动就被调用 - async:是否要在异步执行此工作,默认false。
最初一个,APP动静响应的实现。
还是回到之前的例子:假如一个女性、夜里12点、KTV上车、偏远地点停车,那么咱们就能够依据这样的一系列先决条件,交由后端的智慧大脑剖析,而后下发给客户端一个动作:比方关上视频或语音,让客服染指。
而把这个例子形象一下,所有用户的操作,比方点击、曝光、页面跳转等等埋点数据,都能够作为剖析数据交给服务端剖析,而后让客户端执行:跳转页面、弹窗、优惠券、或者其余本地办法。
这样的一个流程做完了当前,只有咱们有一个牢靠的行为分析模型,咱们是大概率能够预测用户接下来的行为是要做什么的。
当然,即使咱们没有这样一个用户行为剖析的大脑,纯客户端的计划,也是可能反对的,这就是离线端智能计划了。
最初咱们再来看一下后面提到的几个 APP 的弊病,在 TheRouter 中是怎么解决的呢?
- 第一个:页面Crash,咱们能够通过去批改路由表,而后咱们把某一些页面的 Crash 给它降级,降级成 H5 或者说是小程序。当假如咱们这个页面没方法拜访的时候,咱们能够让用户先临时地去拜访 H5 页面或者说小程序页面。同样的,如果某个页面白屏很久,咱们也能够通过降级,间接通过H5或小程序的形式兼容关上。
- 第二个:对于一些接口字段,老版本的兼容问题,咱们也是可能去下发默认参数的形式。如果老版本它强制要求有某一个参数,那其实咱们能够把这个参数给下发成一个默认参数。如果咱们做了千人千面的话,那每一个用户都能够达到不同参数不同展现的成果。
- 第三个:新性能透传及时性。假如咱们以后有某一个直播的页面,新版本曾经有一个能够让用户打赏或者说是让用户发礼物这样的性能了。那老版本它还没有这样的一个性能的话,咱们能够通过点击礼物图标后,批改落地页把它给他提醒降级弹窗。这样的降级弹窗对用户是影响最小的,它只在应用到这个性能的时候才须要做某一些降级。
- 第四个意外事件解决:就是我后面讲到的云端大脑或端智能这样的利用场景了。
最初咱们来看明天的第三局部,往年的状况大家都能感触,各种人员优化,大家都很忙,那如何将这种大的技术重构老本降到最低呢,咱们为TheRouter
开发了很多周边能力:
TheRouter
提供了图形化界面的一键迁徙工具,能够一键从其余路由迁徙到TheRouter
,整个迁徙过程都是基于字符串匹配实现的,不波及任何黑科技,所有的替换点也都会展现进去,十分平安。在替换实现后,主动输入改变页面与测试点,大幅缩小了开发与测试的工作量。
还有一个用于主动跳转的高效IDE
辅助插件,能够间接从路由的申明处查看到哪些地方跳转到本路由,再也不必怕路由字符串满天飞了。
只须要点一下右边的图标,就能主动跳转到落地页了。假如咱们有多个跳转,跳转到同一个落地页的,点击落地页左侧的图标,也会展现出对应的代码,抉择当前也能够主动跳转过来。
另外还有一个很好的个性,就是如果你写了没有落地页的跳转,会在IDE
左侧有个黄色的正告,提醒你是不是因为手抖或其余起因,写错了path
。
另外TheRouter
还提供了官网和微信群,官网有大量的技术文档和领导教程,有不懂的问题还能够退出微信群寻求帮忙。
官网:https://therouter.cn
微信群:https://therouter.cn/wx/
总的来说,TheRouter
并不仅仅是一个玲珑灵便的路由库,而是一整套残缺的 Android
模块化解决方案,可能解决简直全副的模块化过程中会遇到的问题。 对于现有的路由框架,咱们也在最大限度反对平滑迁徙。你也能够在Github
issue
中提出需要,咱们评估后会尽快反对,也欢送任何人提供 Pull Requests
。