Android 利用Gradle实现app的环境分离

环境分离

有过互联网软件开发经验的朋友一定对于测试环境和生产环境这两个词很是熟悉,一般软件开发阶段都是在测试环境(比较常用的是内网环境)上运行调试,而正式打包发布时会配置生产环境(也称之为线上环境)的服务器,也就是不同的接口URL和数据库的区别。在开发和测试阶段,我们常常需要在同一个设备上同时安装着两套甚至多套环境的同一个应用,便于观察调试。

但对于Android App来讲,相同包名的apk在同一个设备上只能存在一个。所以我们无法做到在同一个设备上同时安装生产环境和测试环境的安装包,这对于日常的开发工作和测试人员的测试工作极不方便。总不能将整个工程复制一份,再通过修改包名的方式打包出另一个apk吧。所以在这种情况下,以往常见的做法就是在app中提供一个隐形的入口,供内部人员切换服务器地址,然后通过以下代码重启App:

1
2
3
4
5
private void restartApp(){
Intent intent = getContext().getPackageManager().getLaunchIntentForPackage(getContext().getPackageName());
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
startActivity(intent);
}

显然,这种做法也只是一种缓兵之计,多少还是有些不尽人意。然而,值得庆幸地是,进入Android Studio时代后,Google开始使用引入Gradle构建系统,applicationId的出现使得环境分离的问题迎刃而解。

package 与 applicationId

在使用Eclipse开发Apk或旧版本的Gradle构建系统中,应用的包名由AndroidManifest.xml文件中package属性决定。同时,这个package还被用来定义命名被引用的资源类R文件。

但是在新的Android Gradle构建系统中,package属性的两大作用得到了解藕:applicationId作为应用的唯一标识符(包名),用于区分不同应用;package属性定义资源类R文件,用于引用。

applicationId存在于app/build.gradle文件中的defaultConfig配置下,新建项目时默认使用package属性值初始化,所以如果没有特殊的需求,一般我们不会在意和修改这个值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
apply plugin: 'com.android.application'

android {
compileSdkVersion 19
buildToolsVersion "19.1"

defaultConfig {
applicationId "com.example.my.app"
minSdkVersion 15
targetSdkVersion 19
versionCode 1
versionName "1.0"
}
...

所以,要实现Apk的环境分离,也就是在同一设备上安装同一应用的不同版本,从本质上我们要修改applicationId的值,构建打包出不同包名的apk安装文件。Gradle构建系统提供了两种方式供开发人员修改applicationId的值,productFlavors和buildTypes,通过这两个方式我们可以轻松实现apk的打包定制,或者说Build Variants(构建变种)。

Build Variants

项目的productFlavors和buildTypes配置可以在app/build.gradle代码文件或者Project Structure上修改,作用是一样的。

productFlavors

项目可以通过定义多个不同的productFlavors来实现应用的不同定制版本,每一个Flavor与buildTypes配合产出对应的一种输出类型的apk文件,新建的项目初始化只有一个默认的Flavor:defaultConfig

productFlavors

注意:默认的defaultConfig为新建的productFlavors提供基本的配置,也就说,productFlavors的配置会覆盖defaultConfig中相同的属性,从而实现产品的不同定制版输出。对于环境分离,这里可以通过定义新的applicationId属性来实现。

buildTypes

默认情况下,项目的buildTypes包含debug和release两个构建版本,其中release版本的执行需要手动设置签名文件。对于环境分离,与productFlavors不同的是,buildTypes通过定义applicationIdSuffix来实现的,即添加后缀名:

http://ocq7gtgqu.bkt.clouddn.com/buildTypes.png

除了这些可配置的属性外,productFlavors和buildTypes都会通过各自的sourceSet来提供代码和资源,默认的路径为:src/flavorName和src/typeName。利用这个特性,我们可以实现不同定制版本的apk显示不同的应用名称和桌面图标,以便从设备上进行区分。

productFlavors和buildTypes配合产出各种格式为“flavorName + typeName”的Build Variants,以打包出不同版本的apk。当你没有自定义flavors,默认的defaultConfig也会与buildTypes形成对应的Build Variants,只是没有名字,所以显示为debug和release。比如这段配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
android {

...

productFlavors{
beta{
applicationId 'com.yifeng.mdstudysamples.beta'
}
production{
applicationId 'com.yifeng.mdstudysamples'
}
}

buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
debug {
applicationIdSuffix '.debug'
}
}
}

我们配置了beta和production两种productFlavors,release和debug两种buildTypes。所以,对应的Build Variants就有四种,分别为:betaDebug、betaRelease、productionDebug和productRelease。可以在Build Variants窗口查看并选择对应的构建类型运行应用:

Build Variants

注意:前面提到,buildTypes通过添加后缀的方式修改applicationId(包名)的,换句话说,就是在productFlavors的基础上修改包名的。所以,在上面这个例子中,betaRelease构建类型打包出的apk文件的包名是:com.yifeng.mdstudysamples.beta.debug。

实现方式

通过上面这些介绍,基本上大家能够知道在Android上如何实现app的环境分离了。我们可以选择使用productFlavors和buildTypes这两种方式在同一个设备上来安装同一个应用的不同版本。他们道理上是一样的,只是相比之下,使用buildTypes不用新建productFlavors,更为方便。这里我就buildTypes为例简单描述一下环境分离的实现。

1.对于一个默认productFlavors和buildTypes配置的项目,我们修改debug配置的applicationIdSuffix属性,设为”.debug”(名字可以随意设置),release版本不用变动。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
android {

...

buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
debug {
applicationIdSuffix '.debug'
}
}
}

有了这一步,已经能够将debug版本和release的apk安装在同一个设备上了。还可以做到更好,比如修改debug版本的桌面图标、应用名称等,便于区分。

2.我们在src目录下新建一个debug目录,将main目录下的res目录复制一份到debug目录下,修改各个分辨率下的桌面Icon和strings.xml文件中的应用名称,加个debug标识。目录结构如图所示:

debug_res

在构建打包时,debug目录下的res资源采用叠加的方式合并到main里面去,并替换相同的内容,而这个例子只需要修改桌面Icon和应用名称,所以这里我只复制了res目录下的相关文件,其他文件并未复制。

3.修改代码里的服务器接口地址,选择对应的Build Variants类型,运行即可。其实也可以在debug和main目录下的string.xml资源文件中定义服务器地址,然后在程序的入口处赋值给代码里的全局静态变量。

参考文章