阅读 1975

Gradle系列之Android Gradle高级配置

本篇文章主要在之前学习的基础上,从实际开发的角度学习如何对 Android Gradle 来进行自定义以满足不同的开发需求,下面是 Gradle 系列的几篇文章:

下面是主要内容:

  1. 修改生成的Apk文件名
  2. 版本信息统一管理
  3. 隐藏签名文件信息
  4. 动态配置AndroidManifest文件
  5. 自定义BuildConfig
  6. 动态添加自定义资源
  7. Java编译选项
  8. adb操作选项配置
  9. DEX选项配置
  10. 自动起立未使用的资源
  11. 突破65535方法限制

修改生成的Apk文件名

修改打包输出的 Apk 的文件名主要用到三个属性:

applicationVariants //Android应用Gradle插件
libraryVariants     //Android库Gradle插件
testVariants        //上述两种插件都适用
复制代码

下面是修改打包生成的 Apk 文件名的代码,参考如下:

android{
    //...
    
    /**
     * 修改打包生成的apk的文件名
     */
    applicationVariants.all { variant ->
        variant.outputs.all { output ->
            if (output.outputFile != null && output.outputFile.name.endsWith('.apk') &&
                    'release' == variant.buildType.name) {
                //输出文件名
                outputFileName = "AndroidGradleProject_v${variant.versionName}_${buildTime()}.apk"
            }
        }
    }   
}
//当前时间
def static buildTime() {
    def date = new Date()
    return date.format("yyyMMdd")
}
复制代码

此时,执行 release 模式构建 Apk 的任务,生成的 Apk 的名字就修改了,当然还可以配置在 debug 模式下生成对应的文件名等。

版本信息统一管理

每个应用都有一个版本,版本一般由三部分组成:major.minor.patch,第一个是主版本号,第二个是副版本号,第三个是补丁号,如 1.0.0 这种格式的版本号,在 Android 开发中最原始的版本配置方式就是在 build.gradle 中在 defaultConfig 中配置对应的版本号和版本名称,参考如下:

//最原始的版本配置方式
android{
    defaultConfig {
        versionCode 1
        versionName "1.0"
        //...
    }
}
复制代码

实际开发中一般将这种版本相关的信息单独定义在一个独立的版本管理文件中进行统一管理,定义 version.gradle 文件如下:

ext{
    //应用版本号、版本名称
    appversionCode = 1
    appVersionName = "1.0"
    //其他版本号...
}
复制代码

然后在 build.gradle 中使用 version.gradle 文件中定义的版本号、版本名称即可,参考如下:

//引入version.gradle文件
apply from: "version.gradle"
android {
    //...
    defaultConfig {
        //使用version.gradle里定义的版本号
        versionCode appversionCode
        //使用version.gradle里定义的版本名称
        versionName appVersionName
        //...
    }
}
复制代码

当然不只是应用的版本号,还有使用到的一些第三方的库的版本也可以使用这样的方式来统一管理。

隐藏签名文件信息

签名文件信息是非常重要的信息,如果将签名文件信息直接配置到项目中将是不安全的,那么签名文件如何能够安全呢,签名文件放在本地是不安全的,那么只能放在服务器上才是安全的,打包的时候从服务器上读取签名文件信息即可,当然这个服务器也可以是一台专门用于打包正式 Apk 的电脑,将签名文件和密钥信息配置成环境变量,打包是直接从环境变量中读取签名文件和密钥信息即可。

配置四个环境变量 STORE_FILE、STORE_PASSWORD、KEY_ALIAS、KEY_PASSWORD 分别对应签名文件、签名文件密码、签名文件密钥别名、签名文件密钥密码,环境变量的配置就不具体说了,代码参考如下:

android {
    //签名文件配置
    signingConfigs {
        //读取配置的与签名文件信息对应的环境变量
        def appStoreFile = System.getenv('STORE_FILE')
        def appStorePassword = System.getenv('STORE_PASSWORD')
        def appKeyAlias = System.getenv('KEY_ALIAS')
        def appKeyPassword = System.getenv('KEY_PASSWORD')
        //如果获取不到相关签名文件信息,则使用默认的签名文件
        if(!appStoreFile || !appStorePassword || !keyAlias || !keyPassword){
            appStoreFile = "debug.keystore"
            appStorePassword = "android"
            appKeyAlias = "androiddebugkey"
            appKeyPassword = "android"
        }
        release {
            storeFile file(appStoreFile)
            storePassword appStorePassword
            keyAlias appKeyAlias
            keyPassword appKeyPassword
        }
        debug {
            //默认情况下,debug模式下的签名已配置为Android SDK自动生成的debug签名文件证书
            //.android/debug.keystore
        }
    }
}
复制代码

注意一点,配置好环境变量后,如果不能读取到新配置的环境变量,重启电脑后就能读取到了,至于如何使用专用的服务器进行打包、读取签名文件信息实践后再来介绍。

动态配置AndroidManifest文件

动态配置 AndroidManifest 配置就是动态的去修改 AndroidManifest 文件中的一些内容,如友盟等第三方统计平台分析统计的时候,一般会要求要在 AndroidManifest 文件中指定渠道名称,如下所示:

<meta-data android:value="CHANNEL_ID" android:name="CHANNEL"/>
复制代码

这里 CHANNEL_ID 要替换成不同渠道的名称,如 baidu、miui 等各个渠道名称,那么如何动态的修改这些变化的参数呢,这里需要用到 Manifest 占位符和 manifestPlaceholder,manifestPlaceholder 是 ProductFlavor 的一个属性,是一个 Map 类型,可以配置多个占位符,具体代码参考如下:

android{
    //维度
    flavorDimensions "channel"
    productFlavors{
        miui{
            dimension "channel"
            manifestPlaceholders.put("CHANNEL","google")
        }
        baidu{
            dimension "channel"
            manifestPlaceholders.put("CHANNEL","baidu")
        }
    }
}
复制代码

上述代码中配置了 flavorDimensions 属性,这个属性可以理解为维度,比如 release 和 debug 是一个维度、不同的渠道是一个维度、免费版本还是付费版本又是一个维度,如果这三个维度都要考虑,那么生成 Apk 的格式就是 2 * 2 * 2 供 8 个不同的 Apk,从 Gradle 3.0 开始不管是一个维度还是多个维度,都必须使用 flavorDimensions 来约束,上面代码中定义了一个维度 channel,再加上 buildType 中的 debug 和 release ,故此时生成不同 Apk 的个数是 4 个,如下图所示:

channel.jpg

当然,如果没有配置 flavorDimensions 则会出现如下错误,具体如下:

Error:All flavors must now belong to a named flavor dimension.

实际开发中根据实际情况配置对应的 flavorDimensions 即可。

然后,在 AndroidManifest 文件中使用占位符介绍打包时传递过来的参数,在 AndroidManifest 文件中添加 如下:

<meta-data android:value="${CHANNEL}" android:name="channel"/>
复制代码

最后,执行对应的渠道包任务,如执行 assembleBaiduRelease 将会将 AndroidManifest 中的渠道替换成 baidu,可使用命令执行也可使用 Android Studio 选择对应的 task 来执行,执行命令如下:

gradle assembleBaiduRelease
复制代码

如果使用 Android Studio ,打开右侧 Gradle 控制面板,找到对应的 task 来执行相应的任务,如下图所示:

assembleBaiduRelease

选择对应的 task 执行就会生成对应的 Apk,使用 Android Killer 反编译打开生成的 Apk ,查看 AndroidManifest 文件如下:

<?xml version="1.0" encoding="utf-8" standalone="no"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.manu.androidgradleproject">
    <application android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:supportsRtl="true" android:theme="@style/AppTheme" roundIcon="@mipmap/ic_launcher_round">
        <!--AndroidManifest文件修改成功-->
        <meta-data android:name="channel" android:value="baidu"/>
        <activity android:name="com.manu.androidgradleproject.MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN"/>
                <category android:name="android.intent.category.LAUNCHER"/>
            </intent-filter>
        </activity>
        <meta-data android:name="android.support.VERSION" android:value="26.1.0"/>
        <meta-data android:name="android.arch.lifecycle.VERSION" android:value="27.0.0-SNAPSHOT"/>
    </application>
</manifest>

复制代码

上述案列中,渠道的名称是一致的,可以通过遍历很方便的完成渠道名称的替换,参考如下:

productFlavors.all{ flavor ->
    manifestPlaceholders.put("CHANNEL",name)
}
复制代码

这一小节重要的一点就是关于 manifestPlaceholders 占位符的使用。

自定义BuildConfig

BuildConfig 是一个在 Android Gradle 构建脚本编译后生成的类,默认构建生成的 BuildConfig 内容如下:

/**
 * Automatically generated file. DO NOT MODIFY
 */
package com.manu.androidgradleproject;

public final class BuildConfig {
  public static final boolean DEBUG = false;
  public static final String APPLICATION_ID = "com.manu.androidgradleproject";
  public static final String BUILD_TYPE = "release";
  public static final String FLAVOR = "baidu";
  public static final int VERSION_CODE = 1;
  public static final String VERSION_NAME = "1.0";
}
复制代码

上面 BuildConfig 中的一些常量都是关于应用的一些关键信息,其中 DEBUG 在 debug 模式下为 true,release 模式下为 false,此外还有应用包名、构建类型、构建渠道、版本号及版本名称,所以如果开发中需要用到这些值可以在 BuildConfig 中直接获取,比如包名的获取一般是 context.getPackageName(),如果直接从 BuildConfig 中获取是不是不仅方便而且有利于应用性能提升,所以,可在构建时在该文件中添加一些额外的有用的信息,可以使用 buildConfigField 方法,具体如下:

/**
 * type:生成字段的类型
 * name:生成字段的常量名称
 * value:生成字段的常量值
 */
public void buildConfigField(String type, String name, String value) {
    //...
}

复制代码

下面使用 buildConfigField 方法为每个渠道配置一个相关地址,参考如下:

android{
    //维度
    flavorDimensions "channel"
    productFlavors{
        miui{
            dimension "channel"
            manifestPlaceholders.put("CHANNEL","miui")
            buildConfigField 'String' ,'URL','"http://www.miui.com"'
        }
        baidu{
            dimension "channel"
            manifestPlaceholders.put("CHANNEL","baidu")
            //buildConfigField方法参数value中的内容是单引号中的,如果value是String,则String的双引号不能省略
            buildConfigField 'String' ,'URL','"http://www.baidu.com"'
        }
    }
}
复制代码

再打包时就会自动生成添加的字段,构建完成后查看 BuildConfig 文件,生成了上面添加的字段,参考如下:

/**
 * Automatically generated file. DO NOT MODIFY
 */
package com.manu.androidgradleproject;

public final class BuildConfig {
  public static final boolean DEBUG = false;
  public static final String APPLICATION_ID = "com.manu.androidgradleproject";
  public static final String BUILD_TYPE = "release";
  public static final String FLAVOR = "baidu";
  public static final int VERSION_CODE = -1;
  public static final String VERSION_NAME = "";
  // Fields from product flavor: baidu
  public static final String URL = "http://www.baidu.com";
}

复制代码

至此,自定义 BuildConfig 的学习就到此为止,当然 buildConfigField 也可以使用到构建类型中,关键就是 buildConfigField 方法的使用。

动态添加自定义资源

Android 开发中资源文件都是放置在 res 目录下,还可以在 Android Gradle 中定义,自定义资源需要使用到 resValue 方法,该方法在 BuildType 和 ProductFlavor 对象中可以使用,使用 resValue 方法会生成相对应的资源,使用方式和在 res/values 文件中定义的一样

android{
    //...
    productFlavors {
        miui {
            //...
           /**
            * resValue(String type,String name,String value)
            * type:生成字段的类型(id、string、bool等)
            * name:生成字段的常量名称
            * value:生成字段的常量值
            */
            resValue 'string', 'welcome','miui'
        }

        baidu {
            //...
            resValue 'string', 'welcome','baidu'
        }
    }

}
复制代码

当生成不同的渠道包时,通过 R.string.welcome 获取的值是不相同的,如生成的百度的渠道包时 R.string.welcome 的值为 baidu、生成小米渠道包时 R.string.welcome 的值为 miui,构建时生成的资源的位置在 build/generated/res/resValues/baidu/... 下面的 generated.xml 文件中,文件内容参考如下:

<?xml version="1.0" encoding="utf-8"?>
<resources>

    <!-- Automatically generated file. DO NOT MODIFY -->

    <!-- Values from product flavor: baidu -->
    <string name="welcome" translatable="false">baidu</string>

</resources>

复制代码

Java编译选项

在 Android Gradle 中还可以配置 Java 源代码的编译版本,这里使用到 compileOptions 方法, compileOptions 可配置三个属性:encoding、sourceCompatibility 和 targetCompatibility,通过这些属性来配置 Java 相关的编译选项,具体参考如下:

//配置Java编译选项
android {
    compileSdkVersion 26
    buildToolsVersion '26.0.2'
    compileOptions{
        //设置源文件的编码
        encoding = 'utf-8'
        //设置Java源代码的编译级别()
        sourceCompatibility = JavaVersion.VERSION_1_8
//        sourceCompatibility  "1.8"
//        sourceCompatibility  1.8
//        sourceCompatibility  "Version_1_8"
        //设置Java字节码的版本
        targetCompatibility = JavaVersion.VERSION_1_8
    }
}

复制代码

adb操作选项设置

adb 的全称是 Android Debug Bridge,adb 主要用来连接手机来进行一些操作,比如调试 Apk、安装 Apk、复制文件等操作,在 Android Gradle 中可借助 adpOptions 来配置,可配置的有两个属性:installOptions 和 timeOutInMs,也可以通过相应的 setter 方法来设置,具体参考如下:

android{
    //adb配置选项
    adbOptions{
        //设置执行adb命令的超时时间
        timeOutInMs = 5 * 1000
        /**
         * 设置adb install安装这个操作的设置项
         * -l:锁定应用程序
         * -r:替换已存在的应用程序
         * -t:允许测试包
         * -s:把应用程序安装到SD卡上
         * -d:允许应用程序降级安装
         * -g:为该应用授予所有运行时的权限
         */
        installOptions '-r', '-s'
    }    
}
复制代码

installOptions 的配置对应 adb install [-lrtsdg] 命令,如果安装、运行或调试 Apk 的时候,如果出现 CommandRejectException 可以尝试设置 timeOutInMs 来解决,单位是毫秒。

DEX选项配置

Android 中的源代码被编译成 class 字节码,在打包成 Apk 的时候又被 dx 命令优化成 Android 虚拟机可执行的 DEX 文件,DEX 格式的文件是专为 Android 虚拟机设计的,在一定程度上会提高其运行速度,默认情况下给 dx 分配的内存是 1024M,在 Android Gradle 中可以通过 dexOptions 的五个属性:incremental、javaMaxHeapSize、jumboMode、threadCount 和 preDexLibraries 来对 DEX 进行相关配置,具体参考如下:

android{
    //DEX选项配置
    dexOptions{
        //设置是否启用dx增量模式
        incremental true
        //设置执行dx命令为其分配的最大堆内存
        javaMaxHeapSize '4g'
        //设置是否开启jumbo模式,如果项目方法数超过65535,需要开启jumbo模式才能构建成功
        jumboMode true
        //设置Android Gradle运行dx命令时使用的线程数量,可提高dx执行的效率
        threadCount 2
        /**
         * 设置是否执行dex Libraries库工程,开启后会提高增量构建的速度,会影响clean的速度,默认为true
         * 使用dx的--multi-dex选项生成多个dex,为避免和库工程冲突,可设置为false
         */
        preDexLibraries true
    }
}
复制代码

自动清理未使用资源

Android 开发中打包 Apk 总是希望在相同功能的情况下 Apk 体积尽量小,那就要在打包之前删除没有使用的资源文件或打包时不将无用的资源打包到 Apk 中,可以使用 Android Lint 检查未使用的资源,但是无法清除一些第三方库中的无用资源,还可以使用 Resource Shrinking,可在打包之前检查资源,如果没有使用则不会被打包到 Apk 中,具体参考如下:

//自动清理未使用资源
android{
    buildTypes {
        release {
            //开启混淆,保证某些资源在代码中未被使用,以便于自动清理无用资源,两者配合使用
            minifyEnabled true
            /**
             * 打包时会检查所有资源,如果没有被引用,则不会被打包到Apk中,会处理第三方库不使用的资源
             * 默认不启用
             */
            shrinkResources true
            //开启zipalign优化
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
        debug{
        }
    }
    //...
}
复制代码

为防止有用资源未被打包到 Apk 中,Android Gradle 提供了 keep 方法来配置那些资源不被清理,在 res/raw/ 下新建一个 xml 文件来使用 keep 方法,参考如下:

<!--keep.xml文件-->
<?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/l_used"
    tools:shrinkMode="safe"/>
复制代码

可配置的三个属性:keep 表示要保留的资源文件,可使用以(,)分割的资源列表,可使用(*)作为通配符,discard 表示要移除的资源,和 keep 类似,shrinkMode 用于设置自动清理资源的模式,一般设置为 safe 即可,如果设置为 strict 则有可能清除可能会使用的资源。

此外,还可以使用 ProductFlavor 提供的方法 resConfigs 和 resConfig,可配置那些资源打包到 Apk 中,使用方式如下:

android{
    defaultConfig{
       //参数可以是Android开发时的资源限定符
        resConfigs 'zh'
        //...
    }
}
复制代码

上述自动清理资源的方式只是不打包到 Apk 中,在实际的项目中并没有被清除,可通过日志查看哪些资源被清理了,然后决定要不要在项目中清除。

突破65535方法限制

在 Android 开发中总会遇到方法数大于 65535 时出现异常,那为什么会有这个限制呢,因为 Java 源文件被打包成一个 DEX 文件,这个文件是优化过的、可在 Dalvik 虚拟机上可执行的文件,由于 Dalvik 在执行 DEX 文件的时候,使用了 short 来索引 DEX 文件中的方法,这就意味着单个 DEX 文件可被定义的方法最多只有 65535 个。解决办法自然是当方法数超过 65535 个的时候创建多个 DEX 文件。

从 Android 5.0 开始的 Android 系统使用 ART 的运行方式,原生支持多个 DEX 文件,ART 在安装 App 的时候执行预编译,把多个 DEX 文件合并成一个 oat 文件执行,在 Android 5.0 之前,Dalvik 虚拟机只支持单个 DEX 文件,要想突破单个 DEX 方法数超过 65535 的限制,需使用 Multidex 库,这里就不在赘述了。

总结

​本篇文章的很多内容都可以用到实际开发中,这篇文章也是在边学习边验证的情况下完成的,断断续续花了一周时间,距离上次更文已有一周时间,希望阅读此文能够对你有所帮助。

可以关注公众号:零点小筑(jzman-blog),一起交流学习。