博客主页

Android Gradle 高级自定义

批量修改生成的apk文件名

Android Gradle中有很多相同的任务,这些任务的名字都是通过Build TypesProduct Flavors 动态创建和生成的。

如果修改生成的apk文件名,就要修改Android Gradle打包的输出。Android对象提供了3个属性:applicationVariants 仅仅用于Android应用Gradle插件,libraryVariants 仅仅适用于Android库Gradle插件,testVariants 以上两种Gradle插件都适用。

这3个属性返回的都是DomainObjectSet对象集合,里面元素分别是ApplicationVariantLibraryVariantTestVariant ,这3个元素都是变体(就是Android构建产物)。如:ApplicationVariant表示Baidu渠道的release包,是基于Build TypesProduct 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提供manifestPlaceholdersManifest占位符替换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)

  1. multiDexEnabled 设置为 true,同时还需添加多dex依赖库
dependencies {
  // 配置multidex依赖
  implementation 'com.android.support:multidex:1.0.1'
}
https://developer.android.goo...
  1. 控制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局限性

  1. 如果辅助DEX文件较大,可能导致应用无响应ANR
  2. 多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文件中。

  1. multiDexKeepFile

创建一个名为multidex-config.txt文件,在文件中添加需要放在主DEX的类,每行包含一个类,格式如下:

com/example/MyClass.class
com/example/MyOtherClass.class

Gradle会读取相对于build.gradle文件路径,multidex-config.txtbuild.gradle 文件在同一目录中。

android {
   buildTypes {
      release {
         multiDexKeepFile file('multidex-config.txt')
      }
   }
}
  1. 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 都会输出下列文件:

  1. dump.txt 说明 APK 中所有类文件的内部结构
  2. mapping.txt 提供原始与混淆过的类、方法和字段名称之间的转换
  3. seeds.txt 列出未进行混淆的类和成员
  4. usage.txt 列出从 APK 移除的代码
这些文件保存在 ${module-name}/build/outputs/mapping/release/ 中

自定义要保留的代码

默认 ProGuard 配置文件 (proguard-android.txt) 足以满足需要,ProGuard 会移除所有(并且只会移除)未使用的代码。但是,ProGuard 很难以对多情况进行正确分析,可能会移除应用需要的代码。举例来说,它可能错误移除代码的情况包括:

  1. 当应用引用的类只来自 AndroidManifest.xml 文件时
  2. 当应用调用的方法来自 Java 原生接口 (JNI) 时
  3. 当应用在运行时(例如使用反射或自检)操作代码时

可以强制 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...

如果我的文章对您有帮助,不妨点个赞鼓励一下(^_^)


小兵兵同学
56 声望23 粉丝

Android技术分享平台,每个工作日都有优质技术文章分享。从技术角度,分享生活工作的点滴。