作者|个推高级安卓研发工程师 萨其拉
在个推SDK 开发过程中,通常而言一个完整的开发上线流程是这样的:提出需求-> 需求开发 -> 打包测试 -> 问题修复 -> 打包交付测试验证 -> 测试通过 -> 产品上线。
在这个流程中,不难发现打包是一项重复性劳动,而且我们常常会重复经历问题修复阶段,也就意味着我们在一个开发上线周期内需要进行多个重复性的打包过程。如果能优化这个环节,那么开发效率就能得到显著提升。
除此之外,手动打包往往还容易引入一些风险,比如说打包环境不一致、签名文件泄漏、效率低、包的溯源管理困难等等,所以我们需要一套持续集成的方案来减少这种重复性劳动以及规范打包流程。Jenkins作为持续集成的方案,拥有丰富的插件支持,以下我们将为大家介绍个推基于Jenkins的构建实践经验。
Jenkins 是一款开源的 CI&CD 软件,用于自动化各种开发任务,包括构建、测试和部署软件等。Jenkins 支持多种运行方式,比如通过系统包、Docker 或者独立的 Java 程序的运行。关于 Jenkins 构建 Android 实践,可以参考小编写的这篇教程 《使用 Jenkins 构建 Android 应用》:
https://jenkins-zh.cn/tutoria...
开发者可以通过选择流水线的方式来创建一个 PipeLine 构建任务,如下:
通过创建 Jenkinsfile 进行版本控制,如下:
Jenkins Pipeline 允许你像写代码一样管理你的构建脚本。我们通常可以通过创建 Jenkinsfile 控制版本从而更好地进行功能的更新迭代。一个声明式流水线语法的基本结构如下:
pipeline { agent any stages { stage('Build') { steps { // } } stage('Test') { steps { // } } stage('Deploy') { steps { // } } } }
pipeline 和agent分别是声明式流水线的两种特定语法,后者为流水线指定了一个特定的工作区。stages 由多个不同的stage组成,stage可以展示在 stage view 中,用来表示构建环节中的不同阶段。steps我们可以理解成每个构建阶段中包含的不同步骤。通常我们的构建指令可以在这里编写。
接下来本文将介绍关于个推使用 Jenkins 持续集成方案的实践(本文基于 Jenkins 2.19,使用 Jenkins Pipeline 构建)。
以个推的实践经验来分析,在打包过程中,我们最好能有一种控制代码风险的机制,这种机制能够自动化地控制有关风险代码进入打包环节,并中断打包流程,给出风险警示。另外,中大型项目的模块依赖往往比较复杂,我们应提供一种按需依赖组合的方式。研发人员还可以引入一些自动化测试,保证代码的质量。综上所述,一个打包方案的需求分析如下:
根据上述需求的分析,一个完整的持续集成流程图如下所示:
首先,进入准备阶段(prepare), 在该阶段会进行打包前的准备工作,比如环境准备等。接着是解析编译打包相关参数(parse jenkinsConfig), 用于之后的打包构建,包括模块依赖组合的支持等。
然后是拉取相关打包代码(checkout code)阶段。随后进入代码检测(Android Lint)阶段和单元测试阶段(Unit Testing)。通过前面的单元测试以及代码检测之后就可以进入我们的编译 SDK 阶段(Build SDK)了, 在该阶段我们可以配合个推的预编译插件以及相关的打包环境变量参数,选择相对应的模块组合或者选择对应的功能代码进行按需编译。
编译完成后,我们需要对相关的输出产物(JAR/AAR)进行格式检测(CheckJar 阶段)。待检测通过就可以进行真机模拟测试了(Automated Testing 阶段,可选),随后就可以输出构建产物(PrintSDK 阶段)并发送邮件通知相关构建人员(Email Notification 阶段)。至此,一次完整的打包流程已经结束。
上文介绍了打包构建方案的流程,但是在实践中,我们会发现随着构建的任务越来越多,构建的环境会变得越来越繁杂,难以管理。
痛点分析:当我们依赖于宿主机构建 Android 时,不同的项目有着不同的 gradle 环境。随着 gradle 的升级、项目的迭代,当不同的构建项目任务数量越来越庞大时,宿主机的 gradle 环境就越容易出现污染。另外,由于构建的环境依赖宿主机的编译环境,一旦宿主机的编译环境发生变化,就很容易对构建项目产生影响。
为解决这个问题,我们可以将不同的 Android gradle 构建环境放进一个 Docker 容器中。Docker 是一个开源的应用容器引擎,可以实现虚拟化,也可以由开发者打包应用或者依赖包到一个轻量级、可移植的容器,然后发布至不同的机器上。容器完全使用沙箱机制,相互之间不会有任何接口(类似 iPhone 的 App), 更重要的是容器性能开销极低。
在每次编译构建时我们都依赖于一个 Docker 容器的环境,通过这种方式将任务之间的编译环境以及与宿主机之间通过 Docker 容器隔离开来,这样可以保证宿主机的环境变化对编译任务不产生影响,保证构建环境的干净。再之后随着 gradle 版本升级,我们需要升级并发布对应的Docker镜像版本,以兼容高版本的编译环境、做相应的环境版本管理。
Jenkins Pipeline 提供了使用 Docker 镜像作为构建环境的功能,代码如下:
pipeline { agent { docker { image 'allbears/jenkins-android:1.0.1' } } }
维护和扩展
使用 Pipeline 构建时,开发者可以通过维护 Jenkinsfile对打包功能进行版本管理。这种构建方式更方便,开发者可以自由地回归历史版本进行打包。Jenkinsfile 的大致结构模板如下。
pipeline{ agent { docker { image 'allbears/jenkins-android:1.0.1' //指定构建环境 } } stages { stage('Prepare'){ steps { echo "构建前准备" } } stage('Parse Jenkins Config'){ steps { echo "Jenkins 构建参数解析" } } stage('Checkout Code'){ steps { echo "构建代码检出" } } stage('Android Lint'){ steps { echo "代码静态检测" } } stage('Unit Testing') { steps { echo "单元测试" } } stage('Clean') { steps { echo "编译前环境初始化" } } stage('Build SDK') { steps { echo "构建 SDK" } } stage('Check JAR') { steps { echo "Jar 包合规性分析" } } stage('Automated Testing') { steps { echo "自动化测试" } } stage('Print SDK') { steps { echo "构建产物归档" } } stage('Email Notification') { steps { echo "邮件通知" } } } }
从上述的 Jenkinsfile 中我们不难得知,每个流水线会由不同的 stage 组成,而每一个 Stage 则可以作为一个独立的小功能模块。开发者可以通过将不同的 stage 进行排列组合来进行相应的扩展。
人工构建过程繁琐、操作耗时,还容易在手动打包过程中引入一些风险。个推使用 Jenkins 自动化构建免去了繁琐的人工操作过程,充分解放了研发人员的生产力。此外,个推使用Pipeline方式“持续集成”构建,让研发人员管理、迭代构建工程更加方便。接下来,个推还会在“持续集成”这个领域深入拓展,向开发者们分享最新的实践方案。