给 Android Studio 项目添加持续交付功能

Android 和 CI/CD

通常正经的安卓项目构建都会经过一个持续集成的过程,相关的工具选择也不少,例如很知名的通用性很强的 Jenkins、Github 所支持的 Travis、Jetbrains 的 TeamCity 等等。持续集成的意义我不想描述,也不是本文的目的。
其实 Android 项目构建所用的 Gradle 在客户端已经可以算作一个 CI 工具了,但是 Gradle 缺乏 Continuous delivery (持续交付:CD) 的能力,想发布构建完成的 APK 还需要借助第三方工具或者纯手动进行,面对频繁、大量的构建,在不集成 CD 系统的情况下,这样做效率当然是极低的。

为什么不集成 CD 系统?

因为对于(非公司)个人项目而言,搭建 CD 系统或者集成进开放的成熟的 CD 环境,比较费力也没有很容易并且免费的渠道,而且往往没有那个必要。个人项目的构建频率小得多了,发布频率就更小了。
如果能改改配置,点个构建就自动发布到服务器或者资源 CDN 那就再好不过了。嗯,还可以发布的到各种 APP 分发平台(应用市场)。

通过 SSH

假设你的 APP 的升级是靠你自己服务器(我开发的 APP 都是如此)上的直链下载,那么就最好做了。用 SSH 上传是一种最廉价的方式,它完全可以轻松的集成到你的 Gradle 配置中,成为构建过程的一个 Task 。

有一个 id 为 org.hidetake:gradle-ssh-plugin 的 gradle 插件,它的说明为: “Deploy your App from Gradle” 。发布项目就是这么的容易,仅仅靠这个插件即可做到。gradle-ssh-plugin 顾名思义,它是一款 SSH 插件,可以让 Task 对目标主机执行命令以及传送文件(已经达到了所需要的所有基本功能)。
不过,在安卓上使用它没有那么简单。安卓的构建过程和项目结构都比一般的 Java(或者 Java 系) 项目复杂得多,并且 Gradle 的使用方式也有差异,所以按照文档上那样使用是不行的。正确的做法如下:

首先,在根 build.gradle 的 buildscript 代码块的 dependencies 中,加入:

buildscript {
              dependencies {
                  // Other classpath
                  classpath "org.hidetake:gradle-ssh-plugin:2.9.0"
              }
          }
          

然后在 app 模块的 build.gradle 中分别加入以下配置:

在配置末尾添加 apply plugin 和 remotes 代码块。

apply plugin: 'org.hidetake.ssh'
          
          remotes {
              webServer {
                  host = '<host>'
                  user = '<user>'
                  identity = file('<private_key_path>')
              }
          }
          

解释:添加一个开放 SSH 权限的主机,也可以用 password 代替 identity ,自行选择。

在 android -> productFlavors 代码块中添加 Task:

android {
              // Ohter config ...
              productFlavors {
                  task deploy {
                      doLast {
                          ssh.run {
                              session(remotes.webServer) {
                                  put from: "$buildDir/outputs/apk/release/${archivesBaseName}-release.apk", into: "/data/apk/"
                                  // put from: ... 
                              }
                          }
                      }
                  }
              }
              // Ohter config ...
          }
          

解释:定义任务 “deploy” ,执行内容为:传送构建完成的 release 版本 apk 文件到服务器的 /data/apk/ 目录。

在配置根(不被包含在任何 Block 中)中定义 tasks.whenTaskAdded 任务周期:

tasks.whenTaskAdded { task ->
              if (task.name.contains('assembleRelease')) {
                  task.dependsOn deploy
              }
          }
          

解释:把 assembleRelease 任务 dependsOn 到上面定义的 deploy 任务上,让 deploy 在 assembleRelease 后执行。

此时所有配置已经大功告成(尖括号中的值表示变量请自行替换)。执行 ./gradlew build 会在 release 版本 APK 构建完成之后自动发布到目标服务器的指定路径。

在我的服务器上,有一个 checkUpdate 的 API 会给所有 APP 使用。APP 检查更新时调用这个 API ,它会检索某个路径下的最新的 apk 文件并生成下载地址,所以只要我将新的 apk 发送到该路径即可完成新版本的发布。

当然,我自己的服务器那边是更加复杂的。我在 Task 中不仅仅发送了 APK 文件,还有 changelog 文件,还有一条 execute 命令(SSH 插件自带的功能,和 put 类似),它的作用是:将 apk 发送到跳板服务器上,然后执行跳板服务器的某个命令,跳板服务器去做更复杂的分发。至于 changelog 是给 checkUpdate API 读取更新日志用的,也就是说我的 APP 发布不需要动后台,在客户端(开发机)进行一次 release 版本的 Build 即可。

通过自定义发布 API

待更新。