前言
Android自从引入Gradle编译以来,在打包与扩展方面引入了很多新的变化,使得打包过程变得更加丰富与有趣。Android打包的需求千变万化,如何使用Gradle脚本来实现这些需求是每个Android开发人员都在摸索的问题。本文将从作者多年的开发经验来总结一些常见与不常见的打包技巧。
技巧一:打包文件名自动更名
这是比较常见的一个需求,通常的更名规则一般需要包括时间,版本,编译类型与渠道等信息。具体的规则根据项目的要求会有所变化。由于gradle版本的迭代,在修改输出文件的文件名的规则在不同的版本中也会有所不同。
下面的示例脚本主要针对release文件进行更名,规则为: {应用名}v{version}{releasetime}_{buildtype}for{渠道}.apk。请特别注意其中标红部分,就是3.0版以后的一些变更。
gradle 2.3.3以前版本
def appName = 'ShakeDemo'
def buildVersionName = "1.3.2.0101";
def now = new Date().format('yyyyMMdd_HHmmss');
applicationVariants.all { variant ->
def buildType = variant.buildType
variant.outputs.each { output ->
def outputFile = output.outputFile
// 保证只修改APK,不对lib进行变更
if (outputFile == null || !outputFile.name.endsWith('.apk'))
return
if (buildType.name == "debug") {
def fileName = "${appName}.apk"
output.outputFile = new File(outputFile.parent, fileName)
} else {
// 输出apk名称为appname _version_releasetime_buildtype.apk
def fileName = "${appName}_v${buildVersionName}_${now}_${variant.buildType.name}_for_${variant.flavorName}.apk"
output.outputFile = new File(outputFile.parent, fileName)
}
}
}
gradle 3.0 版本
def appName = 'ShakeDemo'
def buildVersionName = "1.3.2.0101";
def now = new Date().format('yyyyMMdd_HHmmss');
applicationVariants.all { variant ->
def buildType = variant.buildType
variant.outputs.all { output ->
def outputFile = output.outputFile
// 保证只修改APK,不对lib进行变更
if (outputFile == null || !outputFile.name.endsWith('.apk'))
return
if (buildType.name == "debug") {
outputFileName = "${appName}.apk"
} else if (buildType.name == "release"){
// 输出apk名称为appname_version_releasetime_buildtype_for_flavor.apk
outputFileName = "${appName}_v${buildVersionName}_${now}_${variant.buildType.name}_for_${variant.flavorName}.apk"
}
}
}
技巧二:混淆文件自动保存
大部分打包都会有混淆代码的需求,一是出于代码安全角度(当然,也只是增加一些阅读难度而已),一是出于减少输出文件的大小。为了自动化打包的需要,自动保存混淆打包过程中产生的mapping.txt,seeds.txt等文件是一个很自然的场景。下面代码主要关注如何处理混淆文件方面:
applicationVariants.all { variant ->
def buildType = variant.buildType
variant.outputs.all { output ->
......
output.assemble.doLast {
delete fileTree(dir: 'build/outputs/apk', include: '*unaligned*.apk')
if (buildType.minifyEnabled) {
def mappingDestFile = new File(output.outputFile.parent, "mapping/${variant.flavorName}_${buildType.name}");
def srcMappingFile = new File("build/outputs/mapping/${variant.flavorName}/${buildType.name}/mapping.txt")
println "move mapping files from ${srcMappingFile} to ${mappingDestFile}"
if (!(srcMappingFile.parentFile == mappingDestFile)) {
mappingDestFile.mkdirs();
copy {
from srcMappingFile
into mappingDestFile
}
}
}
}
}
}
技巧三:多渠道打包
Android应用发布渠道多样,从统计的角度来说,每个渠道发布带特定渠道号的包是必要的。gradle在这方面有比较完善的扩展支持。常见的方案有二种:
AndroidManifests.xml文件与gradle结合,示例如下:
AndroidManifests.xml文件中定义一个渠道Meta
<manifest ...
<application ....
.....
<meta-data
android:name="CHANNEL"
android:value="${PUBLISH_CHANNEL}" />
</application>
</manifest>
在build.gralde文件中,增加productFlavors定义
android {
compileSdkVersion 25
buildToolsVersion "25.0.2"
defaultConfig {
minSdkVersion 15
targetSdkVersion 25
versionCode 31
versionName buildVersionName
...
}
productFlavors {
googleplay {
manifestPlaceholders = [PUBLISH_CHANNEL: "googleplay"]
}
appcenter {
manifestPlaceholders = [PUBLISH_CHANNEL: "appcenter"]
}
}
...
}
BuildConfig文件常量预处理,示例如下:
在build.gralde文件中,增加productFlavors定义
android {
compileSdkVersion 25
buildToolsVersion "25.0.2"
defaultConfig {
minSdkVersion 15
targetSdkVersion 25
versionCode 31
versionName buildVersionName
...
}
productFlavors {
googleplay {
buildConfigField "String", "PUBLISH_CHANNEL", "\"googleplay\""
}
appcenter {
buildConfigField "String", "PUBLISH_CHANNEL", "\"appcenter\""
}
}
...
}
技巧四:多维度打包
从本质上来说,多维度打包是对多渠道打包的扩展。但从代码差异层面来说,每个维度代表不同的差异。如代码维度、资源维度、渠道维度。这需要用到flavorDimensions与productFlavors这两个扩展属性。废话不多说,请看示例性代码:
android {
compileSdkVersion 25
buildToolsVersion "25.0.2"
defaultConfig {
minSdkVersion 15
targetSdkVersion 25
versionCode 31
versionName buildVersionName
...
}
flavorDimensions "source", "channel"
productFlavors {
googleplay {
dimension "source"
applicationId "gotube.video.downloader.lite"
manifestPlaceholders = [SEARCH_ACTIVITY: "com.vd.vshining.search.gp.SearchActivity"]
buildConfigField "boolean", "ENABLE_NON_GP", "false"
buildConfigField "String", "APP_ROOT_DIR", "\"GoTube\""
}
pro {
dimension "source"
applicationId "freetube.vd.android"
manifestPlaceholders = [SEARCH_ACTIVITY: "com.vd.vshining.search.SearchActivity"]
buildConfigField "boolean", "ENABLE_NON_GP", "true"
buildConfigField "String", "APP_ROOT_DIR", "\"FreeTube\""
}
apps {
dimension "channel"
manifestPlaceholders = [PUBLISH_CHANNEL: "APPS"]
}
qq {
dimension "channel"
manifestPlaceholders = [PUBLISH_CHANNEL: "qq"]
}
}
...
}
技巧五:源代码与编译预处理
有过多年android项目经验的人都知道,很多时候项目调试与发布往往存在很大的差异,各个渠道间代码也存在一些差异,如何通过脚本一劳永逸地自动处理,是开发人员所追求的。本技巧涉及的内涵太多,无法一一道来,下面只是就曾经的一个项目做一下示范。
调试与发布预处理需求:调试时采用分包支持,发布时因进行了代码混淆可不需要分包,实现代码如下:
def MULTI_DEX_ENABLED = true
android {
compileSdkVersion 25
buildToolsVersion "25.0.2"
defaultConfig {
minSdkVersion 15
targetSdkVersion 25
versionCode 31
versionName buildVersionName
buildConfigField("boolean", "MULTI_DEX_ENABLED", "${MULTI_DEX_ENABLED}")
...
}
dependencies {
...
debugCompile 'com.android.support:multidex:1.0.1'
}
...
}
task generateReleaseConfig() {
MULTI_DEX_ENABLED = false;
}
task generateDebugConfig() {
MULTI_DEX_ENABLED = true;
}
project.afterEvaluate {
tasks.each { task ->
def name = task.name;
if (name.startsWith("pre") && name.endsWith("Build")) {
if (name.endsWith("ReleaseBuild")) {
println("generateReleaseConfig before ${name}");
task.dependsOn("generateReleaseConfig");
} else if (name.endsWith("DebugBuild")) {
println("generateDebugConfig before ${name}");
task.dependsOn("generateDebugConfig");
}
}
}
}
在Application的实现中配合控制MultiDex的初始化
public class EApplication extends Application {
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
if (BuildConfig.MULTI_DEX_ENABLED) {
InvokeUtil.invokeStaticMathod("android.support.multidex.MultiDex", "install", this);
}
}
...
}
渠道预处理需求:不同渠道因为包名不一致,需要引用不同的googleservice.json以及使用不同的代码实现。具体代码如下:
android {
compileSdkVersion 25
buildToolsVersion "25.0.2"
defaultConfig {
minSdkVersion 15
targetSdkVersion 25
versionCode 31
versionName buildVersionName
...
}
flavorDimensions "source", "channel"
productFlavors {
googleplay {
dimension "source"
applicationId "gotube.video.downloader.lite"
manifestPlaceholders = [SEARCH_ACTIVITY: "com.vd.vshining.search.gp.SearchActivity"]
buildConfigField "boolean", "ENABLE_NON_GP", "false"
buildConfigField "String", "APP_ROOT_DIR", "\"GoTube\""
}
pro {
dimension "source"
applicationId "freetube.vd.android"
manifestPlaceholders = [SEARCH_ACTIVITY: "com.vd.vshining.search.SearchActivity"]
buildConfigField "boolean", "ENABLE_NON_GP", "true"
buildConfigField "String", "APP_ROOT_DIR", "\"FreeTube\""
}
apps {
dimension "channel"
manifestPlaceholders = [PUBLISH_CHANNEL: "APPS"]
}
qq {
dimension "channel"
manifestPlaceholders = [PUBLISH_CHANNEL: "qq"]
}
}
...
}
task generateJSAssets(type: Zip) {
...
}
task switchToGooglePlay(type: Copy) {
description = 'Switches to GooglePlay google-services.json'
println description
from "src/googleplay"
include "google-services.json"
into "."
}
task switchToPro(type: Copy) {
description = 'Switches to Pro google-services.json'
println description
from "src/pro"
include "google-services.json"
into "."
}
project.afterEvaluate {
preBuild.dependsOn("generateJSAssets")
tasks.each { task ->
def name = task.name;
if (name.startsWith("process") && name.endsWith("GoogleServices")) {
if (name.startsWith("processPro")) {
task.dependsOn("switchToPro")
} else if (name.startsWith("processGoogleplay")){
task.dependsOn("switchToGooglePlay")
}
}
}
}
技巧六:assets文件压缩处理
因为项目需要,很多时候会在assets中带入大量的文件,如一些html模板文件,资源文件或者js脚本文件。因为安全或者包大小方面的考虑,进行适当的压缩处理是必要的。示例性代码如下
android {
...
}
task generateJSAssets(type: Zip) {
delete 'assets/data.dat'
from 'assets_debug/js'
archiveName "data.dat"
destinationDir file('assets')
}
project.afterEvaluate {
preBuild.dependsOn("generateJSAssets")
...
}
技巧七:如何输出合适的jar包
从Eclipse转入Android Studio后,对如何用gradle打jar包会有点茫然。不过,网上有大量关于如何用gradle导出jar包的教程与脚本。这里直接上代码:
def packageName = 'com.wcc.wink'
def libraryVersion = '1.4.0'
def artifactName = 'wink-okhttp3'
android {
...
}
task clearJar(type: Delete) {
delete "build/libs/${artifactName}-${libraryVersion}.jar"
}
task makeJar(type: org.gradle.api.tasks.bundling.Jar) {
baseName artifactName
version libraryVersion
from('build/intermediates/classes/release/com/wcc/wink/')
into('com/wcc/wink/')
exclude('test/','BuildConfig.class','R.class')
//去掉R$开头的文件
exclude{ it.name.startsWith('R$');}
}
makeJar.dependsOn(clearJar, build)
task sourceJar(type: Jar) {
from android.sourceSets.main.java.srcDirs
classifier "source"
}
// 混淆jar包的方法
task proguardJar(dependsOn: ['makeJar'], type: ProGuardTask) {
//Android 默认的 proguard 文件
configuration android.getDefaultProguardFile('proguard-android.txt')
//会根据该文件对 Jar 进行混淆,注意:需要在 manifest 注册的组件也要加入该文件中
configuration 'proguard-rules.pro'
String inJar = makeJar.archivePath.getAbsolutePath()
//输入 jar
injars inJar
//输出 jar
int s =inJar.lastIndexOf('/');
if (s >= 0)
outjars inJar.substring(0, s) + "/proguard-${makeJar.archiveName}"
else
outjars "proguard-${makeJar.archiveName}"
//设置不删除未引用的资源(类,方法等)
dontshrink
AppPlugin appPlugin = getPlugins().findPlugin(AppPlugin)
if (appPlugin != null) {
List<String> runtimeJarList
if (appPlugin.getMetaClass().getMetaMethod("getRuntimeJarList")) {
runtimeJarList = appPlugin.getRuntimeJarList()
} else if (android.getMetaClass().getMetaMethod("getBootClasspath")) {
runtimeJarList = android.getBootClasspath()
} else {
runtimeJarList = appPlugin.getBootClasspath()
}
for (String runtimeJar : runtimeJarList) {
//给 proguard 添加 runtime
libraryjars(runtimeJar)
}
}
}
技巧八:如何发布jar包或者aar包
发布jar包与aar包并不是一件复杂的事情,目前比较常见的库有maven与jfrog,两者的发布规则稍有些不同。下面一一以案例说明一下。
Maven发布
apply plugin: 'com.android.library'
apply plugin: 'maven'
apply plugin: 'maven-publish'
def packageName = 'com.tcl.framework'
def libraryVersion = '1.2.5'
def artifactName = 'tcl-framework'
android {
...
}
task clearJar(type: Delete) {
...
}
task makeJar(type: org.gradle.api.tasks.bundling.Jar) {
...
}
makeJar.dependsOn(clearJar, build)
task sourceJar(type: Jar) {
from android.sourceSets.main.java.srcDirs
classifier "source"
}
uploadArchives {
repositories.mavenDeployer {
def baseName = "tcl-framework"
repository(url: "${repository_url}") {
authentication(userName: "${artifactory_user}", password: "${artifactory_password}")
}
pom.version = version
pom.artifactId = baseName
pom.groupId = "com.tcl.framework"
pom.name = baseName
pom.packaging = 'jar'
}
}
uploadArchives.dependsOn(makeJar)
Jfrog发布
apply plugin: 'com.android.library'
apply plugin: 'maven'
apply plugin: 'com.jfrog.artifactory'
apply plugin: 'maven-publish'
def packageName = 'com.wcc.wink'
def libraryVersion = '1.4.0'
def artifactName = 'wink-okhttp3'
android {
...
}
task clearJar(type: Delete) {
...
}
task makeJar(type: org.gradle.api.tasks.bundling.Jar) {
...
}
makeJar.dependsOn(clearJar, build)
task sourceJar(type: Jar) {
from android.sourceSets.main.java.srcDirs
classifier "source"
}
publishing {
publications {
aar(MavenPublication) {
groupId packageName
version = libraryVersion
artifactId artifactName
artifact sourceJar
// Tell maven to prepare the generated "* .aar" file for publishing
artifact("$buildDir/outputs/aar/${archivesBaseName}-${version}.aar")
// artifact assembleRelease
pom.withXml {
def dependenciesNode = asNode().appendNode('dependencies')
//Iterate over the compile dependencies (we don't want the test ones), adding a <dependency> node for each
configurations.compile.allDependencies.each {
if(it.group != null && (it.name != null || "unspecified".equals(it.name)) && it.version != null)
{
def dependencyNode = dependenciesNode.appendNode('dependency')
dependencyNode.appendNode('groupId', it.group)
dependencyNode.appendNode('artifactId', it.name)
dependencyNode.appendNode('version', it.version)
}
}
}
}
jar(MavenPublication) {
groupId packageName
version = libraryVersion
artifactId artifactName
artifact makeJar
}
}
}
artifactory {
contextUrl = "${artifactory_contextUrl}"
publish {
repository {
// The Artifactory repository key to publish to
repoKey = '${repo_key}' // repository key
username = "${artifactory_user}"
password = "${artifactory_password}"
maven = true
}
defaults {
// Tell the Artifactory Plugin which artifacts should be published to Artifactory.
publications('aar')
// publications ('jar')
publishArtifacts = true
// Properties to be attached to the published artifacts.
properties = ['qa.level': 'basic', 'dev.team': 'core']
// Publish generated POM files to Artifactory (true by default)
publishPom = true
}
}
}
技巧九:批量打包如何禁止输出某些类型的包
在多渠道多维度层面,批量打包会输出各种变体组合的文件,如果某些组合不是我们想要的,完全可以通过脚本禁止打包,以下是相关的gradle脚本示例:
android {
compileSdkVersion 25
buildToolsVersion "25.0.2"
defaultConfig {
minSdkVersion 15
targetSdkVersion 25
versionCode 31
versionName buildVersionName
...
}
flavorDimensions "source", "channel"
productFlavors {
googleplay {
dimension "source"
applicationId "gotube.video.downloader.lite"
manifestPlaceholders = [SEARCH_ACTIVITY: "com.vd.vshining.search.gp.SearchActivity"]
buildConfigField "boolean", "ENABLE_NON_GP", "false"
buildConfigField "String", "APP_ROOT_DIR", "\"GoTube\""
}
pro {
dimension "source"
applicationId "freetube.vd.android"
manifestPlaceholders = [SEARCH_ACTIVITY: "com.vd.vshining.search.SearchActivity"]
buildConfigField "boolean", "ENABLE_NON_GP", "true"
buildConfigField "String", "APP_ROOT_DIR", "\"FreeTube\""
}
none {
dimension "channel"
}
apps {
dimension "channel"
manifestPlaceholders = [PUBLISH_CHANNEL: "APPS"]
}
qq {
dimension "channel"
manifestPlaceholders = [PUBLISH_CHANNEL: "qq"]
}
}
variantFilter { variant ->
def dim = variant.flavors.collectEntries {
[(it.productFlavor.dimension): it.productFlavor.name]
}
def source = dim.get('source')
def channel = dim.get('channel')
if (source == 'googleplay' && channel != 'none') {
variant.setIgnore(true)
return;
}
def buildType = variant.buildType.name
if (source == 'pro' && channel == 'none' && buildType == 'release') {
variant.setIgnore(true)
return;
}
if (source == 'pro' && channel != 'none' && buildType != 'release') {
variant.setIgnore(true)
return;
}
}
...
}
技巧十:裁剪jar包
将一个大的jar按照我们使用的需要,裁剪出项目需要依赖的部分,可缓解方法超65536,具体示例如下
task repack(){
tasks.create(name: "stripPlayServices", type: Jar) {
destinationDir = new File("..\\app\\") //生成新的jar包的存储位置
archiveName = "google-play-services_shrink.jar" //新jar包的名称
from(zipTree(new File("google-play-services.jar"))) {
//用到的package
include "com/google/ads/**"
include "com/google/android/gms/ads/**"
}
}.execute()
}