返回

Android 构建:BuildConfig 字段动态生成 APK 文件名

Android

Android 构建:根据 BuildConfig 字段动态生成 APK 文件名

构建 Android 项目时,我们可能需要根据不同的构建配置生成具有不同名称的 APK 文件。例如,根据是否启用蓝牙检查功能,来区分生成的 APK 文件。遇到过一个棘手的问题:怎样在 Gradle 中,利用 buildConfigField 的值动态设置 APK 输出文件名?折腾了许久,终于搞定。这里,我把我的经验分享给大家。

问题

假设我们定义了一个名为 NO_BLUETOOTH_CHECKboolean 类型 buildConfigField。目标是:

  • NO_BLUETOOTH_CHECKfalse 时,APK 文件名为 myproject_BTOn_r_10.0.2.apk
  • NO_BLUETOOTH_CHECKtrue 时,APK 文件名为 myproject_BTOff_r_10.0.2.apk

最初尝试的代码,以及各种修改版本,总是遇到各种编译错误。问题出在哪儿呢?

问题分析

原代码的主要问题在于访问 buildConfigFields 的方式,以及对 Gradle 构建生命周期的理解不够深入。variant.buildTypes.buildConfigFields 这种方式是拿不到正确值的。应该直接从 variant 对象获取。而且,对输出文件(output)的操作,需要放在一个正确的作用域中。

解决方案

下面提供几种可行的解决方案,一步步来,总有一款适合你。

方案一:直接在 android 闭包内修改

这是最直接的方法,在 android 闭包内处理变体和输出。

android {
    // ... 其他配置 ...
    buildTypes {
        release {
            buildConfigField "boolean", "NO_BLUETOOTH_CHECK", "false"
            // ... 其他配置 ...
        }
        debug {
            buildConfigField "boolean", "NO_BLUETOOTH_CHECK", "true"
           // ... 其他配置 ...
        }
    }

    applicationVariants.all { variant ->
        variant.outputs.all { output ->
            def btValue = variant.buildConfig.NO_BLUETOOTH_CHECK ? "BTOff" : "BTOn"
            def buildType = variant.buildType.name == 'release' ? "r" : "d"
            outputFileName = "${rootProject.name}_${btValue}_${buildType}_${variant.versionName}.${variant.versionCode}.apk"
        }
    }
}

原理:

  1. applicationVariants.all { variant -> ... }:遍历所有应用程序变体(例如,release、debug)。
  2. variant.outputs.all { output -> ... }:遍历当前变体的所有输出(通常情况下,每个变体只有一个输出)。
  3. variant.buildConfig.NO_BLUETOOTH_CHECK:直接访问 buildConfigField 的值。这个值在构建过程中会自动生成到 BuildConfig 类中。
  4. 利用三元运算符(? :)构建 btValue 字符串。
  5. 根据构建类型是 release 或者debug 变更为更短的表示,提高辨识度。
  6. 使用字符串模板构建最终的文件名。

操作步骤:

  1. 将上述代码复制到你的 build.gradle 文件(Module 级别的)的 android 闭包内。
  2. 同步 Gradle 项目。
  3. 构建项目,APK 文件名将根据 NO_BLUETOOTH_CHECK 的值自动改变。

方案二:使用 setProperty 方法(推荐)

这种方式更加符合 Gradle 的惯用方式,利用 setProperty 方法修改输出文件的属性。

android {
 // ... 其他配置 ...
    buildTypes {
        release {
            buildConfigField "boolean", "NO_BLUETOOTH_CHECK", "false"
             // ... 其他配置 ...
        }
        debug {
            buildConfigField "boolean", "NO_BLUETOOTH_CHECK", "true"
            // ... 其他配置 ...
        }
    }

    applicationVariants.all { variant ->
        variant.outputs.all { output ->
            def btValue = variant.buildConfig.NO_BLUETOOTH_CHECK ? "BTOff" : "BTOn"
            def buildType = variant.buildType.name == 'release' ? "r" : "d"
            output.outputFileName = "${rootProject.name}_${btValue}_${buildType}_${variant.versionName}.${variant.versionCode}.apk"

          //Gradle 5.0 以上
          //output.outputFileName.set("${rootProject.name}_${btValue}_${buildType}_${variant.versionName}.${variant.versionCode}.apk")
        }
    }
}

原理:

与方案一类似,但这里使用 output.outputFileName = ...直接对文件名进行修改。这种方式比直接设置outputFileName更加明确。setProperty 方法提供了更好的 API 封装,是 Gradle 推荐使用的。

注意:

  • output.outputFileName = 的方式适用大部分版本。
  • Gradle 5.0 以上的版本更推荐output.outputFileName.set()的方式修改输出文件名字。

操作步骤: 与方案一相同。

方案三: 使用闭包分离逻辑

如果逻辑比较复杂,可以将文件名生成的逻辑提取到一个单独的闭包中。

def getApkName = { variant, output ->
    def btValue = variant.buildConfig.NO_BLUETOOTH_CHECK ? "BTOff" : "BTOn"
    def buildType = variant.buildType.name == 'release' ? "r" : "d"
    return "${rootProject.name}_${btValue}_${buildType}_${variant.versionName}.${variant.versionCode}.apk"
}

android {
 // ... 其他配置 ...
    buildTypes {
        release {
            buildConfigField "boolean", "NO_BLUETOOTH_CHECK", "false"
            // ... 其他配置 ...
        }
        debug {
            buildConfigField "boolean", "NO_BLUETOOTH_CHECK", "true"
             // ... 其他配置 ...
        }
    }
    applicationVariants.all { variant ->
        variant.outputs.all { output ->
            output.outputFileName = getApkName(variant, output)

             //Gradle 5.0 以上
             //  output.outputFileName.set(getApkName(variant,output))
        }
    }
}

原理:

将文件名生成的逻辑封装到 getApkName 闭包中,使代码更具可读性和可维护性。

操作步骤: 与方案一相同。

进阶使用技巧: versionName包含"_"处理

有用户反馈, 在versionName中带有"_"的情况下, 脚本行为不够完美,文件名生成的版本号信息截取有误. 这就需要我们做更细致的处理:

def getApkName = { variant, output ->
    def btValue = variant.buildConfig.NO_BLUETOOTH_CHECK ? "BTOff" : "BTOn"
    def buildType = variant.buildType.name == 'release' ? "r" : "d"

     // 改进的版本名称和版本号获取方式,处理versionName带下划线的情况
     def versionName = variant.versionName
     def versionCode = variant.versionCode
    return "${rootProject.name}_${btValue}_${buildType}_${versionName}.${versionCode}.apk"
}

android {
    // ... 其他配置 ...
     // build.gradle(Project) 的 allprojects 里配置 versionName, versionCode,统一管理
    buildTypes {
        release {
            buildConfigField "boolean", "NO_BLUETOOTH_CHECK", "false"
            // ... 其他配置 ...
        }
        debug {
            buildConfigField "boolean", "NO_BLUETOOTH_CHECK", "true"
           // ... 其他配置 ...
        }
    }

    applicationVariants.all { variant ->
        variant.outputs.all { output ->
             output.outputFileName = getApkName(variant, output)
              //Gradle 5.0 以上
            //output.outputFileName.set(getApkName(variant, output))
        }
    }
}

// 推荐把 versionName, versionCode 定义挪到 build.gradle(Project) 的 allprojects 里进行配置
// 方便多 Module 情况下的统一版本管理。

改进点说明

  1. 直接使用 variant.versionName 和 variant.versionCode : 避免手动拼接版本信息,规避潜在问题.

  2. 版本号和版本名集中管理 (可选) :为了更好的维护, 将 versionNameversionCode 的定义移动到项目的根 build.gradle 文件中的 allprojectssubprojects 块内,这样可以对整个项目进行统一管理。这是一个很好的工程实践,强烈推荐。

总结

这几种方案都能实现根据 buildConfigField 动态生成 APK 文件名。具体选择哪种,取决于个人偏好和项目实际情况。推荐使用方案二或者方案三结合进阶使用技巧,可读性以及健壮性更好。记住,要理解 Gradle 的构建生命周期,才能更好地解决这类问题。搞明白原理,这类问题就迎刃而解了。