前言
最近在发布 Unity Windows 客户端时,我遇到了一个很典型、也很容易误判的问题:
项目原本使用 Mono 脚本后端打包,安装包体积大约是 400 多 MB。后来为了发布需求,将 Unity 的脚本后端从 Mono 切换成了 IL2CPP,结果安装包体积一下变成了 800 多 MB。
一开始我很容易得出一个模糊结论:
IL2CPP 会把 C# 转成 C++ 再编译成 native 代码,所以包体变大是正常的。
这个说法只对了一半。经过实际对比 Mono 模式和 IL2CPP 模式下发布出来的 Windows 应用目录后,我发现真正导致体积暴涨的原因并不是 IL2CPP 正常运行文件本身,而是把 Unity 生成的调试/备份目录也打进了安装包。
这篇文章记录完整排查过程,希望能帮助遇到类似问题的同学少走一些弯路。
项目背景
项目是 Unity Windows 客户端。前后两次发布的主要区别是脚本后端不同:
| 发布产物 | 脚本后端 |
|---|---|
| Mono 版本 Windows 客户端 | Mono |
| IL2CPP 版本 Windows 客户端 | IL2CPP |
为了方便说明,下面统一使用示例应用名:
text
GameApp
假设两个 Unity 原始发布目录分别是:
text
GameApp_Mono
GameApp_IL2CPP
其中:
text
GameApp_Mono 是 Mono 模式下发布出来的应用目录
GameApp_IL2CPP 是 IL2CPP 模式下发布出来的应用目录
现象
两个安装包大小对比大概是:
| 安装包 | 大小 |
|---|---|
| Mono 版本安装包 | 约 400 多 MB |
| IL2CPP 版本安装包 | 约 800 多 MB |
从结果上看,IL2CPP 安装包几乎比 Mono 安装包大了一倍。
这个现象很容易让人误以为:
只要从 Mono 切到 IL2CPP,包体翻倍就是正常现象。
但实际排查后发现,并不是这么简单。
初步分析:IL2CPP 确实会让包变大
Unity 中 Mono 和 IL2CPP 的打包机制不同。
Mono 模式下,项目的 C# 代码主要以托管 DLL 的形式发布,运行时依赖 Mono runtime。
IL2CPP 模式下,Unity 会先把 C# IL 代码转换成 C++,再编译成原生代码。Windows IL2CPP 包中通常会出现这些关键文件:
text
GameAssembly.dll
GameApp_Data\il2cpp_data
GameApp_Data\il2cpp_data\Metadata\global-metadata.dat
所以,IL2CPP 比 Mono 大一些是正常的。常见增长来源包括:
text
GameAssembly.dll
global-metadata.dat
il2cpp_data
这些是 IL2CPP 客户端运行需要的文件。
但是,问题在于:正常情况下不应该因为切换 IL2CPP 就让安装包体积直接翻倍。增长幅度如果明显过大,就需要继续对比两个 Unity 原始发布目录。
关键排查:对比 Unity 原始发布目录
排查时不要只看最终安装包,更应该先对比 Unity 发布出来的原始应用目录。
也就是对比:
text
GameApp_Mono
GameApp_IL2CPP
如果 IL2CPP 原始发布目录本身已经远大于 Mono 发布目录,就说明问题大概率发生在 Unity 构建产物阶段,而不是安装包压缩阶段。
在这次排查中,IL2CPP 发布目录里出现了一个非常大的目录:
text
GameApp_BackUpThisFolder_ButDontShipItWithYourGame
这个目录名其实已经说明了它的用途:
text
BackUpThisFolder_ButDontShipItWithYourGame
翻译过来就是:
text
备份这个文件夹,但不要把它随游戏一起发布
也就是说,这是 Unity 明确提示不要发布的目录。
继续拆解这个大目录
GameApp_BackUpThisFolder_ButDontShipItWithYourGame 内部通常会包含:
text
GameAssembly.pdb
il2cppOutput
Managed
其中:
text
GameAssembly.pdb
是 Windows 调试符号文件,用于崩溃分析、堆栈符号解析、定位源码行号等。它对普通客户端运行不是必需的。
text
il2cppOutput
是 IL2CPP 把 C# 转成 C++ 后生成的中间源码目录,里面有大量 .cpp、.c 等文件。运行时不需要这些源码文件,运行时真正需要的是编译后的:
text
GameAssembly.dll
text
Managed
是托管程序集备份,同样不属于普通客户端运行所需目录。
所以,这个目录的性质非常明确:它是给开发排查、调试、符号分析、备份使用的,不应该随正式客户端发布。
另一个容易被误打包的目录
除了 BackUpThisFolder_ButDontShipItWithYourGame 之外,IL2CPP 或 Burst 相关构建中还可能出现类似目录:
text
GameApp_BurstDebugInformation_DoNotShip
这个名字里也有非常明确的提示:
text
DoNotShip
也就是不要随产品发布。
它主要用于 Burst 相关调试信息,同样不是普通客户端运行所必需的文件。
进一步确认:安装包脚本把它们打进去了
继续检查 Windows 安装包脚本,发现安装脚本确实把这些目录中的文件也包含进去了,例如:
text
SetOutPath "$INSTDIR\GameApp_BackUpThisFolder_ButDontShipItWithYourGame"
File "...GameApp_BackUpThisFolder_ButDontShipItWithYourGame\GameAssembly.pdb"
SetOutPath "$INSTDIR\GameApp_BackUpThisFolder_ButDontShipItWithYourGame\il2cppOutput"
File "...GameApp_BackUpThisFolder_ButDontShipItWithYourGame\il2cppOutput\..."
这就解释了为什么安装包体积会暴涨。
不是 IL2CPP 正常运行文件让安装包大了几百 MB,而是安装包脚本把 Unity 明确标记为 DoNotShip 的调试/备份产物也一起打进去了。
真正的根因
最终根因可以总结为:
- Unity 切换到 IL2CPP 后,会生成
BackUpThisFolder_ButDontShipItWithYourGame目录。 - 这个目录包含
GameAssembly.pdb、il2cppOutput、Managed等调试/中间产物。 - 这些文件不是客户端运行必需文件。
- 打包安装包时,安装脚本把这些目录一起打进去了。
- 因此安装包体积从 400 多 MB 增长到了 800 多 MB。
一句话总结:
这次包体翻倍的主要原因不是 IL2CPP 本身,而是把 IL2CPP 的备份/调试中间产物误打进了安装包。
哪些文件可以排除?
正式安装包中可以排除:
text
GameApp_BackUpThisFolder_ButDontShipItWithYourGame
GameApp_BurstDebugInformation_DoNotShip
*.pdb
il2cppOutput
其中:
text
GameApp_BackUpThisFolder_ButDontShipItWithYourGame
是 IL2CPP 备份/调试目录。
text
GameApp_BurstDebugInformation_DoNotShip
是 Burst 调试信息目录,名字里同样有 DoNotShip。
text
*.pdb
是调试符号文件。普通客户端运行不依赖它。
text
il2cppOutput
是 IL2CPP C++ 中间源码目录。运行不依赖它。
实际处理时,通常优先排除整个目录:
text
GameApp_BackUpThisFolder_ButDontShipItWithYourGame
GameApp_BurstDebugInformation_DoNotShip
这样大部分 .pdb 和 il2cppOutput 也会一起被排除。
哪些文件不能删?
IL2CPP 客户端真正运行需要保留:
text
GameApp.exe
UnityPlayer.dll
GameAssembly.dll
baselib.dll
GameApp_Data
GameApp_Data\il2cpp_data
GameApp_Data\il2cpp_data\Metadata\global-metadata.dat
D3D12
UnityCrashHandler64.exe
尤其注意:
text
GameAssembly.dll
这是 IL2CPP 编译后的核心原生代码。
text
global-metadata.dat
这是 IL2CPP 运行需要的元数据。
这两个不能误删。
为什么删除后客户端仍然能运行?
因为最终客户端运行依赖的是编译后的产物:
text
GameAssembly.dll
global-metadata.dat
UnityPlayer.dll
资源文件
插件 DLL
而不是:
text
GameAssembly.pdb
il2cppOutput 下的 C++ 源码
DoNotShip 调试目录
备份 Managed 程序集
这些调试/备份文件对于开发排查可能有价值,但对普通用户运行客户端没有价值。
所以从安装包中排除它们,不影响客户端运行。
推荐的打包排除规则
如果使用 NSIS、Inno Setup、自研打包工具或其他安装包工具,可以考虑增加类似排除规则:
text
*_BackUpThisFolder_ButDontShipItWithYourGame
*_BurstDebugInformation_DoNotShip
*.pdb
il2cppOutput
如果打包工具是通过脚本遍历 Unity 发布目录生成安装包文件列表,也建议在遍历阶段就过滤掉这些目录。
例如逻辑上可以表达为:
text
如果目录名包含 DoNotShip,则不加入安装包
如果目录名包含 BackUpThisFolder_ButDontShipItWithYourGame,则不加入安装包
如果文件后缀是 .pdb,则不加入安装包
如果目录名是 il2cppOutput,则不加入安装包
当然,具体规则要结合项目的崩溃分析需求。如果线上确实需要保留符号文件,也应该单独上传到符号服务器或内部归档,而不是直接随客户端发布给用户。
验证结果
排除这些目录后:
text
GameApp_BackUpThisFolder_ButDontShipItWithYourGame
GameApp_BurstDebugInformation_DoNotShip
重新生成安装包,包体明显下降。安装客户端后,程序也可以正常启动和运行。
这说明这些目录确实不是运行依赖,而是构建过程中生成的调试/备份产物。
本次排查得到的经验
1. 不要只凭"IL2CPP 会变大"下结论
IL2CPP 确实会增加包体,但正常增长通常来自:
text
GameAssembly.dll
il2cpp_data
global-metadata.dat
如果体积增长非常夸张,应该优先检查是否误打包了调试目录、中间产物、符号文件。
2. 看到 DoNotShip 要警惕
Unity 生成目录中如果出现:
text
DoNotShip
BackUpThisFolder_ButDontShipItWithYourGame
基本就说明这些目录不应该出现在正式安装包里。
3. 安装包脚本不要无脑递归整个发布目录
如果安装包工具直接递归发布目录,很容易把 Unity 的调试目录也带进去。
更稳妥的方式是维护排除规则,例如:
text
*_BackUpThisFolder_ButDontShipItWithYourGame
*_BurstDebugInformation_DoNotShip
*.pdb
il2cppOutput
4. 对比原始发布目录比只看安装包更有效
这次问题之所以能快速定位,是因为同时对比了:
text
Mono 原始发布目录
IL2CPP 原始发布目录
Mono 安装包
IL2CPP 安装包
如果只看安装包大小,很容易误以为是安装器压缩率或 IL2CPP 本身的问题。
最终结论
这次 Windows 客户端从 Mono 切换到 IL2CPP 后安装包体积翻倍,真正原因是:
IL2CPP 发布目录中生成了
BackUpThisFolder_ButDontShipItWithYourGame和BurstDebugInformation_DoNotShip等调试/备份目录,而安装包脚本误将它们一起打进了正式安装包。
解决方案是:
text
打包安装包时排除 DoNotShip/Backup 调试目录和 PDB/il2cppOutput 中间产物。
实际验证结果:
text
移除这些目录后,安装包体积明显下降,客户端安装后可以正常运行。
这次排查也提醒我:遇到包体异常增长时,不要停留在"某个设置变了,所以体积变大也正常"的模糊判断上,而是要继续拆目录、看大文件、确认哪些是运行必需文件,哪些只是构建过程留下的调试产物。
技术成长很多时候就是这样,从一个看似普通的包体问题里,把构建流程、发布产物和运行依赖一点点拆清楚。