前言
随着Android平台的飞速发展,许多老牌App,都发展成了所谓的“超级应用”,不但功能模块众多、代码量巨大,团队规模更是扩大到了几十人甚至上百人的规模。一线大厂的旗舰应用,甚至可能涉及到多个部门的协同开发。 在这个过程里,必然的产生了一些“代码复用”和“协同效率”方面的问题。所以,组件化开发逐渐被各个大厂提上了日程。如今,移动端开发除了要解决“代码”方面的问题,也需要关注“工程”方面的问题。 2022年,百度网盘App迎来了10周年,在这10年的发展过程中,我们也遇到过各种各样的的“工程”问题,为了解决这些问题,我们引入了组件化开发,而Rubik就是在这个过程中诞生的。关于Rubik
Rubik由百度网盘团队出品,从2019年开始建设,至今已经迭代了两个大版本,并在多款线上产品中得到应用。 Rubik是一套解决Android平台组件化的综合框架,也就是说,Rubik在帮助我们实现模块解耦的同时,还能能够提供一些组件管理能力。Rubik所提供的模块解耦能力是一种基于“函数路由”的组件间低耦合通讯方案。组件管理能力能够帮助我们实现组件的版本控制、maven发布、aar/jar与源码之间的切换等能力,Rubik还可以通过配置文件,更简便的把现有的组件,组合成不同的APK。 Rubik框架的工程结构 换而言之,Rubik专注于解决Android应用开发过程中的组件化问题。但是,Rubik作为一个完全由Kotlin语言实现的开源框架,即使你没有组件化开发方面的需求,也非常欢迎关注Rubik的技术实现,欢迎阅读Rubik的代码,因为: 首先,Rubik完全采用kotlin开发,并且大量应用了Kotlin的函数式编程、DSL等特性,非常适合正在学习Kotlin的小伙伴交流学习; 另外,在Rubik的实现过程中,也应用到了Kotlin代码自动生成方面的技术。几乎尝试了目前全部Kotlin代码自动生成的相关方案,也用到了Tranform这样的传统的字节码修改技术。 最后,作为一个贯穿于Android编译过程的工具链类项目,Rubik中有大量的,关于Gradle插件方面的实践,也封装了大量的Gradle方面的工具类。非常适合想要开发Gradle插件的小伙伴参考和借鉴。最重要的是,这一切都是基于Kotlin的。关于组件化
关于组件化开发,有很多不同的定义。在这里,定义并不重要,我们更应该关心,对于客户端开发而言,组件化开发所能解决的最核心的问题是什么。我们认为,组件化开发的核心,在于两点:- 隔离:让业务组件之间保持相对的独立性
- 复用:复用业务组件形成可运行软件
怎样才算彻底的组件隔离?
让业务组件之间保持独立性,通常可以与代码层面上的“解耦”划上等号,如果两个业务的代码混杂在一起,缺乏对边界的定义,肯定是代码隔离不彻底的表现。 代码隔离不彻底的情况 另外,对于组件化来说,业务隔离的标准还要更苛刻一些,当两个模块,在业务上的关联并不强时,如果存在直接的代码调用,也要视作代码隔离不彻底。由于编译顺序的关系,在Android平台,模块之间是无法建立双向的代码依赖的,这显然与组件化的理念有出入,我们希望组件之间是平等、独立的,应该可以自由的在任何组件之间建立松耦合的依赖关系。像这种基于代码调用的单向依赖,本质上一种“剪不断”的“从属关系”。 代码隔离不彻底的情况所以,在组件化开发中,组件之间应该保持没有代码耦合,在需要互相通信时,使用一种通过接口,间接的依赖的通信方式。 间接的依赖
在实际的组件化开发过程中,隔离往往是最难以实际操作的,无论是面对难以维护的历史代码,还是在前期规划不足的新业务。但是,一旦清晰的划分出两个组件间的边界,把彼此的代码隔离开,就会带来一系列的工程方面的好处:
- 明确开发人员的职责:由于对一个模块的修改不会直接影响另一个模块,所以负责开发不同模块的开发人员,只需要对约定好的接口负责,不需要关心其他组件的具体实现。
- 降低测试成本:同样的,对于测试人员而言,如果能够保证对一个组件的修改,只影响组件本身和组件的接口,那么对于没有被修改过的组件,就没有回归测试的必要了。
- 提升编译速度:在实际开发中,可以让大部分组件提前编译成二进制,只让少数经常变更的组件保持源码状态。
- 故障隔离:当一个组件出现故障,能够做到不影响其他组件的正常运行。
组件级别的复用应该做到什么程度?
组件级别的复用,指的是在不同应用程序之间,复用已有的业务组件。这往往都是为了快速搭建新项目,并且最大限度的保证新老项目的代码的一致性,从而降低代码的维护成本。所以,组件化开发框架,应该能够做到对现有的组件进行储备,并且在新项目启动时,通过简单的配置,对已有的组件进行筛选,快速的生成新的应用程序。 组件的筛选复用 在实际开发中,有可能会遇到同一个组件,给不同的应用复用时,存在一些功能差异的情况。所以,组件化框架也必须能够做到将同一套代码,差异化的编译成在功能上略有差异的组件变体,以便在不同需求下复用。 组件的变体复用Rubik在组件化中的作用?
Rubik由两部分组成,一部分是解决组件间低耦合通信的Rubik Router模块,另一部分是基于Gradle Plugin实现的Rubik工具链,负责解决组件管理和依赖管理方面的问题。Rubik Router:基于Kotlin DSL的“函数”路由
Rubik在依赖倒置与依赖注入的基础上,实现了一套基于Uri的路由通信方式,与一般的页面路由或四大组件路由不同,Rubik Router允许把Uri及参数,导航到组件内部任意的一个公开的Java或Kotlin函数的执行上。Rubik Router的选择以函数而非Android中的四大组件为路由的终点,主要基于三方面考虑:- 灵活性:在实际开发中,组件的边界通常不是简单的页面跳转,有可能是Api的调用或数据、实例的获取,相比于传统的页面路由,“函数”路由可以更加轻量级的满足这些需求。
- 可扩展性:“函数”路由有更低的层次,使用者可以在函数的基础上延伸更多的用法。
- 一致性:对于路由调用者而言,路由的终点无论是函数、页面还是数据,Rubik Router都提供一致的调用方式。
@RRoute(path = "user")
fun getUser(id : Int, name : String) : User? {
…
}
用元注解把函数注册到路由
navigate {
uri = "app://com.account/user"
query {
"id" with 400
"name" with "zhangsan01"
}
result<User?> { user ->// 通过泛型指定接收数据类型
…
}
}
通过Kotlin DSL调用其他组件提供的接口
Rubik Plugins:基于Gradle Plugin的组件管理和依赖管理工具
Rubik gradle plugins 提供了组件定义、版本控制、maven发布、二进制依赖与源码依赖切换等能力,包括4个gradle plugin:-
rubik:
-
提供全局定义组件的能力,并根据全局定义自动启用rubik-context、rubik-root等插件
rubik插件工程结构
rubik { component { uri "app://com.cloud-file" // uri 是组件的唯一 id,和路由根路径 dependencies {// 组件所依赖的其他组件uri uri ("app://com.local-file" ) uri ("app://com.upload" ) } source {// 定义的多种来源 project (":lib-cloud-file") maven { // 其他组件依赖自己的默认版本 version "0.2.0" variant "english-debug" } } } component { … }// 继续定义下一个组件 }
组件的定义方式
-
提供全局定义组件的能力,并根据全局定义自动启用rubik-context、rubik-root等插件
-
rubik-root:
- 给App工程提供筛选组件能力,根据flavor、版本号筛选要打包进apk的业务组件
- 提供组件的源码工程和aar切换的能力
rubik {
packing {
uri ("app://com.cloud-*") { // 筛选范围,uri表示用uri筛选,支持*匹配任意字符
projcetMode () // 筛选方式, projcetMode通过工程筛选一些组件
}
uri ("app://com.preview-file") {
mavenMode {// 筛选方式, mavenMode通过maven依赖aar筛选一些组件
version "0.2.0"
variant "netdisk-english-debug"
}
}
……
}
}
筛选组件的方式
-
rubik-context:
- 提供把业务代码按flavor、版本号编译成aar 并发布到maven的能力
- 提供辅助函数路由,把中间代码打包成context.jar ,并按版本号发布到maven的能力,并根据全局定义,为组件自动添加其他组件的中间代码依赖
-
rubik-test:
- 给工程提供单元测试环境
最后
希望Rubik能够帮助大家更便捷的实现组件化开发,也欢迎大家进行代码和技术层面的交流,如果你觉得我们做的还不错,请小伙伴们不要吝惜star、fork和watching: https://github.com/baidu/Rubik