Android 多源码仓库的依赖库版本统一管理方案

8,606 阅读7分钟

1 背景

目前,随着 Android 模块化、插件化和组件化等 App 架构设计的普及,项目中各个插件或组件通常会部署在不同的源码仓库中,这些仓库共同引用的依赖库版本的升级维护成本随之变高。当升级某个依赖库的版本号时,壳工程及相关的插件或组件源码仓库中的版本号都要同步修改,维护成本高。为了解决这个问题,本文将循序渐进地介绍一种 Android 多源码仓库的依赖库版本统一管理方案。

2 单一源码仓库的依赖库版本统一管理

在介绍多仓库依赖库版本统一管理方案前,这里先简单介绍下目前通用的单一仓库多个 module 的版本号统一管理配置方法。

首先,在 Project 根目录下创建一个依赖库版本的 config.gradle 配置文件,内容类似如下:

ext {
  depsVersion = [
    supportV7_androidx: '1.1.0',
    design_androidx: '1.1.0',
    recyclerview_androidx: '1.1.0',
    kotlin: '1.3.72',
    uilib: '2.38.0'
  ]
  depsLibs = [
    design_androidx: "com.google.android.material:material:${depsVersion.design_androidx}",
    recyclerview_androidx: "androidx.recyclerview:recyclerview:${depsVersion.recyclerview_androidx}",
    supportV7_androidx: "androidx.appcompat:appcompat:${depsVersion.supportV7_androidx}",
    gson: "com.google.code.gson:gson:${depsVersion.gson}",
    kotlin_gradle_plugin: "org.jetbrains.kotlin:kotlin-gradle-plugin:${depsVersion.kotlin}",
    kotlin_android_extensions: "org.jetbrains.kotlin:kotlin-android-extensions:${depsVersion.kotlin}",
    kotlin_stdlib: "org.jetbrains.kotlin:kotlin-stdlib-jdk8:${depsVersion.kotlin}",
    uilib: "com.bk:uilib:${depsVersion.uilib}@aar"
  ]
}

其中 depsVersion 标签内声明了各个依赖库的版本号,depsLibs 标签中声明了各个依赖库的常量。

然后,在 Project 顶层的 build.gradle 文件中通过 apply from: "config.gradle" 引用这个配置文件:

buildscript {
    apply from: "config.gradle"

    repositories {
        maven {
            jcenter()
        }
     ....

最后,我们就可以在各个子 module 中通过如下方式统一依赖 config.gradle 配置文件中定义的依赖库了:

dependencies {
  implementation rootProject.ext.depsLibs.supportV7_androidx
  implementation rootProject.ext.depsLibs.recyclerview_androidx
  implementation rootProject.ext.depsLibs.kotlin_stdlib
  implementation rootProject.ext.depsLibs.gson
  ...

将来如果某个依赖库需要升级,只需要统一修改 config.gradle 文件即可,不必在每个module 中逐一进行修改。这在单一源码仓库内的实现可以说是完美的。

3 多源码仓库的依赖库版本维护存在的问题

但随着项目复杂度提高,插件化,模块化和组件化等框架设计通常会将 App 工程分层划分为多个仓库。如下图所示是一种通用的架构设计:

app框架设计

每个组件或插件库以及基础库都是独立的源码仓库,这些仓库与壳工程同时引用了很多的依赖库。每个仓库都维护着一份 config.gradle,依赖库的版本号更新就成了一个头疼的问题。每当升级某个库的版本号时,需要同时修改壳工程,各个插件、组件和基础库源码仓库中的 config.gradle 配置。

这带来的弊端是:

  • 版本号维护复杂,每次升级要涉及多个源码仓库的配置文件修改

  • 人工修改容易出错,出现漏改,导致各仓库间依赖库版本出现不一致,进而引起编译问题、功能异常和崩溃问题,如 NoSuchMethodError, VerifyError 等

4 如何统一多仓库的版本配置文件

壳工程是真正集成编译依赖库的源码仓库,我们要做的就是希望其他所有的仓库都能够去引用壳工程的 config.gradle 配置。该如何将多个仓库的 config.gradle 依赖库配置文件收敛到一个文件内呢?

最初想到的一种方法是,非壳工程仓库通过相对路径的方式引用壳工程中的 config.gradle 文件,比如各个仓库都放在同一个目录内,可以通过下面方式来引用:

buildscript {
   apply from: "../ke_main_project/config.gradle"
   ....

这么做的确实现了所有仓库共用同一个 config.gradle 配置,但这种本地的依赖方式存在一个严重问题。当壳工程与其他仓库不在同一个版本迭代分支上时,会出现编译失败,依赖版本号不匹配的问题。比如壳工程当前处于1.0版本的开发分支,当其他仓库的当前分支处于2.0版本的分支上时,如果壳工程在2.0分支上的 config.gradle 配置文件有更新,其他仓库可能引用不到新增的依赖库或引用了错误的旧版本的依赖库。除此以外,还有多个仓库需要在同一目录下的限制,这种本地依赖的方式一不安全,二不灵活。我们进而转向考虑采用远程依赖的方式。

值得一提的是,gradle 脚本的 “apply from” 的参数不仅可以是本地文件系统的脚本,还可以指定远程位置的脚本应用,即 HTTP URL 的远端脚本文件。那我们是不是可以将 config.gradle 配置文件上云呢?类似下图设计,非壳工程的其他仓库统一远程依赖壳工程的依赖库版本配置文件。

配置文件依赖关系

答案是肯定的,远程依赖的确可以解决本地依赖存在的问题。经过进一步调研,我们发现目前很多常用的代码托管平台提供的 API 支持通过网络请求获取代码内的某个文件,并且借助 git 的版本管理能力,我们可以指定获取远端具体某个分支的配置文件,不需要额外搭建服务器存储配置文件。

5 多个源码仓库的依赖库版本统一管理方案实现

下面,我们将详细介绍两个常用的代码托管平台 gitlab 和 gerrit 实现多仓库的依赖库版本统一配置方案。至于其他的代码托管平台的实现方式也是大同小异,本文不再赘述。

5.1 gitlab

首先,你需要申请一个 Personal Access Tokens 才可以访问接口,申请路径在 gitlab 个人的设置页面内:

gitlab_token

gitlab 获取原始文件的接口可以参考链接 docs.gitlab.com/ee/api/repo…

GET /projects/:id/repository/files/:file_path/raw

参数说明:

  • id 参数:是 project 的 id

  • file_path (必须) - 是Project内文件的相对路径,需要对文件进行 URLEncode 编码

  • ref (必须) - branch, tag or commit 的名字,需要 URLEncode 编码

其中,project 的 id 是数字类型,需要通过请求查询接口来确认下壳工程的 project id,可以通过 curl 命令快速进行查询:

curl --header "PRIVATE-TOKEN: {你的 access token}" "{你的gitlab域名}/api/v4/search?scope=projects&search={壳主工程的名字}"

返回结果是个json,如下面 id 字段就是 project id。

[
  {
    "id": 12345,
    "description": "Nobis sed ipsam vero quod cupiditate veritatis hic.",
    "name": "Flight",
    "name_with_namespace": "Twitter / Flight",
    "path": "flight",
    "path_with_namespace": "twitter/flight",
    "created_at": "2017-09-05T07:58:01.621Z",
    "default_branch": "master",
    "tag_list":[],
    "ssh_url_to_repo": "ssh://jarka@localhost:2222/twitter/flight.git",
    "http_url_to_repo": "http://localhost:3000/twitter/flight.git",
    "web_url": "http://localhost:3000/twitter/flight",
    "avatar_url": null,
    "star_count": 0,
    "forks_count": 0,
    "last_activity_at": "2018-01-31T09:56:30.902Z"
  }
]

在非壳工程的其他仓库根目录下的 build.gradle 脚本中的实现方式有两种。

第一种,token 需要做为header 参数请求接口,先下载壳工程的配置文件到本地,apply from引用后,删除该文件:

buildscript {
  //引用壳工程的依赖库版本配置
  def HOST_BRANCH = 'feature-1.0.0'
  def HOST_PROJECT_ID = '{壳工程project id}'
  def REF = URLEncoder.encode("$HOST_BRANCH", 'UTF-8')
  URL configFileUrl = new URL(
      "{你的的gitlab域名}/api/v4/projects/$HOST_PROJECT_ID/repository/files/config.gradle/raw?ref=$REF")
  URLConnection urlConnection = configFileUrl.openConnection()
  urlConnection.setRequestProperty("PRIVATE-TOKEN", "{你的 access token}")
  urlConnection.inputStream.withStream {
    file("config.gradle").bytes = it.bytes
  }
  apply from: "config.gradle"
  delete "config.gradle"
  ...

第二种,实现更简单,推荐使用该方法。gitlab 接口文档中未提及 token 其实还可以做为 query 参数请求接口,这样我们可以直接 apply from 文件链接的方式实现远程依赖。

buildscript {
    //引用壳工程的依赖库版本配置
    def HOST_BRANCH = 'feature-1.0.0'
    def HOST_PROJECT_ID = '{壳工程project id}'
    def REF = URLEncoder.encode("$HOST_BRANCH", 'UTF-8')
    apply from: "{你的gitlab域名}/api/v4/projects/$HOST_PROJECT_ID/repository/files/config.gradle/raw?ref=$REF&private_token={你的 access token}"
    ...

5.2 gerrit

gerrit 获取仓库文件的接口链接可参考 gerrit-review.googlesource.com/Documentati…

'GET /projects/{project-name}/branches/{branch-id}/files/{file-id}/content'

参数说明:

  • project-name: 项目名
  • branch-id: 分支名
  • file-id: 文件路径

这三个参数都需要进行 URLEncode 编译。

同样,gerrit的接口请求也是需要 token 的,我们可以在 gerrit 的个人设置页面生成,如下图:

gerrit token

生成的 http password 复制出来,不要再重新点击生成,不然会导致之前的失效。

最后,非壳工程的其他仓库的 build.gradle 脚本的代码实现方式如下:

buildscript {
  	//引用壳工程的依赖库版本配置
    def HOST_BRANCH = 'feature-1.0.0'
    def REF = URLEncoder.encode("$HOST_BRANCH", 'UTF-8')
    def MAIN_PROJECT_NAME = URLEncoder.encode("{你的壳工程的 Project Name}", 'UTF-8')
    def pwd = "{你的 gerrrit username}:{你生成的http password}".getBytes("UTF-8")
    String token = Base64.getEncoder().encodeToString(pwd)
    URL configFileUrl = new URL(
        "{你的gerrit域名}/a/projects/$MAIN_PROJECT_NAME/branches/$REF/files/config.gradle/content")
    URLConnection urlConnection = configFileUrl.openConnection()
    urlConnection.setRequestProperty("Authorization", "Basic " + token)
    urlConnection.inputStream.withStream {
        file("config.gradle").bytes = Base64.getDecoder().decode(it.bytes)
    }
    apply from: "config.gradle"
    delete "config.gradle"
    ...

另外,每个迭代版本分支都需要修改一下 HOST_BRANCH 变量,指定获取壳工程对应分支的依赖库版本配置文件显然比较麻烦。建议所有仓库的每个迭代版本都使用一致的分支名,这样 HOST_BRANCH 变量可以通过 git 命令动态获取当前仓库的分支名,不用每次修改:

def HOST_BRANCH = 'git symbolic-ref --short -q HEAD'.execute().text.trim()

通过以上实现,每次组件库或基础库在编译时,都会统一远程依赖壳工程的依赖库版本配置文件,当涉及某个依赖库版本升级时,也只需要修改壳工程内的一个配置文件即可,完美实现了多个源码仓库的依赖库版本统一管理。

6 小结

不需要额外搭建服务器,仅借助代码托管平台的 api 接口和版本管理能力,本文实现了一种 Android 多源码仓库的依赖库版本统一管理方案,将多仓库的依赖库版本配置文件收敛成一个,不仅减轻了维护成本,而且提高了 App 稳定性。希望能够给大家的项目开发带来一些帮助。