ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Jenkins+Unity在Windows下自动打包APK完整指南与踩坑实录

Jenkins+Unity在Windows下自动打包APK完整指南与踩坑实录 前阵子帮团队搭了一套用 Jenkins 在 Windows 上自动打包 Unity 工程 APK 的流水线整个过程可以说是步步踩坑。最让人头疼的倒不是 Jenkins 本身而是它在 Windows 环境下调用 Unity 命令行打包时各种“打不出来”的问题——场景加载不出来、Gradle 直接失败、SDK 路径找不到、Unity 编辑器闪退甚至 Jenkins 服务跑起来后连 Unity 的许可证都认不出来。这篇文章我就把从 Jenkins 安装、环境配置到最终跑通 Unity 打包 APK 的完整链路梳理一遍。重点会放在那些文档里不常写、但实际工作中一定会遇到的坑上特别是“场景打不出来”“APK 没生成”这类问题。愿意看完这篇的人大概率能省下一周时间。1. 整体链路梳理Jenkins 怎么和 Unity 协作的1.1 为什么非要把 Unity 打包丢给 Jenkins很多团队一开始打包 APK 都是靠开发本地手动操作打开 Unity Hub切到 Android 平台等 Gradle 编译最后一键 Build。这条流程看着简单但一旦项目到了多分支迭代、每天要出测试包、或者需要定时构建的时候问题就全冒出来了。手动打包至少有三个硬伤。第一它依赖某个开发者的电脑环境哪天换个人打包可能就失败因为不同人的 SDK 路径、Unity 版本、安卓模块可能不一致。第二打包过程极其耗时通常一个中大型 Unity 项目出 APK 要 20 到 40 分钟这段时间如果人盯着就完全浪费了。第三手动操作容易出错比如选了错误的场景、忘了勾选 IL2CPP、导出路径写错目录全是细节坑。Jenkins 解决的就是这些问题。它本质上是一个任务调度器你可以把它理解成一个准点上班的流水线工人。它帮你定时拉代码、调 Unity 执行打包命令、最终产出 APK并把构建日志全部记录下来。更关键的是Jenkins 可以做到“不复用人”只要环境配置对了它每次打包用的都是同一套逻辑结果稳定可复制。1.2 Jenkins 调用 Unity 的核心原理很多人第一次接触这个方向时会误以为 Jenkins 里要装一个 Unity 插件然后插件直接点按钮就能打包。这是个很普遍的误解。真实的做法是Jenkins 在自己的构建任务里执行一段命令行脚本脚本去调用 Unity 编辑器程序Unity.exe并且通过参数告诉 Unity 要去执行哪个项目、用哪个方法、打什么平台。Unity 收到这些指令后会以批处理模式batchmode启动不加载图形界面直接在后台执行打包逻辑执行完毕后把 APK 写到指定目录然后退出。这就意味着前提是你必须在跑 Jenkins 的那台 Windows 机器上装好完整版的 Unity 编辑器并且工程里还需要有一段专门的静态打包方法通常会写在 Editor 目录下。没有这个方法Jenkins 就算把 Unity 喊起来了也无事可做。调用关系大概是这样的Jenkins 任务 → 执行 .bat 脚本 → 启动 Unity.exe 带参数 → Unity 进入批处理模式 → 执行打包脚本 → 输出 APK 日志 → 退出 → Jenkins 读取结果理解了这条链路之后你才会明白为什么这么多坑集中出现在“环境配置”和“参数传递”上。因为整条链路里有好几个环节是黑盒的任何一环出问题最后看到的都一样——没产出 APK。2. Windows 构建环境的搭建JDK、安卓 SDK、NDK、Gradle2.1 JDK 版本到底选哪个才不会被 Unity 骂Unity 打包 Android APK 时其实是靠 Android Gradle 插件完成的而 Gradle 本身又依赖 JDK。所以 JDK 版本不对是整个构建失败里最隐蔽又最常见的元凶之一。Unity 2019 到 Unity 2021 之间推荐使用 JDK 8 或 JDK 11很多是基于 OpenJDK 改的版本。如果你用的是 Unity 2021.3 LTS 或更新版本那就强烈建议直接用 JDK 11某些版本甚至支持 JDK 17。如果在 JDK 版本上踩坑最常见的报错是类似 “Unsupported class file major version 61” 这类。看到这个基本就是当前 Unity 内置的 Gradle 版本认不出新 JDK 编译出来的 class 文件。我在配置 CI 机器时路径是这样规划的可以直接照抄JDK 安装路径C:\Program Files\Java\jdk-11.0.18设置系统环境变量JAVA_HOME指向上面的路径在 PATH 里追加%JAVA_HOME%\bin有个细节需要注意Jenkins 安装成 Windows 服务后它启动时的系统环境变量和你登录桌面看到的环境变量可能不是同一份。所以在 Jenkins 的系统配置里最好再单独设置一次全局属性不然很容易出现明明命令行里 java -version 正常Jenkins 构建却说找不到 JDK。2.2 安卓 SDK 和 NDK 的安装方式如果你本机已经装了 Android Studio并且能手动从 Unity 打包 APK那说明 SDK 是有的。可问题在于 Jenkins 跑的时候往往用的是另一个 Windows 系统账号它访问不到用户目录下的 SDK 配置。我的建议是在规划 CI 机器时把 Android SDK 放到一个公共的、没有空格的目录下。比如D:\Android\SDK而不是默认的C:\Users\你的用户名\AppData\Local\Android\Sdk。为什么要强调没有空格因为后面 Unity 和 Gradle 拼接路径的时候空格会导致很多兼容问题。虽然理论上带引号也能处理但能避就避省得为这种小事排查半天。NDK 方面不是所有项目都用但如果你的 Unity 项目选了 IL2CPP 脚本后端那基本就绑定了 NDK。Unity 各版本推荐的 NDK 版本是不同的比如 Unity 2021.3 官方建议的是 r23b。直接在 Unity Hub 里添加模块时可以一并勾选 Android SDK NDK Tools这样 Unity 会自动下到它自己认的版本但我个人在 CI 上还是更倾向手动指定因为可控性更高。在 Jenkins 的系统设置里建议明确加上这样几个变量ANDROID_HOMED:\Android\SDKANDROID_SDK_ROOTD:\Android\SDKANDROID_NDK_HOMED:\Android\NDKNDK_ROOTD:\Android\NDK2.3 Gradle 的版本陷阱Unity 工程里实际上不只是 Unity 自己那个打包逻辑Android 打包最终是交给 Gradle 工程的。Unity 发布导出时会自动生成一个 Gradle 工程如果你选择的是 Export Project 模式或者直接内置调用 Gradle 来构建 APK。这里最常见的坑是 Gradle 下载失败。Unity 在构建时如果检测到默认的 Gradle 版本和项目的 gradle-wrapper.properties 不一致会尝试去下载新版 Gradle。而国内访问 Gradle 官方发行版的网速大家都懂经常下载到一半超时然后整个构建过程就卡死或者直接失败。解决方案有两种思路。一种是在构建机上预先把 Gradle 完整包下载好放到C:\Users\用户名\.gradle\wrapper\dists\目录对应的版本目录里。另一种是在 Unity 的 Preferences 设置里手动填入一个 Gradle 路径让 Unity 直接用你本地的 Gradle。第二种我更推荐因为对 Jenkins 构建来说只要这个路径设置在工程上或者构建机全局配置里构建速度会有明显提升。如果你发现卡在 Gradle 相关的报错别急着去调 Unity 工程先确认一下 Gradle 能不能离线构建。跑一下gradle -v检查安装再用命令行进入导出的 Android 工程目录试一把gradle assembleRelease只要能出 APK问题基本就定位到 Unity 调用 Gradle 的衔接环节了。3. Jenkins 安装与初始化配置3.1 安装包选择Windows Service 还是 jar 方式Jenkins 在 Windows 上常见的安装方式有两种一种是下载官方提供的 Windows 安装包它会将 Jenkins 安装成 Windows 服务另一种是直接下载 jenkins.war然后用java -jar jenkins.war --httpPort8080在命令行里启动。如果你的 Jenkins 是长期要跑的建议用 Windows 服务方式。因为服务支持开机自启就算没人登录服务器Jenkins 也能正常构建。但也正因为它是系统服务默认账号是 Local System这个账号的权限和网络环境比较特殊后面调用 Unity 时可能有许可证问题。如果你只是临时测试、不打算常驻可以直接用命令行窗跑 war 包。这种方式调试起来更直观因为你能直接看到 Jenkins 的启动日志以及后续构建过程中有没有奇怪的异常抛出来。我平时排障时会先用 jar 方式跑一遍确认链路通了再切回服务模式。3.2 初始化配置和插件安装要点Jenkins 首次启动时会要求你输入管理员初始密码这个密码在安装目录的secrets/initialAdminPassword文件里能找到。接着是选择插件安装方案这里我不建议全选推荐因为你实际上用不到那么多插件装多了反而拖慢启动。真正用得到的插件其实就几类代码拉取相关Git Plugin构建触发器相关可以用自带的 Poll SCM 或定时构建插件非必须构建信息展示Role-based Strategy权限管理、Build Name and Description Setter参数化构建Parameterized Trigger Plugin如果你需要传自定义参数进打包脚本Unity 插件本身不用装。网上偶尔会看到 Unity3d Plugin但我试过几次它是调用开个 Unity 编辑器窗口的那种方式实际 CI 自动化中用不太上。我们的目标是用命令行这个思路别跑偏了。安装完成后先做一个关键动作在 “系统管理 → 系统配置 → 全局属性” 里勾选环境变量把上一节提到的 JDK、SDK、NDK 路径全部写进去。这个动作会直接决定 Jenkins 调 Unity 时能不能找到对应工具链。3.3 创建构建任务自由风格还是流水线Jenkins 里创建任务有两种主流形式自由风格项目和流水线Pipeline项目。如果你的目标只是简单打包 APK那自由风格项目就够了。自由风格任务里你需要配置的核心内容只有几块源码管理填 Git 仓库地址和分支拉取 Unity 工程代码构建触发器选 “Build periodically” 填入定时表达式比如每个工作日晚上跑一次H 18 * * 1-5构建环境可以勾选 “Delete workspace before build starts”可选看仓库大小构建步骤选 “执行 Windows 批处理命令”在文本框里写调用 Unity 的命令而流水线项目最大的优势是可以用脚本描述整个流程方便把打包步骤、产物归档、邮件通知这些环节写成代码存进仓库。如果你的团队以后想持续优化 CI那直接上流水线会更省事。但我见过太多团队一上来就整流水线结果语法没搞懂排错排半天。先跑通自由风格再迁移流水线才是稳的路线。4. 调用 Unity 打包的核心命令与脚本编写4.1 Unity 命令行参数逐个拆解Jenkins 的构建步骤本质就是执行一段 Windows 批处理。核心命令骨架是这样的D:\Program Files\Unity\Hub\Editor\2021.3.30f1\Editor\Unity.exe ^ -batchmode ^ -nographics ^ -quit ^ -projectPath D:\workspace\UnityProject ^ -buildTarget Android ^ -executeMethod BuildScript.BuildAndroidAPK ^ -logFile D:\workspace\logs\unity_build_%date:~0,10%.log这里拆开解释一下几个关键参数的作用理解这些参数是排查一切问题的基础。-batchmode表示以批处理模式运行Unity 就不会尝试打开图形窗口而是后台执行。-nographics进一步禁用图形设备初始化因为 CI 机器压根没有交互桌面不让 Unity 初始化显卡相关内容能减少大量崩溃风险。-quit表示执行完指定方法后自动退出 Unity 进程不加这个参数会导致 Unity 一直挂起Jenkins 构建永远结束不了。-projectPath指定要操作的工程路径必须是包含 Assets 文件夹的那一层目录。-buildTarget Android告诉 Unity 构建平台是 Android如果漏了或者写错构建目标就会变成默认的 Standalone Windows打出来的自然不是 APK。-executeMethod是最关键的参数它指定了要调用的静态方法名格式必须是类名方法名而且带完整命名空间否则 Unity 找不到入口。4.2 Editor 脚本里写打包方法的关键细节Jenkins 命令里的BuildScript.BuildAndroidAPK对应的是工程里 Editor 目录下的一个 C# 脚本。比如新建一个文件放在Assets/Editor/BuildScript.cs里面的方法必须写成静态的using UnityEditor; using UnityEditor.Build.Reporting; using UnityEngine; public class BuildScript { public static void BuildAndroidAPK() { BuildPlayerOptions buildPlayerOptions new BuildPlayerOptions(); buildPlayerOptions.scenes new[] { Assets/Scenes/Main.unity, Assets/Scenes/Level1.unity }; buildPlayerOptions.locationPathName Build/Android/MyGame.apk; buildPlayerOptions.target BuildTarget.Android; buildPlayerOptions.options BuildOptions.None; BuildReport report BuildPipeline.BuildPlayer(buildPlayerOptions); if (report.summary.result ! BuildResult.Succeeded) { throw new System.Exception(Unity build failed: report.summary.result); } } }很多人忽略了buildPlayerOptions.scenes这一项这就是标题里“场景打不出来”最直接的来源之一。如果这个数组是空的或者填写的场景路径和工程里的实际情况不一致Unity 构建时要么报错说场景文件找不到要么产出一个空包。我有时候会看到团队用EditorBuildSettings.scenes那种方式加载场景而不是在代码里写死。如果要用这种方式记得先检查File → Build Settings → Scenes In Build里有没有勾选场景否则 CI 上就是白屏空包。4.3 批处理脚本的容错和日志设计在 Jenkins 里写批处理调用 Unity 时不能只把命令一抄就完事还得考虑失败能不能被 Jenkins 正确识别。Unity 命令行模式执行完打包方法后如果过程中有异常抛出Unity.exe 会返回一个非零退出码。Jenkins 判断构建失败靠的也是这个退出码。但有时候不是每次异常都会触发非零退出码。所以我习惯在批处理脚本里主动检查 APK 文件是否存在echo off cd /d D:\workspace\UnityProject D:\Program Files\Unity\Hub\Editor\2021.3.30f1\Editor\Unity.exe ^ -batchmode ^ -nographics ^ -quit ^ -projectPath D:\workspace\UnityProject ^ -buildTarget Android ^ -executeMethod BuildScript.BuildAndroidAPK ^ -logFile D:\workspace\logs\unity_build.log echo Unity process finished with exit code %ERRORLEVEL% if not exist D:\workspace\UnityProject\Build\Android\MyGame.apk ( echo APK file was not generated. exit /b 1 )这段脚本不仅执行打包还在结束后检查 APK 是否真的生成。如果没有生成直接让 Jenkins 构建失败。这样比对着一堆日志翻找“是不是成功了”要直观得多。-logFile参数也很重要。如果不指定Unity 会把日志写到常见位置比如C:\Users\用户名\AppData\Local\Unity\Editor\Editor.log而 Jenkins 是以服务账号跑的根本访问不到你的用户目录。把日志定向到工作区目录Jenkins 的日志输出里就能直接看到 Unity 的完整报错信息排查问题会快得多。5. 场景打不出来的核心坑排查方向与解决实录5.1 场景路径不对APK 要么打不出要么是空壳先说说我遇到的最经典的“打不出来”场景。最开始 Jenkins 构建日志停在ExecuteMethod: BuildScript.BuildAndroidAPK这个阶段就报了异常。我盯着日志看了半天最后的错误是ArgumentException: The Input Scene is not included in the build settings。这个报错的本质就是buildPlayerOptions.scenes里指定的场景路径跟 Build Settings 里的场景列表不一致。如果项目是通过 Git 拉下来的新副本可能会存在ProjectSettings/EditorBuildSettings.asset里场景列表没被正确同步的情况。特别是新人切了分支或者把工程挪过位置很容易出现场景路径失效。解决方式有两个方向。一个是在打包脚本里写死场景路径比如上面代码那样。另一个是在脚本里读取EditorBuildSettings.scenes并过滤掉未启用的场景。我的建议是如果项目的场景结构比较稳定直接写死最省心如果场景经常增删那就用动态读取的方案但一定记得在代码里做校验如果场景列表为空就直接抛异常终止构建。5.2 Gradle 卡在下载依赖上APK 半天出不来还有一种“打不出来”是构建过程看着在跑但卡在 Gradle 阶段一直不动最终超时失败。这和场景没太大关系纯粹是构建机访问外网资源太慢。Gradle 启动后会去检查gradle-wrapper.properties里配置的 distributionUrl下载对应版本的 Gradle 包。而这些资源大多部署在国外的 CDN 上构建机的网络稍微差一点就可能下载失败。我的处理方式是在构建机上提前手动下载好对应版本的 Gradle解压放到一个本地固定目录然后在 Unity 的Preferences → External Tools → Gradle里指定这个本地路径。这样一来Unity 构建时就不会再去联网拉 Gradle直接用本地副本编译速度上有质的提升。另外Android 依赖库也喜欢在线下载比如 AndroidX 的库、各种 Support 库。这些依赖通常会上传到 Google 的 Maven 仓库。如果机器网络和仓库之间的连接不稳定在项目根目录的build.gradle里加 aliyun 镜像仓库会是比较快的解决方式。5.3 环境变量传不到构建任务“明明 Windows 系统变量里配了 JAVA_HOMEJenkins 构建时却还是找不到 java”这种问题我猜凡是玩过 Jenkins 当服务的人基本都遇到过。原因很简单Windows 服务启动时从注册表读取环境变量但是如果你是在安装了 Jenkins 服务之后才新加的 JAVA_HOME这个服务不会动态刷新。需要重启 Jenkins 服务或者干脆删了服务重装一次才能让服务进程继承新的环境变量。我更推荐的做法是在 Jenkins 的系统管理 → 系统配置 → 全局属性里直接手动添加环境变量。这种配置方式存储在 Jenkins 内部和 Windows 系统变量没有关系无论 Jenkins 服务什么时候重启这些变量都会在构建任务里生效。5.4 Unity 许可证校验导致批处理模式下直接退出还有一个很多人不知道的坑Unity 编辑器在交互模式下登录了许可证不代表批处理模式下就能识别。Jenkins 服务如果用的是 Local System 账号它和桌面登录的账号完全隔离自然读不到用户目录下缓存的 Unity 账号信息。这种情况下的经典报错是No valid Unity Editor license found. Please activate the license.解决方式有几种。一个是让 Jenkins 服务使用一个有桌面权限的本地账号登录并运行服务在这个账号下手动打开一次 Unity 激活许可证。这样批处理模式下就能正常识别了。另一种是用 Unity 的-manualLicenseFile参数指定许可证文件路径适合团队用了 Unity 批量激活服务UAS的情况。对于个人和小团队最方便的还是让 Jenkins 服务跑在一个能正常登录桌面的账号下提前用这个账号打开 Unity Editor 完成激活即可。5.5 Unity 进程挂死导致 Jenkins 构建永远不结束还有一种让人抓狂的情况Jenkins 构建任务显示一直在跑但日志半天不更新全是 Unity 进程卡死导致的。最典型的原因是-batchmode下 Unity 弹出了对话框比如 Crash Reporter 或者别的错误弹窗这个弹窗永远不会被点掉进程就死等在那里。虽然参数里写了-nographics但部分异常还是会触发对话框。规避方案是给批处理加一个超时机制。构建时在批处理脚本外面套一层用timeout命令监控 Unity 进程是否超时比如超过 30 分钟没结束就强制杀掉 Unity 进程并让脚本返回失败状态。这样 Jenkins 任务不会一直挂着占用构建资源下一轮构建也能正常启动。另外建议在 Jenkins 的构建任务里把“执行超时”配置成一个合理值比如 60 分钟。Jenkins 会主动终止超出时间限制的构建并释放工作空间锁避免后续任务排队等死。6. 从一次完整构建中提炼的优化建议6.1 把构建脚本纳入版本管理整个链路稳定了之后我强烈建议把构建相关的脚本和配置全部放进 git 仓库。包括BuildScript.cs、批处理脚本、甚至 Jenkinsfile如果用了流水线。这样每一次修改都有记录出了问题还能对比前后差异。比如你升级了 Unity 版本改动了打包脚本里的参数其他同事拉取代码后就能同步修改不会出现“本地打包是好的Jenkins 上是老的”这种混乱情况。6.2 产物归档与构建报告Jenkins 构建成功后APK 会生成在那个固定目录里。为了让团队成员方便取件最好在构建任务的后置操作里添加“归档产物Archive Artifacts”配置把Build/Android/*.apk归档到 Jenkins 构建记录中。之后无论谁点开本次构建记录都能直接下载 APK十分方便。同时建议在构建脚本里把 Unity 的构建报告写到工作区。BuildReport对象里包含耗时、告警、错误列表、打包大小等信息用File.WriteAllText输出成文本文件再归档到 Jenkins。排障时直接看报告而不用翻冗长的日志效率翻倍。6.3 并行构建多个场景或渠道包Unity 的构建很吃 CPU 和内存但这不代表不能并行。如果你的机器配置足够高比如 16 核 32G完全可以把不同渠道、不同场景的打包任务拆成两条流水线同时构建两个 APK。我在项目里就试过一次同时打国内版和海外版主要区别就是场景列表和包名不同。只要构建输出目录隔离Unity 进程之间基本不会互相干扰。不过注意控制并行度实际经验是每 8 核跑一个 Unity 构建任务比较稳给系统留出余量以便处理 IO 和编译任务。6.4 日志分级与关键标识最后一个小建议。Unity 的日志文件动辄几十 MB想从里面找到有用的信息比翻字典还累。我习惯在BuildScript.cs里主动加入自定义日志标识比如构建开始时打印 CI BUILD START 结束时打印 CI BUILD DONE, result: success 。这样在 Jenkins 日志里搜索关键词就能快速定位打包是否走到了最后的成功节点而不是在一堆警告里大海捞针。说实话Jenkins 搭配 Unity 打包这套东西难度不在于写代码而在于把环境、参数、脚本三者之间的衔接理清楚。一旦跑通了一次后续的维护成本其实是极低的。我自己把这套流程跑完之后的最大收获是再也不用手动去点 Unity 那个 Build 按钮了而且每次出包的质量也更稳定。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进