转载请注明出处: https://blog.csdn.net/anyfive/article/details/87645007
18年中,由于各种原因,换了一份工作,加入了现在这家公司,负责终端的业务开发。这半年时间有一些印象比较深刻的点,趁着空档记录下来。
解耦单个模块
我们的项目采用的是多module的组织形式,SDK层与业务层在同个项目里,其中业务层又分为多个app模块。
在入职前,同事抽离了一个Component模块,希望复用多个产品中通用的代码,但随着业务的推移,这个模块很快就陷入了耦合地狱之中了,只是由于业务的进度压力,一直未正视问题。
后来在一次打包时,另一个产品的代码不断报错,导致打包不断失败。也就是这次事件,让我感受到了解耦的刻不容缓。于是第二天,花了一天的时间将代码分离,通过设置sourceSets,实现各部分的编译期隔离。之后,又陆陆续续加入了同级依赖(同module不同部分的依赖)等功能,实现起来其实很简单:
-
module的build.gradle文件的末尾加入以下代码:
// 引入各config applyComponents(new ArrayList<String>(), rootProject.ext.Components) /** * 递归依赖所有需要的模块 * @param appliedComponents * @param childComponents * @return */ ArrayList<String> applyComponents(ArrayList<String> appliedComponents, ArrayList<String> childComponents) { for (String it : childComponents) { if (appliedComponents.indexOf(it) >= 0) continue apply from: "${it}/pin.gradle" appliedComponents.add(it) if (ext.has("componentDep") && ext.componentDep != null) { appliedComponents = applyComponents(appliedComponents, ext.componentDep) } ext.componentDep = null } return appliedComponents }
如代码所示,从rootProject.ext.Components中获得要依赖的模块,apply其模块下的pin.gradle文件。
-
在module的build.gradle的android中,加入sourceSets设置,清空默认的源码路径:
android { sourceSets { main.java.srcDirs = [] main.res.srcDirs = [] main.assets.srcDirs = [] main.renderscript.srcDirs = [] main.jniLibs.srcDirs = [] main.manifest.srcFile "AndroidManifest.xml" } }
-
在各子模块目录下添加pin.gradle,该文件用于声明源码路径和依赖及其他内容:
// 源码目录 def dir = "${projectDir}/ComponentA" android { sourceSets { main.java.srcDirs += "${dir}/java" main.res.srcDirs += "${dir}/res" main.assets.srcDirs += "${dir}/assets" main.renderscript.srcDirs += "${dir}/rs" } } // 依赖 dependencies { implementation "com.android.support:appcompat-v7:28.0.0" } // 同级依赖 ext.componentDep = ["ComponentB"]
通过sourceSets指定该模块的源码路径,通过dependencies添加该模块需要的依赖。
-
在app或者根项目的build.gradle中声明需要依赖的模块:
rootProject.ext.Components = ["ComponentA"]
这样,就达到了代码的编译期隔离效果,如图:
从图中可以看到: 由于在app中声明了ComponentA,因此Component模块中,ComponentA被编译了;ComponentA中,又声明了ComponentB的同级依赖,因此ComponentB也被编译了;而ComponentC未被声明依赖,因此,ComponentC目录下的代码都不会参与编译。
四层+pins架构
在业务稳定下来之后,为保证之后可以更加快速地响应需求、解决问题,便又开始动起了架构调整的心思。于是,便延续着Components模块的风格,将整个项目的代码分成了四层:
- app层,即实际的业务项目层,目前有三个app层,通过settings.gradle和gradle.properties,实现项目的快速切换;
- Components层,一些至少两个项目通用的部分,包含了较强的独特性;
- Commons层,与公司其他业务或平台相关的部分,如日志系统等;
- Base层,通用的、不与公司任何业务相关的部分,如相机、网络等;
其中,Components、Commons、Base三层均使用pins的方式进行拆分,每个模块都进行了封装,并拥有自己的接入文档,保证最快速度的接入。
另外,这三层允许向下依赖和同级依赖,不允许向上依赖。为保证编译顺序,实现向下依赖,另编写了Pins.gradle,以Commons的Pins.gradle文件为例:
// 初始化Commons数组
if (!rootProject.ext.has("Commons")) {
rootProject.ext.Commons = []
}
// 获得所有module的名称
import java.util.function.BiConsumer
ArrayList<String> projectNames = new ArrayList<String>()
rootProject.childProjects.forEach(new BiConsumer<String, Project>() {
@Override
void accept(String s, Project project) {
projectNames.add(s)
}
})
// 如果Commons module参与了编译,则将其编译顺序挪到除Base之外最后的位置
allprojects {
if (it == rootProject) return
if (projectNames.contains("如果Commons") && it.name != "Base" && it.name != "Commons") {
project(":Commons").evaluationDependsOn(":${it.name}")
return
}
}
然后,在根项目的build.gradle中,依次apply这三层的Pins.gradle:
apply from: "Components/Pins.gradle"
apply from: "Commons/Pins.gradle"
apply from: "Base/Pins.gradle"
这样一来,就可以保证编译顺序,顺利实现向下依赖,最终的架构如下图:
如此,便可通过配置依赖,尽量复用代码,快速完成新项目的开发;同时,出现问题时也更加容易定位和解决。
gradle脚本替代git-tag
我们项目的代码托管,使用submodule的形式,每个app是一个submodule,每层也是单独的submodule。每次发布版本时,都需要在release分支上打tag。架构调整后,由于底下三层逐渐趋于稳定,导致同一commit中,逐渐出现大量tag。
于是决定,不再打tag,而是通过在打包时,记录每个submodule的commitId用以代替tag,代码与之前写过 android 打造自己的gradle构建脚本(以集成Tinker为例)的类似,在此不再累赘。
演进思考
在项目的调整过程中,每一次模块的拆分,其实都有不少的工作量。每次抽离出一个模块,都需要对其进行封装,保证其可以快速地被接入、方便地扩展,如Camera模块,基本算是重写了一遍。但值得高兴的是,最后的结果达到了我们的预期,对后续的工作,带来了不小的帮助。
若是采用jenkins+maven的方式,跨层的依赖也会变得更加方便和规范;但我们希望,每一个参与项目的人,都可以对每一层的代码尽可能熟悉,且都可以方便地参与到模块的完善中来,因此,在架构调整到可以满足业务的需求和适应可预见的变化的这一步时,也就可以先放一放,把精力放到其他更有价值的事情上。
虽然本篇文章的代码并没有什么难度,也没有什么黑科技,但我觉得,这次经历是一次有意义的事,因此在此记录。
没有最好的架构,只有最适合业务需求的架构。