Unity与Android Studio自动化打包:Gradle脚本实现APK/AAB文件智能重命名

📅 2026/7/21 5:38:39
Unity与Android Studio自动化打包:Gradle脚本实现APK/AAB文件智能重命名
1. 项目概述为什么我们需要自动化修改打包文件名如果你是一个Unity和Android StudioAS双栖的开发者或者你的团队正在使用Unity开发移动游戏或应用那么下面这个场景你一定不陌生项目临近上线你需要打一个包给测试或运营。在Unity里点击“Build And Run”生成一个Android工程。然后你打开Android Studio导入这个工程准备生成最终的APK或AAB文件。这时你需要在AS的“Build”菜单里手动修改输出文件的名称比如从默认的app-release.apk改成MyGame_V1.2.3_20240527_Release.apk。一次两次手动改改没问题。但当你处于快速迭代的测试阶段每天可能要打十几个包给不同渠道、不同环境时这个重复、琐碎且容易出错的手动操作就变成了一个效率黑洞。更糟糕的是在CI/CD持续集成/持续部署流水线中这个过程必须是完全自动化的容不得半点人工干预。这就是“适用Unity的AndroidStudio项目自动修改打包文件名称的方案”所要解决的核心痛点将打包输出文件的命名规则化、自动化彻底解放开发者的双手并确保打包流程的标准化和可追溯性。这个方案的价值远不止于“改个名字”。一个规范的、包含丰富信息的文件名本身就是一份重要的元数据。它可能包含应用名称、版本号、构建类型Debug/Release、构建时间、Git提交哈希、渠道标识等。这对于测试人员快速识别包体、运维人员追溯问题、市场人员管理分发版本都至关重要。因此实现自动化命名是提升团队协作效率和工程成熟度的一个关键步骤。2. 方案核心思路与架构选型要实现自动化修改AS打包后的文件名我们不能在Unity的构建后处理PostProcessBuild中直接操作因为那时AS工程还未被编译打包。核心思路必须转移到Android构建系统本身。Android项目使用Gradle作为构建工具而Gradle构建脚本build.gradle是高度可配置和可编程的。我们的方案就是通过修改Unity导出的Android项目中的Gradle构建脚本在打包任务执行时动态地、按照我们预设的规则来重命名最终输出的APK/AAB文件。2.1 为什么选择Gradle而不是其他方式你可能会想到其他方法比如写一个Python脚本在AS打包完成后扫描并重命名文件或者利用AS的“Run Configuration”配合命令行参数。但这些方法都存在明显缺陷外部脚本需要在CI流水线中额外增加步骤与构建流程耦合度低容易遗漏或出错且无法在AS的图形界面构建时生效。AS配置难以实现复杂、动态的命名规则如嵌入构建时间、Git信息。而Gradle方案的优势在于原生集成Gradle是Android官方的构建系统修改其脚本是“正统”做法与构建生命周期无缝集成。时机精准我们可以在文件真正被生成之前就定义好它的名字确保从构建到输出的整个过程是连贯的。灵活强大可以利用Gradle的API获取项目属性、系统环境变量、执行Shell命令获取Git信息从而组装出任意复杂的文件名。一次配置处处生效无论是在Android Studio里点击“Build Bundle(s) / APK(s)”还是通过命令行执行./gradlew assembleRelease甚至是CI服务器上的无头构建命名规则都会自动生效。2.2 方案实施路径规划我们的目标是将这个功能做成一个“即插即用”的模块方便在任何一个Unity项目中复用。因此整体实施路径分为以下几步创建Unity编辑器工具在Unity中开发一个Editor脚本或窗口让开发者可以方便地配置命名规则如格式模板。设计命名规则模板定义一套灵活可配的模板语法例如{ProductName}_{Version}_{BuildType}_{Date}_{Channel}.apk。实现Gradle脚本注入在Unity构建Android项目时自动将我们编写好的、包含重命名逻辑的Gradle脚本片段插入到导出的项目的build.gradle文件中。测试与验证确保在Debug/Release、APK/AAB等不同构建变体下文件名都能被正确修改。3. 核心细节解析Gradle的重命名魔法理解了整体思路后我们来深入最核心的部分如何在Gradle中修改输出文件名。这里的关键是理解Android Gradle插件提供的变体VariantAPI。一个Android项目可以有多种构建类型BuildType如debug, release和产品风味ProductFlavor常用于区分渠道。它们的组合被称为“构建变体”BuildVariant。例如release类型 huawei风味就构成了huaweiRelease这个变体。每个变体在打包时都会生成对应的输出文件。我们的任务就是拦截这个生成过程在文件产生前给它改名。具体操作在模块级通常是app模块的build.gradle文件中进行。3.1 基础重命名代码剖析下面是一个最基础的示例展示如何在android配置块中修改APK的输出名称android { ... applicationVariants.all { variant - variant.outputs.all { output - def outputFile output.outputFile if (outputFile ! null outputFile.name.endsWith(.apk)) { // 定义新文件名 def newName MyApp_${variant.buildType.name}_v${defaultConfig.versionName}.apk // 重新设置输出文件路径和名称 output.outputFile new File(outputFile.parent, newName) } } } }代码解读applicationVariants.all遍历所有的应用构建变体对于APK。如果是AAB则需要使用libraryVariants.all对于旧插件或直接通过androidComponentsAPI推荐新方式。variant.outputs.all遍历该变体的所有输出现在通常每个变体只有一个输出但这里用all是兼容写法。output.outputFile获取当前输出文件的预期File对象。newName拼接新的文件名。这里使用了变体的属性buildType.name和默认配置中的版本名versionName。output.outputFile new File(...)这是最关键的一步将输出文件指向一个新的File对象从而实现了重命名。注意对于较新版本的Android Gradle插件AGP 7.0直接操作outputFile的方式可能已被标记为废弃。官方推荐使用新的androidComponentsAPI。但考虑到Unity导出的项目AGP版本通常不会太激进且旧API更直观我们在初始方案中仍可使用它但需要知晓其未来可能的变化。3.2 设计一个强大的命名模板引擎上面的例子是硬编码。我们需要一个可配置的模板系统。假设我们希望在Unity中配置这样一个模板字符串{PRODUCT_NAME}_{BUNDLE_VERSION}_{BUILD_TYPE}_{DATE_YYYYMMDD}_{TIME_HHmm}_{CHANNEL}在Gradle中我们需要解析这个模板并将其中的占位符替换为动态值。这些值的来源包括PRODUCT_NAME: 来自defaultConfig.applicationId或从AndroidManifest.xml读取更简单的可以从project.name获取。BUNDLE_VERSION: 来自defaultConfig.versionName。BUILD_TYPE: 来自variant.buildType.name。DATE/TIME: 通过Groovy的new Date()获取并格式化。CHANNEL: 这通常需要自定义。我们可以通过productFlavors来定义渠道或者通过读取一个配置文件、环境变量来传递。在Gradle中实现模板替换的示例android { // 假设我们从外部如gradle.properties或环境变量获取到一个模板字符串 // 这里为了演示我们直接定义一个变量。实际中这个值应由Unity工具注入。 def fileNameTemplate project.hasProperty(CUSTOM_FILE_NAME_TEMPLATE) ? project.property(CUSTOM_FILE_NAME_TEMPLATE) : {PRODUCT_NAME}_{VERSION}_{BUILD_TYPE}.apk applicationVariants.all { variant - variant.outputs.all { output - def outputFile output.outputFile if (outputFile ! null outputFile.name.endsWith(.apk)) { // 准备替换数据映射 def substitutionMap [ {PRODUCT_NAME}: project.name, {VERSION}: variant.versionName, {BUILD_TYPE}: variant.buildType.name.toUpperCase(), {DATE}: new Date().format(yyyyMMdd), {TIME}: new Date().format(HHmm), // 渠道信息假设我们通过flavor获取 {CHANNEL}: variant.flavorName ?: official ] // 执行模板替换 def newName fileNameTemplate substitutionMap.each { key, value - newName newName.replace(key, value) } // 确保文件名是合法的替换掉可能存在的非法文件名字符 newName newName.replaceAll([\\\\/:*?\|], _) output.outputFile new File(outputFile.parent, newName) // 打印日志便于调试 println(Renaming output to: ${output.outputFile.name}) } } } }这个脚本展示了核心逻辑定义模板、准备数据、替换占位符、清理非法字符、应用新名称。println语句在构建时会输出到Gradle控制台非常有助于调试。4. 实操过程从Unity到Android Studio的无缝集成理论有了现在我们来一步步实现一个完整的、可集成的方案。我们的目标是创建一个Unity编辑器扩展它将负责配置命名规则并在构建时自动将配置“注入”到生成的Android项目中。4.1 第一步创建Unity编辑器配置工具在Unity项目的Assets/Editor目录下创建一个脚本例如AndroidBuildNameConfigurator.cs。using UnityEditor; using UnityEngine; using System.IO; public class AndroidBuildNameConfigurator : EditorWindow { private string fileNameTemplate {PRODUCT_NAME}_{VERSION}_{BUILD_TYPE}_{DATE}_{CHANNEL}.apk; private string channelName official; [MenuItem(Tools/Android 打包文件名配置)] public static void ShowWindow() { GetWindowAndroidBuildNameConfigurator(AS打包名配置); } void OnGUI() { GUILayout.Label(文件名模板配置, EditorStyles.boldLabel); EditorGUILayout.HelpBox(可用占位符\\n{PRODUCT_NAME} - 产品名\\n{VERSION} - 版本号\\n{BUILD_TYPE} - 构建类型(DEBUG/RELEASE)\\n{DATE} - 日期(yyyyMMdd)\\n{TIME} - 时间(HHmm)\\n{CHANNEL} - 渠道名, MessageType.Info); fileNameTemplate EditorGUILayout.TextField(模板, fileNameTemplate); channelName EditorGUILayout.TextField(渠道名, channelName); if (GUILayout.Button(保存配置并应用下次构建生效)) { SaveConfig(); EditorUtility.DisplayDialog(提示, 配置已保存。此配置将在下次构建Android项目时生效。, 确定); } } void SaveConfig() { // 将配置保存到一个Unity可读的路径例如Resources文件夹或特定的Editor目录 string configPath Path.Combine(Application.dataPath, Editor/AndroidBuildNameConfig.json); var config new ConfigData { template fileNameTemplate, channel channelName }; File.WriteAllText(configPath, JsonUtility.ToJson(config)); AssetDatabase.Refresh(); } [System.Serializable] private class ConfigData { public string template; public string channel; } }这个编辑器窗口让开发者可以直观地设置模板和渠道。配置会被保存为一个JSON文件。4.2 第二步编写Gradle脚本模板我们需要准备一个“模板”Gradle脚本文件它包含了我们上一节解析的核心重命名逻辑但其中的关键变量如模板字符串、渠道名是待填充的占位符。将这个文件放在Unity项目的Assets/Plugins/Android目录下Unity在构建时会自动将其复制到安卓工程中。创建一个文件例如customBuild.gradle.template// 此文件由Unity构建过程自动生成请勿手动修改 android { // 文件名模板由Unity构建时注入 def customFileNameTemplate \{{FILE_NAME_TEMPLATE}}\ // 渠道名由Unity构建时注入 def customChannelName \{{CHANNEL_NAME}}\ applicationVariants.all { variant - variant.outputs.all { output - def outputFile output.outputFile if (outputFile ! null (outputFile.name.endsWith(.apk) || outputFile.name.endsWith(.aab))) { // 准备替换数据 def substitutionMap [ \{PRODUCT_NAME}\: project.name, \{VERSION}\: variant.versionName, \{BUILD_TYPE}\: variant.buildType.name.toUpperCase(), \{DATE}\: new Date().format(yyyyMMdd), \{TIME}\: new Date().format(HHmm), \{CHANNEL}\: customChannelName ] // 执行替换 def newName customFileNameTemplate substitutionMap.each { key, value - newName newName.replace(key, value.toString()) } // 清理非法字符 newName newName.replaceAll([\\\\/:*?\\\|], _) output.outputFile new File(outputFile.parent, newName) println(\CustomBuild: Renaming output to: \${output.outputFile.name}\) } } } }4.3 第三步实现构建后处理PostProcessBuild进行脚本注入这是连接Unity和Gradle的桥梁。我们需要在Unity构建完成后修改导出的Android项目中的build.gradle文件。在Assets/Editor下创建AndroidBuildPostProcessor.csusing UnityEditor; using UnityEditor.Android; using System.IO; using UnityEngine; public class AndroidBuildPostProcessor { [InitializeOnLoadMethod] public static void RegisterPostProcess() { // 注册构建完成后的回调 BuildPlayerWindow.RegisterBuildPlayerHandler(BuildPlayerHandler); } private static void BuildPlayerHandler(BuildPlayerOptions options) { // 1. 先执行标准构建流程 BuildPipeline.BuildPlayer(options); // 2. 如果是Android平台进行后处理 if (options.target BuildTarget.Android) { PostProcessAndroidBuild(options.outputPath); } } private static void PostProcessAndroidBuild(string outputPath) { // 读取之前在Editor中保存的配置 string configPath Path.Combine(Application.dataPath, Editor/AndroidBuildNameConfig.json); if (!File.Exists(configPath)) { Debug.LogWarning([AndroidBuildPostProcessor] 未找到文件名配置文件将使用默认命名。); return; } string configJson File.ReadAllText(configPath); var config JsonUtility.FromJsonConfigData(configJson); // 定位到Unity导出的主模块build.gradle文件 // 通常路径是: [outputPath]/[projectName]/app/build.gradle 或 [outputPath]/launcher/build.gradle // 具体路径取决于Unity版本和导出设置这里需要根据实际情况调整 string gradleFilePath Path.Combine(outputPath, app, build.gradle); if (!File.Exists(gradleFilePath)) { // 尝试另一种常见路径 gradleFilePath Path.Combine(outputPath, launcher, build.gradle); } if (!File.Exists(gradleFilePath)) { Debug.LogError($[AndroidBuildPostProcessor] 找不到build.gradle文件在: {gradleFilePath}); return; } // 读取Gradle模板文件 string templatePath Path.Combine(Application.dataPath, Plugins/Android/customBuild.gradle.template); if (!File.Exists(templatePath)) { Debug.LogError($[AndroidBuildPostProcessor] 找不到Gradle模板文件: {templatePath}); return; } string templateContent File.ReadAllText(templatePath); // 替换模板中的占位符 templateContent templateContent.Replace({{FILE_NAME_TEMPLATE}}, config.template); templateContent templateContent.Replace({{CHANNEL_NAME}}, config.channel); // 读取原始的build.gradle内容 string originalGradleContent File.ReadAllText(gradleFilePath); // 关键将我们的自定义配置插入到android {} 块中。 // 我们需要找到一个合适的位置插入通常是在android { ... } 块内的末尾在dependencies之前。 // 这里采用一个简单的策略在android {之后插入。 string insertionPoint android {; if (originalGradleContent.Contains(insertionPoint)) { int insertIndex originalGradleContent.IndexOf(insertionPoint) insertionPoint.Length; // 在android { 后面加上换行然后插入我们的自定义配置 string modifiedContent originalGradleContent.Insert(insertIndex, \n templateContent \n); File.WriteAllText(gradleFilePath, modifiedContent); Debug.Log($[AndroidBuildPostProcessor] 已成功将自定义打包命名配置注入到: {gradleFilePath}); } else { Debug.LogError($[AndroidBuildPostProcessor] 在build.gradle中找不到 {insertionPoint}); } } [System.Serializable] private class ConfigData { public string template; public string channel; } }这段代码的要点与风险路径查找Unity导出Android项目的结构可能因版本而异如使用Gradle、Internal、或导出为Project。上述代码中的路径查找逻辑可能需要根据你的Unity版本和导出设置进行调整。最可靠的方法是构建一次后手动检查输出目录的结构。字符串插入直接通过字符串查找和插入来修改Gradle文件是一种简单但脆弱的方式。如果原始的build.gradle结构非常复杂或有特殊格式可能会破坏文件。更稳健的做法是使用真正的Gradle解析库但复杂度极高。对于由Unity生成的、结构相对固定的build.gradle字符串操作在大多数情况下是可行的。备份在修改关键文件前最好先做一个备份。4.4 第四步测试与验证完成以上步骤后进行测试在Unity编辑器中打开配置窗口Tools/Android 打包文件名配置设置模板如{PRODUCT_NAME}_{VERSION}_{BUILD_TYPE}_{CHANNEL}.apk渠道设为googleplay。执行Android构建File - Build Settings - Build。构建完成后用Android Studio打开导出的项目。在AS中选择Build-Generate Signed Bundle / APK...选择APK或AAB并完成签名步骤。观察构建输出目录通常是app/build/outputs/apk/release/或app/build/outputs/bundle/release/查看生成的最终文件是否按照你的模板正确命名例如MyGame_1.0.0_RELEASE_googleplay.apk。5. 常见问题与排查技巧实录在实际操作中你几乎一定会遇到一些问题。下面是我在多次实施类似方案中踩过的坑和总结的技巧。5.1 问题一构建失败Gradle报语法错误现象在Android Studio中同步或构建项目时Gradle Sync失败提示build.gradle文件某行有语法错误。排查思路检查注入的Gradle脚本首先去查看被修改后的app/build.gradle文件。重点检查我们插入的那段自定义脚本。常见的语法错误包括引号不匹配Groovy中单引号和双引号有时有区别。在我们的模板替换中要确保生成的字符串引号是闭合的。这就是为什么在模板文件里我们给字符串变量加了转义的双引号\{{FILE_NAME_TEMPLATE}}\。花括号不匹配检查android { ... }块以及我们插入的代码块是否完整闭合。使用了错误的变量或方法确保脚本中引用的Gradle API对象如variantoutput在当前的上下文和AGP版本中是有效的。查看完整错误日志在AS的“Build”输出窗口或命令行运行./gradlew build --stacktrace查看更详细的错误信息通常会精确到行号。版本兼容性确认你的Gradle脚本语法与项目中使用的Android Gradle PluginAGP版本兼容。例如output.outputFile在AGP 7.0可能已废弃。如果遇到废弃警告可以研究使用新的androidComponentsAPI进行替代。实操心得始终保留一份原始的、未修改的build.gradle备份。在调试时可以手动将我们插入的脚本注释掉看项目是否能正常同步以此快速定位问题是否由我们的脚本引起。5.2 问题二文件名没有被修改还是默认的app-release.apk现象构建过程成功但输出文件的名称没有变化。排查思路确认脚本是否成功注入打开导出的app/build.gradle搜索你定义的变量名如customFileNameTemplate或println输出的日志内容看它们是否存在。检查构建变体Variant我们的脚本是挂在applicationVariants.all上的。确保你构建的正是“应用”变体例如assembleRelease生成APK而不是其他变体如bundleRelease生成AAB。对于AAB需要使用libraryVariants.allAGP 3.x/4.x或新的变体API。一个更通用的方法是使用android.applicationVariants.all对于APK和android.libraryVariants.all对于AAB旧版分别处理或者使用新的androidComponents.onVariantsAPI。查看Gradle构建日志我们在脚本中加入了println语句。在Android Studio的“Build”输出窗口或命令行执行./gradlew assembleRelease --info仔细搜索输出中是否有“CustomBuild: Renaming output to:”这行日志。如果没有说明我们的脚本逻辑可能没有被执行到。文件后缀匹配检查脚本中的条件判断outputFile.name.endsWith(.apk)。如果你在打AAB包后缀是.aab这个条件就不成立。可以修改为outputFile.name.endsWith(.apk) || outputFile.name.endsWith(.aab)或者更通用地判断文件父目录。实操心得在Gradle脚本中多使用println进行调试。这是调试Gradle脚本最直接有效的方法可以打印出变量的值、代码的执行路径帮助你理解脚本在构建过程中的实际行为。5.3 问题三文件名中包含非法字符或格式不符预期现象文件名被修改了但出现了空格、冒号等非法字符或者日期时间格式不对。排查思路清理非法字符我们的脚本中有一行newName newName.replaceAll([\\\\/:*?\\\|], _)它使用正则表达式将Windows/Unix文件名中的非法字符替换为下划线。检查这个正则表达式是否覆盖了你遇到的情况。例如空格通常被认为是合法但不受欢迎的你可以额外添加\\s到正则表达式中。检查数据源检查替换占位符的数据源是否正确。例如project.name可能包含空格或特殊字符。variant.versionName如果定义为“1.0.0-beta”其中的连字符是合法的。你需要决定是否要保留这些字符。一种更严格的做法是对所有用于文件名的动态部分都进行一次清理。日期格式确保new Date().format(yyyyMMdd)产生的格式符合你的要求。Groovy的日期格式化语法与Java相同。实操心得对文件名进行“净化”处理。不要完全信任任何动态数据源。定义一个统一的净化函数在最终拼接文件名前对所有组成部分进行处理移除或替换掉所有非字母、数字、下划线、连字符、点的字符。这能最大程度保证文件名的兼容性。5.4 问题四Unity导出项目结构变化导致脚本注入失败现象Unity版本升级后或者更改了导出设置如从“Export Project”改为“Build And Run”后处理脚本找不到build.gradle文件了。排查思路手动探查结构用新的设置构建一次然后亲自打开输出文件夹查看其目录结构。找到主模块通常是app或launcher下的build.gradle文件。动态路径探测改进我们的PostProcessAndroidBuild方法。不要写死路径可以尝试遍历输出目录寻找包含build.gradle文件的子目录并通过读取文件内容判断其是否为主要的应用模块例如检查文件中是否包含applicationId或com.android.application插件。兼容性处理为不同的Unity版本或导出模式准备多套路径查找逻辑并记录日志方便失败时排查。实操心得将路径查找逻辑模块化并增加日志输出。在尝试每个可能路径时都输出一条Debug日志。这样当方案在新环境下失效时你可以通过日志快速看到脚本尝试了哪些路径以及最终为何失败从而快速调整。这个方案将Unity的灵活配置与Android Gradle构建系统的强大能力结合了起来。它不仅仅是一个“改名工具”更是一个将构建产物元数据化的实践。一旦跑通你可以轻松地扩展它例如从CI环境变量中读取构建编号、从Git命令中获取提交哈希并嵌入文件名让每一个产出的包都自带完整的“身份信息”。这为团队的自动化测试、版本管理和问题追溯打下了坚实的基础。