Android的持续化集成及多版本打包
文档概述
关于Android开发,除了技术方面需要掌握,还有发布流程需要了解。本文档就包括以上两个方面,主要介绍:
- 使用配置文件配置不同功能的apk
- 使用gradle为Android构建签名包
- Jenkins集成Android自动化打包
- 使用gradle为Android生成不同配置的签名包
一、场景描述
在项目开发中,我们可能有多个功能有区别但是整体框架一致的工程。
情景:我们有一app,基础功能包括a,b,c,扩张功能包括d,e,f,其中客户1需要扩展功能d,f,客户2需要e,f。
二、配置文件简介
鉴于以上场景,开发app过程应该怎么做?如果客户1创建一份工程代码,客户2创建一份工程代码效率就太低了。因此我们需要另辟思路,采用配置文件的方式进行开发。
配置文件分类
关于配置文件目前有两种方式:
- apk文件写注释
- apk源码属性文件
配置方式的区别
方式1主要适用于轻量的注释,例如书写渠道名等一些简单的注释,不用修改源码。方式2就比较适合当前的场景,但是缺点在于配置文件在源码部分,所以修改配置文件就必须修改源码。
配置文件的使用
关于方式1的使用,可以: 美团批量打包
基本原理就是在apk文件生成之后,修改apk文件的部分字段,而不影响apk本身签名验证,在源码中根据apk安装的位置获取安装包文件再读取其中的字段。
方式2就是使用属性文件。实现方式就是使用java.util.Properties类进行文件加载。
预先在Android工程的main
文件夹下创建assets
文件夹,里面存放配置文件,客户1的配置文件命名为:a.properties
,b.properties
,里面的内容如下:
fun_a=true
fun_b=true
fun_c=true
fun_d=true
fun_e=false
fun_f=true
读取配置属性代码:
public String funX(Context context, String x){
Properties props = new Properties();
try {
props.load(context.getAssets().open("a.properties"));
// props.load(context.getAssets().open("b.properties"));
} catch (IOException e) {
e.printStackTrace();
}
return props.getProperties("fun_" + x);
}
在源码中根据对应函数的配置信息决定是否执行对应操作。
对于客户1和客户2的不同需求可以选择性的加载配置文件进行打包操作。
以上就是不同需求的具体操作。但是我们可以发现,这个操作效率太低,每次打包都要修改源码。下面介绍自动化过程。
三、 自动化之gradle打包
配置操作
为了之后可以执行自动化操作,签名必须也要能进行自动化执行。
首先放置签名文件到:项目的根目录。
在module
的build.gradle
文件中添加以下内容:
apply plugin: 'com.android.application'
def keystorePSW = ''
def keystoreAlias = ''
def keystoreAliasPSW = ''
// default keystore file, PLZ config file path in local.properties
Properties properties = new Properties()
// local.properties file in the root director
properties.load(project.rootProject.file('gradle.properties').newDataInputStream())
keystorePSW = properties.getProperty("keystore.password")
keystoreAlias = properties.getProperty("keystore.alias")
keystoreAliasPSW = properties.getProperty("keystore.alias_password")
android {
...
signingConfigs {
release {
keyAlias keystoreAlias
keyPassword keystoreAliasPSW
storePassword keystorePSW
storeFile file('../xxx.jks') // 签名文件的位置
}
}
buildTypes {
release {
minifyEnabled false
zipAlignEnabled true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
signingConfig signingConfigs.release
}
}
}
dependencies {
...
}
为了能够在Jenkins环境也能使用自动打包操作,在项目的根目录gradle.properties
文件中添加如下内容:
keystore.password = xxx // 密钥的密码
keystore.alias = xxx // 密钥的别称
keystore.alias_password = xxx // 别称的密码
随后在项目的根目录下使用gradle即可进行打包。
执行命令
打包命令:
gradlew clean // 清除build文件夹
// 二选一
gradlew build // 检查依赖并编译打包,会生成debug和release两个包
gradlew assesmRelease // 生成release包
执行以上命令之后,会在项目的app/build/output/apk/*.apk
生成对应的apk。
注:Windows操作系统使用gradlew
,Linux系统使用./gradlew
。
以上,使用gradle自动打包就以完成。下面就是集成Jenkins持续集成环境了。
四、Jenkins集成
创建项目
关于构建介绍可以参考:Jenkins+Gradle实现android开发持续集成、打包
配置信息没什么特别的。主要是输出文件路径:直接填写app/build/output/app/*.apk
即可。
可能遇到的问题
-
由于Jenkins自动构建,所以对语法要求比较严格。如果出现以下错误:
FAILURE: Build failed with an exception. * What went wrong: Execution failed for task ':app:lint'. > Lint found errors in the project; aborting build. Fix the issues identified by lint, or add the following to your build script to proceed with errors: ... android { lintOptions { abortOnError false } } ... * Try: Run with --stacktrace option to get the stack trace. Run with --info or --debug option to get more log output.
这个是因为代码不符合规范,lint检查时报错,因此中断了整个编译过程。
只要在当前app的
app/build.gradle
文件内增加如下代码:android{ ... lintOptions{ abortOnError false } ... }
-
安装Jenkins配置局域网访问
在mac安装Jenkins之后,局域网无法访问,原因在于使用brew安装jenkins会避免很多其他安装方式产生的用户权限问题,但是会将httpListenAddress默认设置为127.0.0.1,这样我们虽然可以在本地用localhost:8080访问,但是本机和局域网均无法用ip访问。解决办法为修改两个路径下的plist配置。
~/Library/LaunchAgents/homebrew.mxcl.jenkins.plist /usr/local/opt/jenkins/homebrew.mxcl.jenkins.plist
修改之后重启Jenkins即可访问。
brew services start jenkins // 开启服务 brew services stop jenkins // 关闭服务 brew services restart jenkins // 重启服务
-
更多错误查看:
通过以上操作,Android项目就可以使用Jenkins自动集成了。
但是我们第一部分的场景问题还是没有解决,如何自动化区分客户的版本呢?
五、自动化版本区分
Android官网介绍了 构建变体
通过配置不同的 productFlavors
我们可以获取不同版本的apk。
因此第一部分的需求通过以下操作实现。
更新buidl.gradle
文件
android {
...
buildTypes {
...
}
productFlavors {
fun_a {
buildConfigField "String", "CONF_NAME", "\"a.properties\""
}
fun_b {
buildConfigField "String", "CONF_NAME", "\"b.properties\""
}
}
}
- 注1:字段解释参考 GRADLE自定义你的BUILDCONFIG
- 注2:使用
String
属性时候,值需要使用\"
进行转义
修改读取配置文件代码
配置属性文件不变,读取的代码进行如下修改:
public String funX(Context contex, tString x){
Properties props = new Properties();
try {
props.load(context.getAssets().open(BuildConfig.CONF_NAME));
} catch (IOException e) {
e.printStackTrace();
}
return props.getProperties("fun_" + x);
}
之后使用打包命令就会生成两个apk安装包,一个是客户1定制的功能,一个是客户2定制的功能。
输出路径不变,生成包名:
- app-fun_a-releas.apk
- app-fun_b-releas.apk
这样,Jenkins一次编译也就可以获取到正确的版本。
修改输出文件的包名
以上操作之后已经可以正确获取目标apk,但是并不直观。如果可以在输出文件名中添加输出版本号和打包时间就完美了。
在项目的build.gradle
文件中,最后节点添加:
def releaseTime() {
return new Date().format("yyyyMMdd-HHmm", TimeZone.getTimeZone("GMT+08:00"))
}
android{
applicationVariants.all { variant ->
variant.outputs.each { output ->
def outputFile = output.outputFile
if (variant.buildType.name.equals('release')) {
def fileName = outputFile.name.replace("app-","").replace("release", "v${defaultConfig.versionName}-${releaseTime()}")
output.outputFile = new File(outputFile.parent, fileName)
}
}
}
}
修改之后输出文件名:
fun_a-v2.0.5-20171115-1655.apk
fun_b-v2.0.5-20171115-1655.apk
以上。
六、小结
通过使用gradle+Jenkins,可以让程序员从繁复的打包任务中解放出来,更多时间去做核心开发的相关业务。
gradle的功能强大到我没法想象,好好学习,好好钻研。
技术,可以解放你。