1. 打包这件事,为什么会想到Jenkins
大概每个做过Unity客户端开发的人,都有过这样的经历:策划催版本、测试要包、老板要看效果,你一个人坐在电脑前,打开Unity Hub,挑个版本,开个工程,等它慢慢编译,然后手动Build,签名,改包名,传到群里,再@所有人“新包在这”。一天折腾两三次还行,迭代高峰期一天七八个包,人基本就废在打包上了。
我之前在的那家小团队,Unity打包一直是“谁有空谁打”,用的还是最原始的办法——本地Build。后来新版本需求来了,代码在SVN上多人协作,每次合代码都要出包验证,人力成本一下子就上来了。那段时间我们想了想,决定用Jenkins搭一套自动打包流程,把SVN拉代码、Unity编译、APK输出、版本号管理全串起来。
这篇文章就围绕这套流程的完整落地来讲。我在实际搭建过程中踩了不少坑,网上资料又散,所以打算把自己趟出来的路子整理一遍,从环境准备、Jenkins任务配置、Unity命令行调用、SVN集成、签名与版本管理,到常见问题的排查方式,尽量写成一套可以照着抄的作业。适合正在搞Unity自动出包的客户端开发、构建工程师,或者是被打包流程折磨到想离职的可怜人。
先说结论吧:这套方案上手难度不高,核心就三件事——Jenkins负责调度,SVN负责代码版本,Unity命令行负责把工程变成APK。只要把这三者的关系理清楚,后面汇总基本都是细节问题。
2. 整体方案拆解:Jenkins、SVN、Unity怎么协作
2.1 三者的角色划分
一开始我脑子里其实挺乱的,因为Jenkins、SVN、Unity这三样东西各自都有复杂的一面,串在一起容易不知道谁先谁后。这里先把各自的责任边界划清楚。
Jenkins在整条链路里扮演的是调度中心的角色。它负责监听任务触发条件(手动点击、定时轮询SVN变化、接口调用等),然后把“拉代码→执行构建→收集产物”这个流程串起来。它不直接参与Unity的渲染和编译,只是作为一个平台把命令行指令按顺序跑完。
SVN的角色是代码托管和数据源。Jenkins会在构建前从SVN上拉取指定路径、指定版本号的代码到工作目录。这一步很关键,因为Unity打包出的APK必须基于最新或者指定版本的代码,如果代码拉错了,后面全白折腾。
Unity本身提供了命令行模式的打包接口,可以不用打开编辑器界面,直接在后台执行构建逻辑。我们需要写一个编辑器扩展脚本(C#),通过Jenkins调用Unity可执行文件并传入脚本方法名,让Unity自己完成场景加载、资源打包、APK生成这一整套动作。
这三者的关系可以理解成:Jenkins是包工头,SVN是材料仓库,Unity是加工车间。包工头从仓库取料,送到车间加工,最后把成品拿走去分发。
2.2 为什么用Jenkins而不是其他方案
市面上能实现自动打包的工具其实不少,有TeamCity、GitLab CI、GitHub Actions等,还有一些团队直接用批处理脚本配合任务计划程序来跑。那为什么我们最终选了Jenkins?
说实话,最主要的原因是团队当时用的是SVN而不是Git。GitHub Actions和GitLab CI对Git的支持当然很好,但对SVN要么不支持,要么支持得很别扭。Jenkins对SVN有原生插件支持,配置起来非常直接,拉取代码这块不需要额外写很多脚本。
另外Jenkins是自托管的,构建机就是我们自己的一台Windows工作站,Unity的许可证、Android SDK、JDK环境都装在那台机器上。用云端的CI服务反而麻烦,因为云端环境是临时的,每次构建都要重新准备Unity环境,时间和存储成本都不划算。Jenkins装好后常驻在那台Windows机器上,构建环境是稳定的、可复现的。
还有一点是Jenkins的插件生态非常成熟。Unity构建这块虽然没有官方插件,但是通过“Execute Shell”或者Windows下的“Execute Windows Batch Command”步骤,可以非常灵活地拼装命令。SVN插件、参数化构建插件、构建后通知插件都是现成的,基础流程不需要自己造轮子。
2.3 单机版还是主从架构
如果你只是给自己团队用,构建频率不高,单机版Jenkins完全够用。我一开始就是单机跑的,一台Windows工作站,既是Jenkins服务端,也是Unity构建机。后面如果构建任务多了,再考虑拆出独立的agent节点。
单机版的好处是环境配置简单,不用考虑主从之间工作目录、环境变量同步的问题。但有个隐患:Jenkins本身的运行会占用一定内存和CPU,如果同时跑Unity构建,机器性能可能吃紧。我们的工程不算大,构建一次大概3到5分钟,单机跑下来感觉还行。如果你们是大型工程,构建一次十几分钟甚至更久,建议预留一台专门的构建机,Jenkins和Unity都在那台机器上,普通开发机不要承担构建任务。
3. 环境准备:这步没做好,后面全是坑
3.1 Windows下的基础软件清单
先列一份我实际用的软件清单,版本号不一定必须一致,但尽量别差太远,否则一些参数对不上会很头疼。
| 软件 | 版本建议 | 说明 |
|---|---|---|
| Jenkins | 2.4x以上 | 推荐用LTS版本,稳定优先 |
| JDK | 8或11 | Jenkins 2.4xx以上对JDK版本有要求,装JDK11比较稳妥 |
| Unity | 跟你项目一致 | 建议固定一个版本,用Unity Hub管理 |
| Android SDK | 根据Unity版本配套 | Unity自带或单独安装都可以 |
| SVN客户端 | TortoiseSVN或者命令行版 | Jenkins的SVN插件不依赖本机svn命令,但命令行工具方便调试 |
| Android SDK Build-Tools | 对应版本 | 用于APK签名等操作 |
我自己用的是JDK11 + Jenkins 2.414.2 + Unity 2021.3.16f1 + Android SDK Build-Tools 30.0.3,这套组合跑得很稳。
3.2 Unity的Windows环境特别注意事项
Unity在Linux和macOS上跑命令行构建其实更省心,但Windows上会多一些细节问题。
第一点是Unity路径。Unity Hub安装的Unity编辑器默认在C:\Program Files\Unity\Hub\Editor\<版本号>\Editor\Unity.exe,路径里带空格,你在Jenkins的批处理命令里必须用引号包住整个路径,否则命令直接报错找不到。
第二点是Windows服务方式运行Jenkins时的权限问题。如果你把Jenkins安装成Windows服务,默认是以Local System账户运行的,这个账户可能没有加载Unity许可证的权限,导致构建时报“No valid Unity license”这个经典错误。我后来改成以普通用户身份启动Jenkins服务,或者干脆直接运行Jenkins的jenkins.war,问题就解决了。
第三点是Android SDK路径识别。Unity打包Android需要知道SDK路径,可以在Unity编辑器里通过“Preferences → External Tools”配置,也可以在命令行构建时通过-setDefaultProfiler之类的参数指定,但你得确保Jenkins能读到这个SDK路径的环境变量。
3.3 SVN账号认证的提前准备
Jenkins拉取SVN代码时,需要在Jenkins里配置SVN的凭据。这个凭据很简单,就是SVN服务器的用户名和密码。但有个细节容易被忽略:如果你们公司SVN账号绑定了IP或者需要二次验证,那Jenkins这边可能拉不下来代码。
我们当时就遇到过一个情况:开发同事在本地TortoiseSVN保存了认证信息,SVN服务器设置在非工作时间段禁止某些账号访问。结果Jenkins半夜定时构建时,代码检出直接失败。排查了半天,发现是用了一个权限受限的账号。后来专门给Jenkins建了一个独立的只读账号,只允许访问需要打包的仓库目录,问题才彻底解决。
建议:不要用个人账号作为Jenkins的SVN凭据。一旦这个人离职或者改密码,构建任务就会挂掉。建一个专用的服务账号,权限最小化,出问题也好定位。
4. Jenkins任务配置:从零到能出包的关键步骤
4.1 新建任务与基础配置
登录Jenkins后,点击“新建任务”,输入一个任务名称,比如Unity_Android_Build,选择“构建一个自由风格的软件项目”。这种类型的任务最灵活,适合我们这种需要组合多个构建步骤的场景。
进入配置页面后,先看“General”区域。我建议勾选“丢弃旧的构建”,设置保留天数为7天、保留最大构建数为20。因为Unity构建产物非常大,每个APK几十上百MB,构建记录如果无限保留,磁盘很快就满。
再看“源码管理”区域,选择“Subversion”,然后填入SVN仓库地址。这里有一个Jenkins SVN插件很典型的用法:仓库URL填到Repository URL,在Credentials那里添加你的SVN账号密码。Jenkins会自动将代码检出到工作空间目录里的<仓库名>子目录下。
有一个容易踩坑的点:如果SVN仓库很大,建议勾选“Use update”而不是每次全新checkout。因为Unity工程动辄几个GB资源,每次全新checkout不仅慢,还容易把构建机磁盘搞爆。设置成增量update,只有变更的文件才会下载。
4.2 参数化构建:让每次打包灵活起来
单纯的固定流程肯定不够用。比如你可能需要手动选择拉取哪个分支、构建Debug还是Release、版本号打多少。这些如果需要每次改配置就太蠢了,用Jenkins的参数化构建可以完美解决。
在“General”中勾选“参数化构建过程”,然后添加参数。我常用的参数有这么几个:
| 参数名 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| SVN_BRANCH | String Parameter | trunk | 分支路径,比如trunk、branches/test_xxx |
| BUILD_TYPE | Choice Parameter | release | debug/release,影响Unity里的打包配置 |
| VERSION_CODE | String Parameter | 1.0.0 | 版本号,传给Unity脚本 |
| RELEASE_NOTE | String Parameter | 无 | 本次构建说明 |
设置好这些参数后,在SVN的Repository URL里可以直接引用,比如https://svn.example.com/project/${SVN_BRANCH}。参数化构建的好处是,同一个任务既能支持开发手动触发挑选分支,又能支持定时构建使用默认参数,灵活度很高。
4.3 构建触发器:手动、定时、轮询SVN
构建触发器有三种常用方式,我分别说一下。
第一种是手动构建,最简单,点击“立即构建”即可。适用于准备发版、临时出包这类不需要频繁操作的场景。
第二种是定时构建,在“构建触发器”里勾选“定时构建”,然后在日程表里填Cron表达式。比如H 2 * * *表示每天凌晨2点左右构建一次。这种适合每天晚上自动出一个最新的测试包。
第三种是轮询SVN变更,勾选“Poll SCM”,日程表填H/5 * * * *表示每5分钟检查一次SVN有没有提交记录,有变化才触发构建。这个功能很好用,能够实现“代码一提交,自动出包”的理想状态。
但要注意:Poll SCM是Jenkins定期向SVN服务器查询变更,本身有延迟,而且频繁轮询会给服务器增加压力。我的建议是:日常开发用Poll SCM(比如5分钟一次),发版用定时构建,临时需求手动触发。
4.4 构建步骤:调用Unity命令行
这是整个流程里最核心的一步。在“构建”区域点击“增加构建步骤”,选择“Execute Windows Batch Command”,然后填入Unity命令。
我实际使用的命令行大概是这样的:
set UNITY_PATH=C:\Program Files\Unity\Hub\Editor\2021.3.16f1\Editor\Unity.exe set PROJECT_PATH=%WORKSPACE%\UnityProject set EXECUTE_METHOD=BuildScript.BuildAndroid "%UNITY_PATH%" -batchmode -nographics -quit -projectPath "%PROJECT_PATH%" -executeMethod %EXECUTE_METHOD% -buildTarget Android -logFile "%WORKSPACE%\unity_build_%BUILD_NUMBER%.log"关键点拆解一下:
-batchmode表示以批处理模式运行,不启动图形界面。-nographics表示不初始化图形设备,纯命令行构建,加速编译。-quit表示执行完方法后自动退出Unity。-executeMethod指定我们写在Unity工程里的静态构建方法名。-logFile把Unity的构建日志输出到指定文件,方便排查问题。%WORKSPACE%和%BUILD_NUMBER%是Jenkins内置环境变量,WorkSpace表示当前构建的工作目录,BUILD_NUMBER表示构建序号。
这里有个大坑需要提醒:如果不加-quit参数,Unity执行完构建方法后进程可能不退出,导致Jenkins认为构建未结束,一直挂在那。我刚开始漏了这参数,每次构建任务都要手动杀掉Unity进程才能继续,后来加上-quit才正常。
还有一个细节是-logFile参数。如果不指定,Unity日志会输出到Unity默认的日志路径,在Jenkins的控制台输出里看不到具体报错。指定了logFile后,构建结束后可以直接在Jenkins的日志归档里看到完整日志,排查问题效率会高很多。
4.5 构建后操作:产物归档与通知
构建成功后,APK文件默认会生成到Unity工程里的Assets/../Build目录下。我们需要把它归档到Jenkins里,方便下载。
在“构建后操作”里选择“Archive the artifacts”,然后填归档路径。比如:
**/Build/*.apk这个通配符会匹配工作空间下所有Build目录里的APK文件。归档后,Jenkins每次构建的页面都会有一个“已构建的工件”下载链接,直接点击就能下载APK。
如果还需要通知团队,可以安装“Email Extension”插件,在“构建后操作”里配置邮件通知,构建失败时发邮件给相关人。我们团队用的企业微信机器人,通过Webhook方式在构建成功或失败时发一条消息到群里,比邮件更及时。
5. Unity侧的构建脚本:写一个干净的BuildScript
5.1 为什么必须写编辑器脚本
Unity的命令行本身只能打开工程、执行一些基础操作,如果没有一个编辑器脚本接收参数并调用BuildPipeline,那-executeMethod就是一句空话。这个脚本本质上是一个静态方法,Unity执行到这一行时,会调用你指定的方法,然后你在方法里写构建逻辑。
我把这个脚本放在Assets/Editor/BuildScript.cs,因为Editor目录下的代码不会被打进游戏包里,只会在编辑器环境下生效。
脚本的大致结构如下:
using System; using System.IO; using UnityEditor; using UnityEngine; public static class BuildScript { public static void BuildAndroid() { string buildType = GetArg("-buildType"); string versionCode = GetArg("-versionCode"); string branch = GetArg("-branch"); if (string.IsNullOrEmpty(buildType)) { buildType = "release"; } PlayerSettings.bundleVersion = string.IsNullOrEmpty(versionCode) ? "1.0.0" : versionCode; // 在这里可以设置包名、签名、IL2CPP选项等 PlayerSettings.SetScriptingBackend(BuildTargetGroup.Android, ScriptingImplementation.IL2CPP); string outputPath = Path.Combine(Application.dataPath, "../Build/Android"); if (!Directory.Exists(outputPath)) { Directory.CreateDirectory(outputPath); } string apkName = $"Game_{buildType}_v{versionCode}.apk"; string outputFile = Path.Combine(outputPath, apkName); var options = new BuildPlayerOptions { scenes = new[] { "Assets/Scenes/Main.unity" }, locationPathName = outputFile, target = BuildTarget.Android, options = buildType == "debug" ? BuildOptions.Development | BuildOptions.AllowDebugging : BuildOptions.None }; var report = BuildPipeline.BuildPlayer(options); if (report.summary.result != UnityEditor.Build.Reporting.BuildResult.Succeeded) { throw new Exception($"Build failed with result {report.summary.result}"); } Debug.Log($"Build succeeded: {outputFile}"); } private static string GetArg(string name) { string[] args = Environment.GetCommandLineArgs(); for (int i = 0; i < args.Length; i++) { if (args[i] == name && args.Length > i + 1) { return args[i + 1]; } } return null; } }5.2 版本号和包名管理的细节
在脚本里,我从命令行参数里读取-versionCode,然后赋值给PlayerSettings.bundleVersion。这样每次出包的时候,Jenkins那边传入不同的版本号,打出APK的版本号就跟着变。
包名也可以做成参数,比如com.company.product,在脚本里用PlayerSettings.SetApplicationIdentifier(BuildTargetGroup.Android, packageName)设置。尤其是测试环境、正式环境、预发布环境需要不同包名的时候,这个功能特别有用,否则同一个包名无法同时安装两个版本。
有一个坑需要专门说:如果你在Unity的Project Settings里手动设置了包名,然后构建脚本里又覆盖了包名,得注意执行顺序。比如你先调了SetApplicationIdentifier,后面又在Player Settings界面修改过,下次构建可能又变回去。公司里多人改配置的情况尤其严重。我的经验是:包名的最终值只认构建脚本,构建脚本里明确写死,不要依赖编辑器里的默认设置。
5.3 不同渠道的配置
Unity打包经常要面对多渠道的问题——同一个游戏要出应用商店版、广告聚合版、国内SDK版、海外GooglePlay版,每个渠道的包名、签名、服务器地址可能都不同。
在构建脚本里,我习惯用一个渠道参数-channel来区分。Jenkins构建时传入-channel googlePlay或-channel cnSdk,脚本根据渠道值切换对应的包名、SDK配置和资源目录。
string channel = GetArg("-channel"); string packageName = channel == "googlePlay" ? "com.company.game.googleplay" : "com.company.game.cn";这个做法配合参数化构建,效果非常好。同一个Jenkins任务,通过选择不同的渠道参数,就能打出不同的包,不用维护多条构建任务。
6. Android签名配置:APK能出,装上却闪退怎么排查
6.1 Unity内配置签名 vs 外部签名
Unity打包Android时,签名可以在Unity的Project Settings里配置,也可以打出一个未签名的APK,然后用外部工具(比如apksigner)进行签名。两者的区别在于:
- Unity内签名:每次构建都用同一个keystore,配置简单,适合快速出测试包。
- 外部签名:可以让Jenkins在构建完成后,对APK进行单独的签名操作,适合需要分渠道不同签名的场景。
我建议团队日常测试包直接用Unity配置好调试签名,正式发版包单独走签名流程。因为Unity内签名如果每台打包机都要放keystore文件和密码,泄漏风险会增大。外部签名的话,keystore可以统一放在构建机上,由Jenkins的构建脚本调用签名工具,代码里不暴露密码。
6.2 Keystore的存储和密码管理
这部分我踩过很深的坑。刚开始图省事,把keystore密码直接写在Jenkins的批处理命令里。结果有一次排查问题时,意外发现构建日志里清清楚楚打着密码,整个人有点冒冷汗。后来我把密码挪到了Jenkins的“凭据”管理里,在批处理命令中通过环境变量引用:
set KEYSTORE_PASS=%KEYSTORE_PASS%然后在Jenkins凭据里添加一个“Secret text”类型的凭据,在构建步骤中绑定为环境变量。这样日志里就不会直接输出明文密码。
在构建脚本里千万不要用
echo打印密码,哪怕你只是在调试。日志是会归档的,时间一长别人翻到就是安全事故。
6.3 常见签名报错处理
签名相关的报错有很多种,我列几个我遇到过的:
| 报错信息 | 原因 | 解决方式 |
|---|---|---|
Keystore was tampered with, or password was incorrect | keystore密码错误或文件损坏 | 检查keystore文件和密码是否匹配 |
JKS keystore uses a proprietary format | 用的JKS格式,部分工具不支持 | 用keytool转换成PKCS12格式 |
No valid keystore | Unity配置中keystore路径无效 | 确认路径是构建机上的真实路径,而不是本地路径 |
APK signature verification failed | 设备安装时签名校验失败 | 确认APK是用正确的签名工具和keystore签的 |
有一种情况特别隐蔽:Unity打包使用了Debug签名,而你团队内部测试时手机里已经装了一个用正式签名的包。此时用Debug签名的APK去覆盖安装,会提示“与现有应用签名不同”,装上就闪退或者直接安装失败。解决办法就是保持统一:内部测试全部用同一个签名,或者卸载旧包重新安装。
7. SVN与Jenkins集成的那些事
7.1 检出路径与工作空间的关系
Jenkins的SVN插件有个特点,它会根据Repository URL自动在工作空间创建一个子目录。比如你在SVN URL里填的是https://svn.example.com/project/trunk,Jenkins会创建一个名为trunk的目录,代码都在里面。
后面你在Unity命令行里填写工程路径时,必须带上这个子目录,不能只写%WORKSPACE%。我一开始总是忘记,结果Unity报projectPath does not exist的错误。
一种更统一的做法是,在源码管理里把“Local module directory”设置为一个固定的目录名,比如unitycode。这样不管SVN URL怎么变,代码始终检出到工作空间的unitycode目录下,后续命令引用路径就稳定了。
7.2 版本号与代码版本的对应
自动构建系统还有一个很实用的需求:通过构建产物反查代码版本。比如测试提到一个问题,说“这个bug在某某包上出现”,你得能快速知道这个包是用哪个SVN版本编译的。
我用的办法是,在构建脚本里把SVN版本号(REVISION)写进APK的文件名或者生成一个版本信息文件。Jenkins里有内置的环境变量SVN_REVISION,可以直接在批处理里取到。
echo SVN_REVISION=%SVN_REVISION% > %WORKSPACE%\version_info.txt然后把version_info.txt和APK一起归档。这样每次出包都能明确知道代码版本,排查问题就快了。
还有一种更彻底的方案:在Unity构建脚本里,通过File.WriteAllText把一个包含SVN版本号的文本写入StreamingAssets目录,这样APK内部也带着版本信息,运行时可以直接在游戏里查看。
7.3 SVN变更日志与构建通知
Jenkins的SVN插件会在构建完成后生成一份“Subversion 变更日志”,记录本次构建包含哪些提交记录、修改了哪些文件。这个功能在“构建后操作”里勾选“Publish JUnit test result report”旁边的“Subversion”相关选项后会自动生成。
我们团队用这个变更日志做了更进一步的联动:构建完成后,Jenkins把变更日志内容拼进企业微信机器人发送的消息里。这样测试人员收到通知时,能直接看到本次包里包含了哪些代码变更,不需要自己去SVN上查日志。这个细节极大提升了沟通效率,建议有条件都搞上。
8. 实操过程中的常见问题与排查实录
8.1 Jenkins构建超时、一直卡住
表现:点击构建后,任务一直显示在运行,进度条不动,最后超时失败。
我遇到过几次,原因基本都出在Unity进程没有正常退出。最常见的场景是Unity弹出一个模态对话框(比如许可证过期、崩溃报告、以及“Unity正在从Hub打开工程,是否继续”这类弹窗),在batchmode下如果有弹窗,进程就会挂住。
解决办法有几个:
- 第一,确认命令行加了
-quit参数。 - 第二,在批处理里加一个超时自动杀进程的逻辑:
start /wait "" "%UNITY_PATH%" -batchmode -quit ... taskkill /F /IM Unity.exe /FI "MEMUSAGE gt 1000" 2>nul可以用timeout命令或者在Jenkins任务配置里设置超时时间。我用的是在批处理末尾加taskkill /F /IM Unity.exe兜底,确保万一Unity卡住,构建能结束。
但这里要小心:taskkill /IM Unity.exe会把打开着Unity编辑器做开发的人给杀了。如果你这台构建机同时有人用来开发,就不能这么干。所以理想的方案是构建机专用,物理隔离。
8.2 Unity报“Failed to load Android SDK”
表现:构建时Unity提示找不到Android SDK或者SDK版本不匹配。
排查思路:
第一步,确认Unity编辑器里Android SDK路径是否正确。打开Unity → Preferences → External Tools → Android SDK,看看路径是否存在。
第二步,确认SDK包里有没有安装对应版本的Platform和Build-Tools。Unity 2021.3一般需要Android SDK Platform 30+和Build-Tools 30.0.2以上。
第三步,检查环境变量。在Windows下,Jenkins以服务方式运行时,环境变量可能和本地用户不一样。可以在批处理里手动指定SDK路径:
set ANDROID_HOME=C:\Android\Sdk然后传给Unity。Unity命令行支持-androidSdkRoot参数,直接指定:
"%UNITY_PATH%" -batchmode -nographics -quit -projectPath "%PROJECT_PATH%" -executeMethod %EXECUTE_METHOD% -buildTarget Android -androidSdkRoot "%ANDROID_HOME%" -logFile "%WORKSPACE%\unity_%BUILD_NUMBER%.log"8.3 IL2CPP编译巨慢怎么办
Unity的Android构建如果选择IL2CPP + ARM64,编译速度会明显比Mono慢很多。尤其是在Windows机器上,跑一次完整构建可能需要10分钟以上。如果Jenkins构建频率高,开发人员等待的时间会很难受。
我试用过几个优化手段:
- 开启Unity的增量构建。如果工程代码没变,资源没变,第二次构建会明显快很多。不要每次构建都强制全量Build。
- 把构建机性能拉满:CPU多核、内存32GB起步、系统盘用SSD。IL2CPP编译是CPU密集型操作,机器性能直接影响出包速度。
- 尽量避免同时跑多个Unity构建。多个Unity进程同时构建,会互相抢占CPU和内存,反而更慢。
另外还有一个小优化:Unity 2021.3版本开始支持“Build Pipeline”的并行编译选项,可以在构建脚本里设置EditorUserBuildSettings.androidBuildType等参数,减少编译时间。我实际优化后,构建时间从10分钟降到6分钟左右。
8.4 生成的APK在手机上安装失败
这个问题分两类,一类是安装时提示“应用未安装”,另一类是安装成功但打开闪退。
“应用未安装”最常见的原因是包名冲突或者签名不一致。我们之前遇到过一次:同一个包名,之前的包用正式签名,新包却用了测试签名,导致安装失败。解决方案就是检查签名是否匹配,以及包名是否与设备上已安装应用冲突。
“打开闪退”则要重点看Unity日志。如果APK可以装但启动即退,多半是IL2CPP的库没打包全,或者Android API Level不匹配。还有一个容易忽略的点:如果你用BuildOptions.Development模式打包,开启脚本调试时,在某些国产ROM上可能启动异常。这种情况直接用Release包测试即可,或者抓一下logcat看具体崩溃原因。
8.5 Jenkins目录权限导致构建失败
这个坑比较隐蔽,但也比较常见。Jenkins如果是Windows服务方式运行,默认账户可能是Local System,它对某些目录没有写入权限。Unity构建过程中需要在工程目录、Library目录、Temp目录写入大量文件,如果没有权限就会报Access denied。
我的处理方案是:在Jenkins的Manage Jenkins → Configure System里,把Environment variables里的TEMP和TMP指向一个有权限的目录,比如D:\jenkins_temp。同时确保工作空间目录%WORKSPACE%对Jenkins运行账户有完全控制权限。
8.6 常见问题速查表
| 问题现象 | 可能原因 | 解决方法 |
|---|---|---|
| Unity进程卡住不退出 | 没加-quit参数或弹窗阻塞 | 加-quit,批处理末尾兜底杀进程 |
| 构建提示license无效 | Jenkins服务账户加载不到Unity许可证 | 用普通用户运行Jenkins服务 |
| 拉不到SVN代码 | 凭据失效或账号权限不足 | 重新配置凭据,使用专用账号 |
| 生成的APK没有签名 | Unity配置里keystore路径错误 | 检查Unity External Tools中的keystore设置 |
| 构建日志里无输出 | 没指定-logFile参数 | 加上logFile并归档 |
| 产物归档后在页面下载不了 | 归档路径不对 | 检查归档通配符,如**/Build/*.apk |
9. 进阶玩法:这套流程还能怎么扩展
9.1 按分支自动出包
如果你维护多个分支,比如trunk、test分支、release分支,可以在Jenkins里创建多个任务,每个任务固定SVN地址的某个分支。然后通过Poll SCM监听到分支变化时,只构建对应分支的包。这样代码往分支上一提交,对应版本的APK也就出来了。
当然也可以只用一个任务,通过参数化构建手动选择分支。自动按分支出包适合发版节奏稳定、分支改动频繁的项目;参数化构建适合临时需求多的阶段。
9.2 对接企业微信/钉钉通知
Jenkins本身有邮件通知功能,但在国内团队实际使用体验很一般——大家不怎么看邮件。我后来接入了企业微信机器人,构建完成后向群里推送一条消息,包含构建编号、APK下载链接、本次变更日志。
实现方式不复杂,在Jenkins构建后的“Execute Windows Batch Command”里调用Webhook地址,用curl或者PowerShell发送POST请求。一个简单的PowerShell示例:
$body = @{ msgtype = "text" text = @{ content = "构建成功 #$env:BUILD_NUMBER`n下载: ${env:BUILD_URL}artifact/Build/Game_release.apk" } } | ConvertTo-Json -Depth 3 Invoke-RestMethod -Uri "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=xxxx" -Method POST -Body $body -ContentType "application/json; charset=utf-8"9.3 把测试包上传到统一分发平台
测试包光放在Jenkins里还不够,测试人员未必有Jenkins账号,也不一定方便从机器下载。我们团队后来用脚本把APK上传到自建的分发平台,自动生成二维码,测试手机扫码直接安装。这一步自动化后,整个出包流程就彻底打通了,从代码提交到测试机安装,中间不需要任何人手工干预。
10. 踩坑几年后,我对这套方案的个人体会
如果你问我,搭建这套Jenkins + SVN + Unity自动打包系统,最费时间的是哪部分?我会说是前三天的环境配置和第一次跑通全流程。一旦第一个APK成功归档到Jenkins页面上,后面的迭代优化都是水到渠成的事。
我个人在实际操作中最大的体会是:不要把自动化想得太复杂,也不要过度设计。刚开始能跑通一个最简单的流程就足够了。比如第一步先做一个手动触发的构建任务,固定拉trunk代码,固定出一个release包,先跑通。跑通之后,再逐步加上参数化构建、渠道配置、通知推送、分支轮询这些高级功能。
另外一个非常重要的心得是:整个流程中,最脆弱、最容易出问题的环节不是Unity,不是SVN,而是环境的一致性。构建机上的Unity版本、Android SDK版本、JDK版本,必须全部记录在案,不能随便升级。有一次我们升级了Unity的小版本,结果Android SDK的兼容性变化,构建出的APK在部分低版本安卓设备上崩溃,排查了很久才定位到是Unity升级惹的祸。从那以后,我都坚持“构建机不动原则”:除非有明确需求,否则不升级任何基础软件。
最后再分享一个小技巧:构建机的磁盘空间管理要提前规划。Unity工程加Library缓存动辄几十GB,加上每次构建产生的APK和日志,磁盘满了构建就会莫名其妙失败。我在Jenkins里开启了“保留最近N次构建”的清理策略,同时定期手动清理Unity的Library缓存目录,整体稳定了很多。
这套方案落地后,我们团队从“每天手动出包到崩溃”变成了“提交代码自动出包、睡醒拿包测试”的节奏,效率提升非常明显。如果你正在被重复的打包流程折磨,建议按这篇文章的步骤试一遍,大概率能少走一半弯路。