使用 repo 组织 Android 工程

关于 Repo 的文章,我之前写过一篇:Repo VS Submodule,不过只是讲了 Repo 和 Submodule 的区别,以及 Repo 使用过程中的一些注意事项,然而在具体的项目里的所谓的最佳实践却并没有涉及。这篇文章,我想谈一下关于实践的内容,当然,是不是“最佳”实践,我也不敢妄言,每个人能有自己的理解,如果我能够抛砖引玉,引申出其他人更优的实践方案,那就很好了。

工程结构的变化

项目达到的一定的阶段,业务的组件化是必经之路,这一点毋庸置疑。如今,已经很少有项目还是一个子工程了,对于 Android 项目来讲,一般也都是基于 Gradle 的多 project 项目。Android Studio 默认创建项目,也是 multi-project 的形式,大致可能如下:

  1. 根目录下一个 setting.gradle 文件和 build.gradle 文件,setting.gradle 里配置此项目包含的子项目,build.gradle 里包含整个工程所需要的 gradle 插件和子项目依赖所需的仓库地址;
  2. 根目录下有一个或者多个子 module,这些子 module 就是整个工程的子项目,在子项目里,有自己独立的 build.gradle 文件,里面包含了此子项目所需的各种配置;

host、library1、library2、library3、library4 是工程里的子项目

今天我们要讲 repo,那如果把我们的工程结构进行 repo 改造之后,会是什么样呢?实际上,还是跟上面讲的一模一样。我们首先要明确 repo 只是一个版本控制管理工具,因此项目在形式上跟之前是没有区别的。但是从版本控制的角度讲,区别就大了。以前不管有多少个子项目,我们所有的代码都是在一个 git 仓库里,而 repo 改造之后,我们的宿主项目和其他所有的子项目都分别是一个独立的 git 仓库了。

从上面的目录树截图,我们可以看到,相对于我们常见的工程结构,在项目的 rootProject 同一级多了一个 .repo 目录,因此这里就出现了第一个实践经验:

  • 如果是一个 repo 工程的话,我们最好新建一个空目录,然后在这个空目录里 init 我们的 repo 工程。

在 repo 里,每个子项目都是一个独立的 git 仓库,而且所有的子项目都是平级的,不存在任何从属关系,只是对一个 App 来讲,子 module 项目需要依附于宿主 module 才能打包安装运行。因此,这里又出现了第二个实践经验:

  • 我们可以把工程的 rootProject 和宿主 module 合并起来作为一个 git 仓库,这也是我们打包运行 App 的最小单元。而其他的子 module,也完全可以像宿主 module 一样,单独导入 IDE 进行开发,编译打包成 aar。

repo 是通过 .repo/manifest/default.xml 文件来管理项目的,xml 里每个 project 节点代表一个项目,管理多少个项目,就有多少个 project 节点。

然而我们看到这个 xml 文件是在 .repo/manifest 目录的,而我们导入 IDE 的是 projects 这个目录,这样在 IDE 里我们就看不到 .repo 目录,这对我们管理子项目是非常不方便的,因此这里又出现一个实践经验:

  • 在主工程目录里创建一个 .repo/manifest/default.xml 的软链接,default.xml 初始化完毕之后(默认只有最小单元不被注释掉,其他所有的子 module 都会被注释掉),将其加入 .gitignore 里,这样我们本地开发就可以随意更改这个文件,同时也不会影响到其他人。

主 module 与子 module 的互动

我们拆分 module 的目的是什么呢?

  1. 厘清业务职责边界,避免模块间过多的相互影响;
  2. 业务模块独立开发编译打包,提高并行开发效率;
  3. 业务模块独立发布到 maven 上维护,宿主引用 maven 上的依赖,提高编译速度。

既然业务 module 要独立开发编译,那将这个模块作为一个单独的 git 仓库,一定是要比跟其他 module 杂糅到一起,效率要高很多,但是独立出来,打出来一个 aar,也并没有什么卵用,还需要上传到 maven 上,然后宿主 module 再去引用打包才可以。这就牵涉到主 module 与子 module 的互动问题,怎样互动效率才最高呢?基于 repo,我们就可以这样做:

  1. 将 repo 里打包运行 App 的最小单元(即 rootProject 和宿主 module)导入 IDE;
  2. 修改我们创建的软链接,如果我们要开发 library1 模块,那就将 default.xml 里 library1 这个模块的注释放开;
  3. 在 rootProject 目录里,执行 repo sync library1 即可检出 library1 的代码;
  4. 在宿主 module 里 implementation project(':library1') //compile project(':library1')

这样比起将 library1 单独导入到一个 IDE,然后宿主 module 通过 maven 依赖还有一个好处,就是调试起来会更方便了,无需每次修改一点代码,就要打包上传 maven。

这是日常业务开发层面的问题,但是我们只是解决了我们要开发的 module 与宿主 module 之间的互动问题,那那些不是我们当前要开发的模块怎么办呢?当然是直接使用 maven 上的了。但是这样显然就太麻烦了,每次都要手动改一堆东西,所幸这里我们可以通过 gradle 脚本来让整个过程自动化。

  1. 我们可以在宿主 module 的 gradle.properties 里列出来宿主 module 依赖的各个 module 打包出来的 aar 的版本号:

    1
    library1=1.0.0-SANPSHOT
    library2=1.0.0-SANPSHOT
    library3=1.0.0-SANPSHOT
    library4=1.0.0-SANPSHOT
  2. 在宿主的 build.gradle 里我们可以这样去声明依赖:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    def moduleVersions = new Properties()
    def moduleVersionsFile = new File("${project.projectDir}/gradle.properties")
    moduleVersions.load(moduleVersionsFile.newDataInputStream())

    dependencies {
    def projects = moduleVersions.stringPropertyNames()
    rootProject.subprojects.each {
    if (it.name != 'host') {
    implementation it
    projects.remove(it.name)
    }
    }
    projects.each {
    //为了方便引用,这里要求我们上传到 maven 的各模块的包遵循一定的规则,groupid 需要保持一致,
    // artifactid 最好是以子 module 的 project.name 来命名,如果不是这样的规则,就需要自己修改 gradle 脚本啦
    implementation "me.ailurus.repo:${it}:${moduleVersions.getProperty(it)}"
    }
    }

这样就保证了本地代码里没有子 module,可以自动引用 maven 上的 module 依赖。

工程动态添加子项目

其实关于 repo 的实践,上面的部分基本已经讲完了,不过既然是实践经验,在这样的工程结构,附带的还有一点可以优化。

我们都知道,在 gradle 工程里,由于 gradle 的生命周期的存在,任何一个子项目的编译,都要经过所有子项目的 configuration 阶段,如果有什么依赖的 maven 坐标定位缓慢或者依赖本身下载缓慢,这个阶段将会非常耗时。因此要解决这个问题,最行之有效的手段就是减少子项目数量。那么问题又来了,我们总不能每次都手动去注释 setting.gradle 的 include 的子项目吧。

当然不!有了 repo,这个问题就简单了,我们可以通过脚本,去解析我们放在 rootProject 目录下的 xml 软链接文件,读取到的 project 节点就是我们要 include 的子项目(当然,最小运行单元最好还是单独 include 进来)。

1
2
3
4
5
6
7
8
9
10
include ':host'//最小运行单元

def manifest = new XmlParser().parse("${rootProject.projectDir}/default.xml")
manifest.project.each {
it.attributes().each { k, v ->
if (k == 'path' && !v.contains('host')) {
include v.replace('/', ':') - rootProject.name
}
}
}

这就是 gradle 工程动态添加子项目。

这里可以下载代码演示查看。

结语

以上就是关于 repo 在 Android 工程里全部的实践经验。我们可以看到,这完全不是从无到有的颠覆性的开发方式创新,而是针对现有工程的简单改造优化,而这些简单的优化带来的收益是非常显著的。当然,这也只是一家之言,也欢迎有兴趣的朋友多多尝试,以寻求更优更好的开发方式。