Android Gradle 高级自定义
批量修改生成的apk文件名
Android Gradle中有很多相同的任务,这些任务的名字都是通过Build Types 和 Product Flavors 动态创建和生成的。
如果修改生成的apk文件名,就要修改Android Gradle打包的输出。Android对象提供了3个属性:applicationVariants 仅仅用于Android应用Gradle插件,libraryVariants 仅仅适用于Android库Gradle插件,testVariants 以上两种Gradle插件都适用。
这3个属性返回的都是DomainObjectSet对象集合,里面元素分别是ApplicationVariant,LibraryVariant,TestVariant ,这3个元素都是变体(就是Android构建产物)。如:ApplicationVariant表示Baidu渠道的release包,是基于Build Types 和 Product Flavors 生成的产物。
android {
buildTypes {
release {
minifyEnabled true
signingConfig signingConfigs.release
proguardFiles getDefaultProguardFile('proguard-android.txt')
}
debug {
minifyEnabled false
}
}
productFlavors {
//发布使用
arm {
}
//开发使用
dev {
}
}
// variant就是生成的产物,共有armRelease,armDebug,devRelease,devDebug四个产物
applicationVariants.all { variant ->
variant.outputs.all { output ->
println "applicationVariants>>>>> outputFile: ${output.outputFile}, name: ${output.outputFile.name}"
println "applicationVariants>>>>> flavorName: ${variant.flavorName}, baseName: ${variant.baseName}, name: ${variant.name}"
if (output.outputFile != null && output.outputFile.name.endsWith('.apk')
&& 'debug'.equals(variant.buildType.name)) {
def apkFile = new File(output.outputFile.getParent(),
"${project.name}-${variant.baseName}-${new Date().format('yyyyMMdd')}.apk")
outputFileName = apkFile.name
println "output apk file: >>>>>${output.outputFile}"
}
}
}
}
其中一个输出
applicationVariants>>>>> outputFile: D:\work\yqb.com\newCode\merchantApp\app\build\outputs\apk\dev\debug\app-dev-debug.apk, name: app-dev-debug.apk
applicationVariants>>>>> flavorName: dev, baseName: dev-debug, name: devDebug
output apk file: >>>>>D:\work\yqb.com\newCode\merchantApp\app\build\outputs\apk\dev\debug\app-dev-debug-20190903.apk
动态修改版本信息
版本一般由3个部分构成:major.minor.patch,版本号.副版本号.补丁号
原始配置方式,比较直观。最大问题就是修改不方便
android {
defaultConfig {
applicationId "com.kerwin"
minSdkVersion 19
targetSdkVersion 26
versionCode 100
versionName "1.0.0"
}
}
可以分模块方式配置,将版本号的配置单独抽取出来,放在单独的文件里,供其他build引用。Android是可以通过apply from方式引用
// 新建config.gradle文件
ext {
versionCode = 100
versionName = '1.0.0'
}
ext { }块为当前project创建扩展属性。其他build.gradle中引用后就可以使用
apply from: 'config.gradle'
我们也可以从属性文件中动态获取,例如创建一个config.properties属性文件
// config.properties
versionCode=100
versionName='1.0.0'
然后在build.gradle文件中动态获取
Properties properties = new Properties()
if (project.hasProperty("config.properties")
&& file(project.property("config.properties")).exists()) {
properties .load(new FileInputStream(file(project.property("config.properties"))))
}
if (properties != null && properties .size() > 0) {
String versionCode= properties ['versionCode']
String versionName= properties ['versionName']
}
动态配置AndroidManifest文件
在构建的过程中,动态修改AndroidManifest文件中内容。在使用友盟第三方分析统计时,要求在AndroidManifest文件中指定渠道名
<meta-data
android:name="UMENG_CHANNEL"
// ${UMENG_CHANNEL}占位符,UMENG_CHANNEL是变量名
android:value="${UMENG_CHANNEL}"/>
其中Channel ID要替换成不同渠道名,如google,baidu,miui。在构建时,根据生成的不同渠道包来指定不同的渠道名,Android Gradle提供manifestPlaceholders、Manifest占位符替换AndroidManifest文件中的内容
android {
productFlavors {
google {
// 是一个属性,Map类型。key就是在AndroidManifest文件中占位符变量
manifestPlaceholders.put("UMENG_CHANNEL", "google")
}
}
// 也可以迭代productFlavors批量修改
productFlavors.all { flavor ->
println "productFlavors>>> name: ${flavor.name}"
manifestPlaceholders.put("UMENG_CHANNEL", flavor.name)
}
}
自定义BuildConfig
下面是Android Gradle自动生成的
/**
* Automatically generated file. DO NOT MODIFY
*/
package ${packageName};
public final class BuildConfig {
public static final boolean DEBUG = Boolean.parseBoolean("true");
public static final String APPLICATION_ID = "${packageName}";
public static final String BUILD_TYPE = "debug";
public static final String FLAVOR = "arm";
public static final int VERSION_CODE = 215;
public static final String VERSION_NAME = "2.1.5";
}
还可以自定义一些常量,动态配置。Android Gradle提供了buildConfigField(@NonNull String type, @NonNull String name, @NonNull String value)可以自定义常量到BuildConfig中。
android {
buildTypes {
debug {
buildConfigField "String", "testBuildConfig", "\"测试\""
}
release {
buildConfigField "String", "testBuildConfig", "\"测试\""
}
}
}
动态添加自定义的资源
除了可以res/values文件夹中使用xml的方式定义资源外,还可以在Android Gradle中定义。
通过**resValue(@NonNull String type,
@NonNull String name,
@NonNull String value)**方法,在 BuildType 和 ProjectFlavor 中都存在,可以针对不同的渠道,或者不同的构建类型自定义特有资源。
android {
buildTypes {
debug {
// 第一个参数可以是 string、id、bool、dimen、integer、color
resValue "string", "baidu_map_api_key", "\"1234567\""
}
release {
resValue "string", "baidu_map_api_key", "\"76544321\""
}
}
}
在下图目录中可以找到生成的自定义资源
Java编译选项
可以通过compileOptions对java源文件编码、源文件使用的JDK版本配置
android {
compileOptions {
encoding Charsets.UTF_8.name()
// Java源代码编译级别,格式可以是 "1.8" 、1.8 、JavaVersion.VERSION_1_8 、VERSION_1_8
sourceCompatibility JavaVersion.VERSION_1_8
// 配置生成的Java字节码的版本
targetCompatibility JavaVersion.VERSION_1_8
}
}
DEX选项配置
android {
dexOptions {
// 配置最大堆内存
javaMaxHeapSize "4g"
// 函数超过65535个时,有时需要开启jumbo模式才可以构建成功。默认是false
jumboMode = true
// 配置是否预执行dex Libraries库工程,开启后可以提高增量构建速度,默认是开启的
// 当使用dx的--multi-dex选项生成多个dex,会导致和库工程冲突,应该关闭
preDexLibraries true
// Integer类型,配置运行dx命令时使用的线程数
threadCount 4
}
}
开启MultiDex,突破65535方法限制
APK中包含 Dalvik Executable (DEX) 文件形式的可执行字节码文件,这些DEX文件包含应用运行已编译代码。 65,535等于 64 X 1024 - 1
因为Dalvik虚拟机在执行DEX文件时,使用short类型索引DEX文件中方法,单个DEX文件中方法可以被定义最多是65535个,当超过就会报错。
trouble writing output:
Too many field references: 131000; max is 65536.
You may try using --multi-dex option.
较低版本的编译系统会报告一个不同的错误,但指示的是同一问题:
Conversion to Dalvik format failed:
Unable to execute dex: method ID not in [0, 0xffff]: 65536
可采用生成多个DEX文件来解决这个问题。
在Android 5.0之后,Android使用ART运行时方式,支持从APK文件加载多个DEX文件,ART在APK安装时执行预编译,扫描classesN.dex文件,将多个DEX文件合并成一个.oat文件执行;在 minSdkVersion 为 21 或者更高,不需要多dex文件支持库。
https://source.android.google...
在Android 5.0之前,Android使用Dalvik运行,而Dalvik虚拟机限制每个APK只能使用一个classes.dex字节码文件,要使用必须使用Multidex库。
配置多dex
minSdkVersion为 21 或者 以上,只需将 multiDexEnabled 设置为 true 就可以。
当配置multidex后,当超过65535时生成多个dex文件,文件名为classes.dex,classes2.dex,classesN.dex
android {
defaultConfig {
// 启用multidex
multiDexEnabled true
}
}
如果minSdkVersion为 21 以下(不包括21)
- multiDexEnabled 设置为 true,同时还需添加多dex依赖库
dependencies {
// 配置multidex依赖
implementation 'com.android.support:multidex:1.0.1'
}
https://developer.android.goo...
- 控制Application入口
// 1. 如果没有自定义Application,只需在AndroidManifest文件中直接配置MultiDexApplication
<application
android:name="android.support.multidex.MultiDexApplication" />
// 2. 如果有自定义的Application,并且是直接继承Application的。可以将修改为MultiDexApplication
public class MMApplication extends MultiDexApplication{ }
// 3. 如果自定义的Application已经继承的第三方提供的Application,就不能继承了。可以在重新attachBaseContext方法实现
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
MultiDex.install(this);
}
在 MultiDex.install(this)完成之前,不要通过反射或者其他任何代码,否则导致ClassNotFoundException
Android 编译工具会根据需要构建主 DEX 文件 (classes.dex) 和辅助 DEX 文件(classes2.dex 和 classes3.dex 等)。然后,编译系统会将所有 DEX 文件打包到您的 APK 中。
后续会讲解下MultiDex实现原理
多dex局限性
- 如果辅助DEX文件较大,可能导致应用无响应ANR
- 多DEX文件配置会增加编译处理时间,因为编译系统需要做出决策,哪些类包含在主DEX中,哪些类包含在辅助DEX中。
可以使用dex预处理缩短增量编译时间。dex 预处理依赖于Android 5.0或以上版本中提供的 ART 格式。Android Studio2.3或以上版本会自动使用此功能。如果命令行运行Gradle编译。需要设置minSdkVersion 21或以上启用dex预处理。
一个开发类型dev 和一个发布类型prod,它们具有不同的 minSdkVersion 值,来创建两个应用版本
android {
defaultConfig {
...
multiDexEnabled true
// The default minimum API level you want to support.
minSdkVersion 15
}
productFlavors {
// Includes settings you want to keep only while developing your app.
dev {
// Enables pre-dexing for command line builds. When using
// Android Studio 2.3 or higher, the IDE enables pre-dexing
// when deploying your app to a device running Android 5.0
// (API level 21) or higher—regardless of what you set for
// minSdkVersion.
minSdkVersion 21
}
prod {
// If you've configured the defaultConfig block for the production version of
// your app, you can leave this block empty and Gradle uses configurations in
// the defaultConfig block instead. You still need to include this flavor.
// Otherwise, all variants use the "dev" flavor configurations.
}
}
buildTypes {
release {
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android.txt'),
'proguard-rules.pro'
}
}
}
dependencies {
compile 'com.android.support:multidex:1.0.3'
}
声明主DEX中必需的类
在构建多DEX时, 编译工具会执行复杂的决策来确定主DEX文件中需要的类,以便能够成功启动。如果主DEX文件中没有提供启动时需要的任何类,就会奔溃出现java.lang.NoClassDefFoundError错误。
对于代码依赖复杂或者自检机制,就可能不会将这些类识别为主DEX文件中必需类。需要使用multiDexKeepFile 或者multiDexKeepProguard 声明主DEX文件中必需的类,在构建时如果匹配到就添加到主DEX文件中。
- multiDexKeepFile
创建一个名为multidex-config.txt文件,在文件中添加需要放在主DEX的类,每行包含一个类,格式如下:
com/example/MyClass.class
com/example/MyOtherClass.class
Gradle会读取相对于build.gradle文件路径,multidex-config.txt 与 build.gradle 文件在同一目录中。
android {
buildTypes {
release {
multiDexKeepFile file('multidex-config.txt')
}
}
}
- multiDexKeepProguard
multiDexKeepProguard中文件添加内容格式与支持 Proguard 语法相同,包含-keep选项
-keep class com.example.MyClass
-keep class com.example.MyClassToo
或者指定包中所有的类
-keep class com.example.** { *; } // All classes in the com.example package
https://www.guardsquare.com/e...
android {
buildTypes {
release {
multiDexKeepFile file('multidex-config.pro')
}
}
}
代码和资源压缩
为了减小APK的大小,应该启动压缩来移除发布构建中未使用的代码和资源。
代码压缩通过 ProGuard 提供,ProGuard 会检测和移除应用中未使用的类、字段、方法和属性,包括自带代码库中的未使用项。ProGuard 还可优化字节码,移除未使用的代码指令,以及用短名称混淆其余的类、字段和方法。
资源压缩通过 Gradle 的 Android 插件提供,该插件会移除应用中未使用的资源,包括代码库中未使用的资源。
代码压缩
要通过 ProGuard 启用代码压缩,在 build.gradle 文件内相应的构建类型中添加 minifyEnabled true
代码压缩会影响构建速度,避免在调试中使用。
android {
buildTypes {
release {
minifyEnabled true
// 用于定义 ProGuard 规则,getDefaultProguardFile 从 ${Android SDK}\tools\proguard\文件夹获取默认的ProGuard 配置
// proguard-rules.pro文件用于添加自定义ProGuard 配置,默认文件位于模块根目录
proguardFiles getDefaultProguardFile('proguard-android.txt'),
'proguard/proguard-rules.pro'
}
}
}
每次构建时,ProGuard 都会输出下列文件:
- dump.txt 说明 APK 中所有类文件的内部结构
- mapping.txt 提供原始与混淆过的类、方法和字段名称之间的转换
- seeds.txt 列出未进行混淆的类和成员
- usage.txt 列出从 APK 移除的代码
这些文件保存在 ${module-name}/build/outputs/mapping/release/ 中
自定义要保留的代码
默认 ProGuard 配置文件 (proguard-android.txt) 足以满足需要,ProGuard 会移除所有(并且只会移除)未使用的代码。但是,ProGuard 很难以对多情况进行正确分析,可能会移除应用需要的代码。举例来说,它可能错误移除代码的情况包括:
- 当应用引用的类只来自 AndroidManifest.xml 文件时
- 当应用调用的方法来自 Java 原生接口 (JNI) 时
- 当应用在运行时(例如使用反射或自检)操作代码时
可以强制 ProGuard 保留指定代码,在 ProGuard 配置文件中添加一行 -keep 代码。或者在想保留的代码添加 @keep 注解,在类上添加 @keep 可原样保留整个类,在方法或者字段上添加可完整保留方法/字段以及类名称。
-keep public class * extends android.app.Activity
解码混淆过的代码追踪
在 ProGuard 压缩代码后,代码追踪变得困难,因为方法名称都混淆处理了。但是ProGuard 每次运行时都会创建一个 mapping.txt 文件,其中显示了与混淆过的名称对应的原始类名称、方法名称和字段名称。ProGuard 将该文件保存在应用的 <module-name>/build/outputs/mapping/release/ 目录中。
可以使用Android SDK 提供的工具解码混淆过的代码,retrace脚本(Window上为retrace.bat,Mac/Linux上为retrace.sh),位于<sdk-root>/tools/proguard/目录中
retrace.bat|retrace.sh [-verbose] mapping.txt [<stacktrace_file>]
例如:
retrace.bat -verbose mapping.txt obfuscated_trace.txt
也可以直接使用 proguardgui.bat 图形化工具,位于<sdk-root>/tools/proguard/bin/目录中
资源压缩
资源压缩只与代码压缩协同工作。代码压缩器移除所有未使用的代码后,资源压缩器便可确定应用仍然使用的资源。
启用资源压缩,在 build.gradle 文件中将 shrinkResources 属性设置为 true,默认是false
android {
buildTypes {
release {
shrinkResources true
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android.txt'),
'proguard/proguard-rules.pro'
}
}
}
资源压缩器目前不会移除 values/ 文件夹中定义的资源(例如字符串、尺寸、样式和颜色)。这是因为 Android 资源打包工具 (AAPT) 不允许 Gradle 插件为资源指定预定义版本
在开始 shrinkResources 后,打包构建时,Android Gradle自动处理未使用的资源,生成的apk就不会包含。可以在构建输出日志中查看,gradlew assembleArmRelease --info | grep "unused resource"
Removed unused resources: Binary resource data reduced from 2977KB to 2879KB: Removed 3%
但是可能会误删有用的资源,如使用反射去引用资源文件,Android Gradle区分不出来,认为这些资源没有被使用。我们可以使用keep配置哪些资源不被清理。
自定义要保留的资源
如果有想要保留或舍弃的特定资源,在项目中创建一个包含 resources 标记的 XML 文件,并在 tools:keep 属性中指定每个要保留的资源,在 tools:discard 属性中指定每个要舍弃的资源。这两个属性都接受逗号分隔的资源名称列表。还可以使用星号字符作为通配符。
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools"
tools:keep="@layout/l_used*_c,@layout/l_used_a,@layout/l_used_b*"
tools:discard="@layout/unused2" />
将该文件保存在项目资源中,例如,保存在 res/raw/keep.xml。构建不会将该文件打包到 APK 之中。
启用严格引用检查
正常情况下,资源压缩器可准确判定系统是否使用了资源。但是,如果在代码调用 Resources.getIdentifier(),这就表示代码会根据动态生成的字符串查询资源名称。当执行这一调用时,默认情况下资源压缩器会采取防御性行为,将所有具有匹配名称格式的资源标记为可能已使用,无法移除。
// 会使所有带 img_ 前缀的资源标记为已使用
String name = String.format("img_%1d", angle + 1);
res = getResources().getIdentifier(name, "drawable", getPackageName());
默认情况下启用的是安全压缩模式,tools:shrinkMode="safe"。如果将 keep.xml 文件中 shrinkMode 设置为 strict,也就是启用严格压缩模式,并且代码也引用了包含动态生成字符串的资源,则必须利用 tools:keep 属性手动保留这些资源。如果不保留,也会被清理掉。
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools"
tools:shrinkMode="strict" />
移除未使用的备用资源
shrinkResources只会移除代码未被引用的资源,不会移除不同设备的备用资源。比如引用的第三方库,特别是Support Library,为了国际化支持几十种语言,但是有的App不用支持这么多的语言,只需中文和英文就可以了;比如图片只支持xhdpi格式就可以。
可以使用 Android Gradle 插件的 resConfigs 属性来移除您的应用不需要的备用资源文件。
android {
defaultConfig {
// 只会保留默认的default 和 en 资源 ,其他的不会打包到APK中
resConfigs "en"
}
}
resConfigs的参数是资源限定符,包括屏幕方向(port,land),屏幕尺寸(small,normal,large,xlarge),屏幕像素密度(hdpi,xhdpi),API Level(V3,V4)等
参考文档
https://developer.android.goo...
https://developer.android.goo...
如果我的文章对您有帮助,不妨点个赞鼓励一下(^_^)
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。