Unity Windows 客户端从 Mono 切换到 IL2CPP 后,安装包体积翻倍的排查记录

前言

最近在发布 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 的调试/备份产物也一起打进去了。

真正的根因

最终根因可以总结为:

  1. Unity 切换到 IL2CPP 后,会生成 BackUpThisFolder_ButDontShipItWithYourGame 目录。
  2. 这个目录包含 GameAssembly.pdbil2cppOutputManaged 等调试/中间产物。
  3. 这些文件不是客户端运行必需文件。
  4. 打包安装包时,安装脚本把这些目录一起打进去了。
  5. 因此安装包体积从 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

这样大部分 .pdbil2cppOutput 也会一起被排除。

哪些文件不能删?

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_ButDontShipItWithYourGameBurstDebugInformation_DoNotShip 等调试/备份目录,而安装包脚本误将它们一起打进了正式安装包。

解决方案是:

text 复制代码
打包安装包时排除 DoNotShip/Backup 调试目录和 PDB/il2cppOutput 中间产物。

实际验证结果:

text 复制代码
移除这些目录后,安装包体积明显下降,客户端安装后可以正常运行。

这次排查也提醒我:遇到包体异常增长时,不要停留在"某个设置变了,所以体积变大也正常"的模糊判断上,而是要继续拆目录、看大文件、确认哪些是运行必需文件,哪些只是构建过程留下的调试产物。

技术成长很多时候就是这样,从一个看似普通的包体问题里,把构建流程、发布产物和运行依赖一点点拆清楚。

相关推荐
_zhourui_h_3 小时前
MyFramework Unity:TweenSequence 和 DOTween 有什么区别
unity3d
SmalBox1 天前
【节点】[IrisOffset节点]原理解析与实际应用
unity3d·游戏开发·图形学
SmalBox2 天前
【节点】[IrisLimbalRing节点]原理解析与实际应用
unity3d·游戏开发·图形学
SmalBox3 天前
【节点】[EyeSurfaceTypeDebug节点]原理解析与实际应用
unity3d·游戏开发·图形学
SmalBox4 天前
【节点】[CorneaRefraction节点]原理解析与实际应用
unity3d·游戏开发·图形学
SmalBox5 天前
【节点】[CirclePupilAnimation节点]原理解析与实际应用
unity3d·游戏开发·图形学
SmalBox6 天前
【节点】[Zigzag节点]原理解析与实际应用
unity3d·游戏开发·图形学
_zhourui_h_7 天前
Unity MyFramework: 框架中的那些非常实用的 GC 处理技巧
unity3d
SmalBox7 天前
【节点】[Whirl节点]原理解析与实际应用
unity3d·游戏开发·图形学