当前位置: 首页 > 后端技术 > Java

动态路由TheRouter的设计与实践

时间:2023-04-01 20:32:47 Java

这篇文章是我在2022【GIAC全球互联网架构大会】分享的时候说的文字版。演讲中多余的词被修改删除,现开放给大家阅读。希望能帮助到开源实验室买不到票参与分享的读者。大家好,今天要跟大家分享的是一个开源路由器TheRouter的设计。代码地址:https://github.com/HuolalaTech/hll-wp-therouter-android先来看看目录吧。我们从三个点来聊聊今天的话题:模块化的开始,以及如何通过路由变化来实现一个模块。然后根据目标,设计一个动态路由来解决我们的问题,以及如何在我们的项目中实践。最后,大家要清楚今年的大环境,考虑在有资源的情况下,如何推动项目的重建。下面是我手机上三个app的截图,分别是:货拉拉、今日头条、美团。它们基本上可以代表当今市场上大多数应用程序的一种形式。在过去的四五年里,互联网公司数量大幅增加。APP的业务功能也在不断增加。从技术角度看:这是我列举的一个APP的大致架构。这张图基本上可以覆盖今天80%到90%的APP架构。首先,顶层是各个业务层。比如像霍拉拉的搬家,货物的运送,大件物品的运输。接下来是各种业务模块,比如普通的用户账号系统,然后可能是直播、音视频、支付等一些场景模块。再往下是一些功能组件:可能和具体的业务功能相关,比如推送、IM、广告控件等一系列功能组件。底层是基础设施:数据上报、异常统计等一系列必要的基础能力。然后是侧面贯穿整个APP的一些能力,比如CICD国际化、终端智能化、热修复等等。从这张图我们也可以看出,如今的APP越来越复杂,功能也越来越多。对于越来越多的功能和越来越复杂的APP架构,我们最直接能想到的就是模块化,将不同的功能和不同的业务独立拆分,分而治之,降低整个系统的复杂度。毕竟,代码块越简单,逻辑越少,bug就越少。因此,大型APP的开发基本都会采用模块化开发,同时对模块之间的解耦要求也更高。说到模块化,肯定需要一条路由来承载不同模块之间的通信。路由是当今Android开发中不可或缺的功能,尤其是对于企业级APP,可以用来解耦原生页面跳转的强依赖,减少跨团队开发的相互依赖。比如UI层面的跳转和功能模块的联动调用,就是模块化中绕不开的两点。这两点最常见的实现方式是:将我们当前的一个UI页面关联一个uri,用Uri替换我们的页面。这样跳转的时候就不需要大量依赖UI页面进行匹配了,只需要通过字符串进行匹配即可。另一种是通过接口下沉,将模块依赖改为协议依赖,这样我们在不同模块之间进行调度时,只需要依赖最基本的协议或接口即可。只是实施它。模块化之后,一个APP的复杂度降低了很多。但是有一个最大的问题我们无法通过模块化来解决。也就是说,APP是靠用户主动更新升级的。如果用户不更新,他们将一直使用旧版本。当年为了解决这个问题,诞生了很多黑科技,比如安卓外挂、热修复等,最后,这些技术最终被验证为歪技能树。今天跟大家说说另外一个解决方案:回到我们今天的话题:前几天我们开源了一套动态路由,Android上的动态路由叫做TheRouter,是一整套动态路由供我们使用实现APP动态设计。包括模块化,包括远程路由下发,包括我刚才列举的一些依赖用户升级导致的问题,我们都是用他来解决的。之所以叫TheRouter,是因为The代表了一种唯一性。在设计时,我们参考了所有现有的开源方案,吸收了大量优秀的实现,弥补了每个方案的不足。我们认为要做移动端的模块化,只需要看这一个。首先我们来看看业界对于路由的设计方案。不管是页面跳转还是跨模块调用,基本都是开发阶段,在着陆页或者被调用的方法上添加注解来使用路由。编译时解析注解,生成一系列中间代码,等待调用。应用启动后,调用中间代码完成路由的准备工作。大多数路由都会额外经过GradleTransform,在编译时进行聚合,提高运行时准备路由表的效率。当发起路由跳转时,本质上是一次路由表遍历,通过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的。从7.X开始,Gradle内置了与编译过程相关的方法,所以AGP在8.0中直接删除了相同功能的方法,导致大量基于TransformAPI的库在AGP8中无法使用。最后一点也是我们之前遇到的坑。在使用tinker等热修复框架时,由于路由编译出来的产品代码是乱序的,可能每次编译都会发生变化,导致补丁包非常大。.TheRouter对于这一点也做了特别的支持。只要不添加或更改路由相关的代码,编译后的产品代码就不会改变。接下来就需要大家思考一个路由器真正需要具备哪些核心能力。我在之前的PPT中列出来了。参考了一些业界通用的路由方案,真正需要解决的核心问题有两点:一是解耦UI跳转,二是降低系统依赖。让我们把这两个目标分开。.在重定向方面,除了业界常用的路由字符串映射页面UI外,我们还加入了动态参数注入。也就是说,可以通过路由表提前声明一个UI页面需要的默认参数,路由表可以远程下发,所以这些默认参数也可以远程下发,实现了默认字段在线。按时更新。另一部分是减少依赖。除了常用的SPI接口下沉,将模块功能依赖改为接口协议依赖外,我们还提供了业务节点的hook。所有模块都可以反向订阅需要的业务节点,在业务发生的时候做自己的逻辑。此功能最常用。比如我们在做隐私合规的时候,要求用户在进行一些敏感的API调用之前,先同意隐私协议。在之前的开发中,这些调用都必须放在隐私弹窗所在的模块中。当用户点击同意按钮时,会调用其他模块的初始化方法。这种逻辑对于模块化来说是很不舒服的,因为它增加了跨模块通信。如果团队很大,不同的团队负责不同的模块,这种沟通会很累。假设初始化方法需要添加一个参数。必须另外处理。哪些能力一启动就调用,哪些API需要用户同意后才能调用,必须沟通清楚。而我们订阅业务节点后,我们将依赖某个业务节点的功能做成订阅发布的方式。你只需要声明初始化方法取决于用户是否同意隐私协议,用户同意后会自动调用。初始化方法。此外,我们还允许客户端基于规则引擎创建一套触发器和响应,可以在全局范围内动态智能地处理用户操作。假设客户此刻遇到任何突发情况,比如某位女性用户在晚上11、12点打车,在路上一些偏僻的地方异常停车,客户可以主动做我们预设的一些事件,如自动报警、语音或视频自动联系我们的客服。比如今年iPhone14的新功能就有车祸检测。如果汽车翻车或撞车,它会自动呼叫救援。而我们这一系列的规则都是可以动态响应的。接下来看走线设计细节。TheRouter在编译时会根据注解生成以RouteMap__开头的类。这些类记录了当前模块的所有路由信息,即当前模块的路由表。在顶层app模块中,通过Gradle插件,将aar和源码中所有以RouteMap__开头的类统一到TheRouterServiceProvideInjecter类中。后续应用启动后,只需要在初始化路由时执行TheRouterServiceProvideInjecter类的方法,就可以加载所有的路由表,不需要任何反射。加载的路由表会保存在一个支持正则匹配的Map中,这也是TheRouter允许多条路径对应同一个登陆页面的原因。每当发生页面跳转时,通过跳转时的路径去Map中获取对应的登陆页面信息,然后正常调用startActivity()。对于模块化开发中的跨模块调用,我们推荐采用SOA(ServiceOrientedArchitecture)设计方式。服务调用者与用户完全隔离,模块外调用的能力不需要关注能力的提供者是谁。ServiceProvider的核心设计思想也是如此。目前,服务之间的调用协议采用接口的方式。当然也兼容不通过接口下沉而是直接调用的情况。具体到Android端,是类似AIDL的设计,但是比AIDL开发要简单的多:服务提供者负责提供服务,不需要关心调用者什么时候调用自己。服务的使用者只关注服务本身,不需要关心服务是谁提供的,只需要知道服务可以提供什么能力即可。比如上图中:服务使用者需要使用录音服务,服务提供者对外提供录音服务,由TheRouter的ServiceProvider负责匹配。服务使用者:无需关心,IRecordService接口服务是谁提供的,他只需要知道自己需要使用这样的服务即可。注意:如果没有服务提供者,TheRouter.get()可能返回nullTheRouter.get(IRecordService::class.java)?.doRecord()服务提供者:服务提供者需要声明一个服务提供者方法,使用@ServiceProvider注解标记。如果是java,一定要publicstatic修饰如果是kotlin,建议写成顶层函数方法名不限/***方法名不限,任何名字都可以*返回值必须是服务接口的名字。如果是实现服务的子类,需要通过returnType来限定(比如下面的代码)*方法必须用publicstatic修饰,否则编译时会报错*/@ServiceProviderpublicstaticIRecordServicetest(){returnnewIRecordService(){@OverridepublicvoiddoRecord(){Stringstr="执行记录逻辑";}};}//也可以直接返回对象,然后标记这个方法的服务名@ServiceProvider(returnType=IRecordService.class)publicstaticRecordServiceImpltest(){//xxx}前面说了TheRouter是一个模块化开发的全套解决方案。在模块化开发中,每个模块都可能有一些代码需要初始化。之前的做法是在Application中声明这些代码,但这可能需要每次业务变化时修改Application所在的模块。TheRouter的单模块自动初始化能力就是为了解决这种情况。只有在当前模块声明了初始化方法后,才能在业务场景中自动调用。每一个想要自动初始化的方法都必须用publicstatic修饰。主要是可以通过类名直接调用。另外,很多初始化代码需要获取Context对象,所以我们使用Context作为初始化方法的默认参数,会自动传递给Application。其他类名和方法名没有限制。反正只要加上@FlowTask注解,在编译时就可以通过APT获取到。或者在隐私合规的情况下,有些函数需要同意隐私协议才可以调用。当需要跨模块依赖时,当前模块的初始化需要在另一个模块初始化后进行,其他服务可以通过独立于业务节点订阅来解耦。每个用@FlowTask注解的方法都会在编译时解析生成对应的Task对象,其中包含初始化方法的信息,如:是否异步执行,任务名称,是否依赖其他任务执行第一的。当所有的aar编译完成,所有的task都生成后,会通过Gradle插件聚合到主app中。此时会检查所有任务,通过构建有向无环图Case来防止任务中出现循环引用。每次启动应用程序时,有向图中的所有任务都会按照路由初始化时的依赖关系依次加载。你可以在当前模块的任意类中声明任意方法名的方法,只需要在方法上加上@FlowTask注解即可。@FlowTask注解参数说明:taskName:当前初始化任务的任务名称,必须全局唯一。推荐格式为:moduleName_taskNamedependsOn:参考GradleTask,任务之间可能存在依赖关系。如果当前任务需要依赖其他任务先初始化,在这里声明依赖的任务名。可以同时依赖多个任务,英文逗号分隔,空格可选,会被过滤:dependsOn="mmkv,config,login",默认为空,应用启动async时调用:是否异步执行该任务,默认false。最后一个是APP动态响应的实现。回到前面的例子:假设一个女人,半夜12点,在KTV上车,停在比较远的地方,那么我们可以交给后端智能大脑进行分析基于这样一系列的前提条件,然后发送给客户端。动作:比如打开视频或语音,让客服介入。但是把这个例子抽象一下,所有的用户操作,比如点击,曝光,页面跳转等等埋点数据,都可以作为分析数据交给服务端进行分析,然后让客户端执行:跳转页面,弹窗窗口、优惠券或其他本地方法。这样一个过程完成后,只要我们有一个可靠的行为分析模型,我们就可以大概率预测用户接下来会做什么。当然,即使我们没有这样的用户行为分析大脑,纯客户端的方案还是可以支持的。这是离线智能解决方案。最后,让我们看看上述APP的缺点。它们在TheRouter中是如何解决的?第一种:页面崩溃,我们可以修改路由表,然后我们将部分页面的崩溃降级为H5或者小程序。当假设我们的页面无法访问时,我们可以让用户暂时访问H5页面或者小程序页面。同样,如果某个页面长时间白屏,我们也可以通过降级直接通过H5或者小程序打开。第二种:对于一些接口字段和旧版本的兼容性问题,我们也可以发布默认参数。如果旧版本强制要求有某个参数,那么我们其实可以把这个参数作为默认参数发送。如果我们有几千个人、几千张脸,那么每个用户都可以通过不同的参数实现不同的显示效果。第三个:新功能透传的时效性。假设我们目前有一个直播页面,新版本已经有了可以让用户打赏或者送礼物的功能。如果旧版本没有这个功能,我们可以点击礼物图标,修改登陆页面,提示他升级弹窗。这样的升级弹窗对用户的影响最小,使用该功能时只需要进行一定的升级即可。第四种事件处理:就是前面提到的云脑或者终端智能的应用场景。最后,我们来看今天的第三部分。今年的情况大家都能感受到。大家都在忙着各种人员优化。那么如何将这次技术大改造的成本降到最低呢?我们为TheRouterPeripheral能力开发了很多:TheRouter提供了图形界面的一键迁移工具,可以一键从其他路由迁移到TheRouter。整个迁移过程是基于字符串匹配完成的,没有涉及任何黑科技,所有的替换点也会显示出来,非常安全。替换完成后自动输出修改后的页面和测试点,大大减少了开发和测试的工作量。还有自动跳转的高效IDE辅助插件。可以直接从路由的声明中查看哪些地方可以跳转到这条路由,不用担心路由字符串满天飞。只需单击左侧的图标,您将自动重定向到登录页面。假设我们有多个跳转,跳转到同一个登陆页面的,点击登陆页面左侧的图标,相应的代码也会显示出来,选择也可以自动跳转到。还有一个不错的功能是,如果你写了一个没有登陆页面的跳转,IDE的左边会有一个黄色的警告,提醒你是不是因为手抖或者其他原因写错了路径。此外,TheRouter还提供了官网和微信群。官网有大量的技术文档和教程。有问题也可以加微信群寻求帮助。官网:https://therouter.cn微信群:https://therouter.cn/wx/总的来说,TheRouter不仅仅是一个小巧灵活的路由库,而是一套完整的Android模块化解决方案,几乎可以解决模块化过程中遇到的所有问题。对于现有的路由框架,我们也在最大限度的支持平滑迁移。您也可以在Githubissue中提出需求,我们会在评估后尽快支持,欢迎大家提供PullRequests。