在日常开发中,我们或多或少都会碰到多渠道打包的一些问题,有些是同一个版本要上传到不同的平台,有些是要提供给不同的代理商,中间可能需要改动里面的图片或其他的一些资源文件,对于版本比较少的我们可以简单的替换,但是遇到很多版本时,相信再通过手动替换会令人吐血~
废话不说,先来看下效果图,下面是根据不同平台需要打包出来的两个apk
apk界面展示效果如下(为了方便展示我在productFlavors里用了不同的applicationId “包名”,防止安装apk时被替换)
下面先来介绍一下如何根据不同版本的需求存放不同的资源文件
资源文件的适配
假如有这么一个需求:现在需要打包两个版本,小米和华为,其页面仅有一张图片,但要求两个版本的图片不一样,并且app的名称也不同,简单的做下适配
1、首先看下页面布局
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.liuw.gradle.MainActivity">
<ImageView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:src="@mipmap/userhead"/>
</LinearLayout>
正如需求所说,仅有一张图片,默认我们的res资源文件夹下肯定会有一张默认的userhead图片,但是要做到不同版本展示不同的图片,是不是很自然的会想到根据不同版本建立不同的资源文件目录
2、这里我建立了两个资源文件,res-huawei和res-xiaomi,展开看一下里面的内容
这里我直接将华为和小米平台所需的图片和文字用了相同的名称添加了进去,这样项目在打包时就会直接覆盖res文件下对应的资源
3、在gradle里配置不同平台采用不同的资源文件
//以下代码放在android{}内
//配置资源文件路径,可动态指定不同版本资源文件
sourceSets {
main {
manifest.srcFile 'src/main/AndroidManifest.xml'
java.srcDirs = ['src/main/java']
resources.srcDirs = ['src/main/resources']
aidl.srcDirs = ['src/main/aidl']
renderscript.srcDirs = ['src/maom']
res.srcDirs = ['src/main/res']
assets.srcDirs = ['src/main/assets']
jniLibs.srcDir 'src/main/jniLibs'
}
//用各自对应的资源文件路径
xiaomi.res.srcDirs = ['src/main/res-xiaomi']
huawei.res.srcDirs = ['src/main/res-huawei']
// Move the tests to tests/java, tests/res, etc...
instrumentTest.setRoot('tests')
// Move the build types to build-types/<type>
// For instance, build-types/debug/java, build-types/debug/AndroidManifest.xml, ...
// This moves them out of them default location under src/<type>/... which would
// conflict with src/ being used by the main source set.
// Adding new build types or product flavors should be accompanied
// by a similar customization.
debug.setRoot('build-types/debug')
release.setRoot('build-types/release')
}
//渠道Flavors,配置不同风格的app
productFlavors {
xiaomi{}
huawei{}
}
//批量配置
productFlavors.all {
flavor -> flavor.manifestPlaceholders = [UMENG_CHANNEL_VALUE: name]
}
配置完gradle里,别忘了在AndroidManifest里配置渠道标签
<meta-data
android:name="UMENG_CHANNEL"
android:value="${UMENG_CHANNEL_VALUE}">
</meta-data>
到这里资源文件的基本配置就算结束了
配置多渠道打包
内容比较多,代码里注释也比较详细,这里就直接上代码了
apply plugin: 'com.android.application'
android {
compileSdkVersion 25
buildToolsVersion "26.0.0"
defaultConfig {
applicationId "com.liuw.gradle"
minSdkVersion 15
targetSdkVersion 25
versionCode 1
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
//配置资源文件路径,可动态指定不同版本资源文件
sourceSets {
main {
manifest.srcFile 'src/main/AndroidManifest.xml'
java.srcDirs = ['src/main/java']
resources.srcDirs = ['src/main/resources']
aidl.srcDirs = ['src/main/aidl']
renderscript.srcDirs = ['src/maom']
res.srcDirs = ['src/main/res']
assets.srcDirs = ['src/main/assets']
jniLibs.srcDir 'src/main/jniLibs'
}
//用各自对应的资源文件路径
xiaomi.res.srcDirs = ['src/main/res-xiaomi']
huawei.res.srcDirs = ['src/main/res-huawei']
// Move the tests to tests/java, tests/res, etc...
instrumentTest.setRoot('tests')
// Move the build types to build-types/<type>
// For instance, build-types/debug/java, build-types/debug/AndroidManifest.xml, ...
// This moves them out of them default location under src/<type>/... which would
// conflict with src/ being used by the main source set.
// Adding new build types or product flavors should be accompanied
// by a similar customization.
debug.setRoot('build-types/debug')
release.setRoot('build-types/release')
}
// rename the apk with the version name,版本比较多时,自定义导出的APK名称,不同的渠道编出的APK的文件名应该是不一样的
applicationVariants.all { variant ->
variant.outputs.each { output ->
def outputFile = output.outputFile
Map<String, String> map = System.getenv();
String userName = map.get("USER");
def des = '/Users/' + userName + '/Desktop/release/'
//根据apk类型打包(release和debug两个版本,一般供单个应用打包使用)
// if (variant.buildType.name.equals('release')) {
// def releaseApkName = 'gradle_v' + getVersionName(project) + '.apk'
// output.outputFile = new File(des + releaseApkName)
// }
// if (variant.buildType.name.equals('debug')) {
// }
//根据apk名称打包(这里就用到了同个apk,为不同平台打包,也可以打debug版本)
if (outputFile != null) {
//不知道名称的可以提前跑一遍,看下这里打印出来的日志
println "======outputFile.name======" + outputFile.name;
def fileName;
if (outputFile.name.endsWith('app-xiaomi-release.apk')) {
//打出包的包名,版本号为当前gradle里的versionName
//fileName="xiaomi-gradle_v-${defaultConfig.versionName}.apk"
//打出包的包名,版本号为AndroidManifest里获取的versionName
fileName="xiaomi-gradle_v-${getVersionName(project)}.apk"
}else if(outputFile.name.endsWith('app-huawei-release.apk')){
// fileName="huawei-gradle_v-${defaultConfig.versionName}.apk"
fileName="huawei-gradle_v-${getVersionName(project)}.apk"
} else {
fileName='liuw-release.apk';//or debug版本
}
//默认outputs/apk目录下
//output.outputFile = new File(outputFile.parent, fileName)
//指定目录,根据自己需要指定
output.outputFile = new File(des + fileName)
}
}
}
//debug和release版本的签名配置
signingConfigs {
debug {
//这样写就得把keystore.jks文件放在项目目录,绝对路径
storeFile file("keystore.jks")
keyAlias = 'keystore'
keyPassword = '123456'
storePassword = '123456'
}
release {
//这样keystore.jks文件可以放在桌面任何地方,配置一下路径即可
storeFile file(getKeyStorePath())
keyAlias = 'keystore'
keyPassword = '123456'
storePassword = '123456'
}
//有不同签名时,可定义多个,在productFlavors中调用
// release1 {
// storeFile file(getKeyStorePath())
// keyAlias = 'keystore'
// keyPassword = '123456'
// storePassword = '123456'
// }
}
//构建类型,通常有release和debug两种
buildTypes {
debug {
// 显示Log
buildConfigField "boolean", "LOG_DEBUG", "true"
versionNameSuffix "-debug"
minifyEnabled false
zipAlignEnabled false
shrinkResources false
signingConfig signingConfigs.debug
manifestPlaceholders=[jpush_appkey:"0f56521436362122"]
}
release {
// 不显示Log
buildConfigField "boolean", "LOG_DEBUG", "false"
//混淆
minifyEnabled true
//Zipalign优化
zipAlignEnabled true
// 移除无用的resource文件
shrinkResources true
//前一部分代表系统默认的android程序的混淆文件,该文件已经包含了基本的混淆声明
//如果需要自定义混淆可把后面参数替换成混淆文件
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
//指定打包时的签名,如果有不同的签名可放在productFlavors不同项目下执行,不过一般都会相同
signingConfig signingConfigs.release
//manifestPlaceholders 可以替换androidmanifest文件中的标签的value值,可作为快速渠道打包替换渠道名的一种方式
//也可以自定义标签用来替换需要的文本,多作为不同环境不同key的修改(如debug、release模式)
manifestPlaceholders=[jpush_appkey:"0d14522663255632"]
}
}
/*
* 渠道Flavors,配置不同风格的app
* 资源文件不能用test字段命令(这里我尝试过会运行错误,如res-test)
* debug模式下添加的资源文件默认只有一个,可以根据res图标看出来,运行时默认运行上次的文件,不过对打包无影响
* */
productFlavors {
xiaomi{}
huawei{}
// baidu{
// applicationId "com.liuw.badu"//可为不同版本动态指定包名
// manifestPlaceholders=[UMENG_CHANNEL_VALUE:"xiaomi"] //可以替换AndroidManifest中标签的value值,此处也可用作多渠道打包标识,不过一般采用下面的批量配置
// signingConfig signingConfigs.release1//此处可单独指定签名,前提是在signingConfigs里定义过了
// }
//注意一点,这里的flavor名如果是数字开头,必须用引号引起来,如"360"{}
}
//批量配置
productFlavors.all {
flavor -> flavor.manifestPlaceholders = [UMENG_CHANNEL_VALUE: name]
}
dexOptions {
//最大堆内存
javaMaxHeapSize '2048m'
//预编译
preDexLibraries = true
//线程数
threadCount = 16
dexInProcess = true
}
}
android {//lint检查
lintOptions {
//所有正式版构建执行规则生成崩溃的lint检查,如果有崩溃问题将停止构建
checkReleaseBuilds false
// Or, if you prefer, you can continue to check for errors in release builds,
// but continue the build even when errors are found:
//错误发生后停止gradle构建
abortOnError false
}
}
//以下的两个def方法可以放在总工程路径下的build.gradle里使用ext置成全局供多个Module调用(通过rootProject.ext.xxx调用获取)
//获取桌面上的keystore路径
def getKeyStorePath() {
Map<String, String> map = System.getenv();
String userName = map.get("USER");
def des = '/Users/' + userName + '/' + 'keystore.jks'
return des
}
//获取AndroidManifest中的versionName
def getVersionName(Project project) {
def xmlFile = project.file("src/main/AndroidManifest.xml")
def rootManifest = new XmlSlurper().parse(xmlFile)
def versionName = rootManifest['@android:versionName']
return versionName
}
//依赖的jar包
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
exclude group: 'com.android.support', module: 'support-annotations'
})
compile 'com.android.support:appcompat-v7:25.3.1'
testCompile 'junit:junit:4.12'
}
如果想要看渠道命名是否成功,可以在主页面里调用以下代码
String channel;
try {
ApplicationInfo appInfo = getPackageManager()
.getApplicationInfo(getPackageName(),
PackageManager.GET_META_DATA);
channel = appInfo.metaData.getString("UMENG_CHANNEL");
Log.i("TAG","UMENG_CHANNEL_VALUE=" + channel);
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
}
签名文件如果有不会的可以参考下生成签名,我这里为了方便测试,在桌面上和项目根目录各放了一份,MAC和windows路径可能不一样,如果出现错误,可能要自己修改一下。
Ok,到这里基本就算结束了,我们可以执行一下./gradlew assembleRelease
,等着打包完成吧。
assemble 这个命令,会结合 Build Type 创建自己的task,如:
./gradlew assembleDebug
- 1
./gradlew assembleRelease
- 1
除此之外 assemble 还能和 Product Flavor 结合创建新的任务,其实 assemble 是和 Build Variants 一起结合使用的,而 Build Variants = Build Type + Product Flavor , 举个例子大家就明白了:
如果我们想打包xiaomi渠道的release版本,执行如下命令就好了:
./gradlew assembleXiaomiRelease
- 1
如果我们只打xiaomi渠道版本,则:
./gradlew assembleXiaomi
- 1
此命令会生成xiaomi渠道的Release和Debug版本
同理我想打全部Release版本: ./gradlew assembleRelease
这条命令会把Product Flavor下的所有渠道的Release版本都打出来。
当我们productFlavors有多个版本,并且只想打包其中一个版本时,除了以上这种执行单个命令外,还可以在gradle里动态配置
1、先在根目录的gradle里添加一个全局参数
ext {
LAUNCHER_FLAVOR = 'xiaomi'
}
- 1
- 2
- 3
2、再去Module的gradle里添加variantFilter进行过滤
android {
//用于指定版本,只打包指定版本
variantFilter { variant ->
def flavor = variant.flavors*.name
println "======variant.flavors======" + flavor;
String name = 'defaultProject'
if (project.hasProperty('LAUNCHER_FLAVOR')) {
name = LAUNCHER_FLAVOR
}
if (!flavor.contains(name)) {
setIgnore(true)
} else if (flavor.contains('xiaomi')) {
dependencies {
xiaomiCompile project(':依赖的文件名') // 动态添加依赖,括号内格式可能要注意下
}
}
}
}
这样打出来的包就只有xiaomi版本的apk。
完整项目已上传至CSDN资源,因为写在一起可能不便理解,可下载下来跑一遍对比看下效果可能会更容易理解些~
Gradle功能还是比较强大的,这里只是简单介绍,如有不妥之处,欢迎指正更改~
还看到两篇不错的文章,说的比较详细,也一起分享出来供大家参考Android打包的那些事,Gradle配置最佳实践