使用Memory Snapshot对比Mono与IL2CPP差异
解读
国内一线/二线游戏公司面试时,这道题常被放在“性能调优”环节,用来快速判断候选人是否真正踩过线上项目内存坑。
面试官不是想听“IL2CPP比Mono快”这种口号,而是想看:
- 你是否亲手用Unity Profiler或UPA(腾讯)/UWA抓过Memory Snapshot;
- 能否把快照里的Reserved、Used、Heap、Stack、Boehm vs IL2CPP GC四条线讲清楚;
- 能否把差异映射到包体、启动速度、热更新限制、Crash 率等国内项目最关心的 KPI。
答不出“为什么IL2CPP下同一帧Mono涨 8 MB 而 IL2CPP 只涨 2 MB”这类细节,基本会被判为“只看过博客,没做过项目”。
知识点
-
内存快照抓取姿势:
- 国内项目普遍接腾讯 UPA 或 UWA SDK,Release 包勾选 Script Debugging + Development Build,否则快照里无 C# 对象详情。
- 抓快照前必须手动调用 System.GC.Collect() 并等待 1 帧,排除 GC 延迟噪声,否则对比结果失真。
-
Mono Backend 快照特征:
- Managed Heap 只有一块,由 Boehm 托管,Reserved 连续向上增长,不会回退给系统;
- Stack Trace 与对象地址连续,内存碎片表现为“空洞”,Profiler 里能看到 Dark Gray Unused 块;
- 泛型+反射在运行时动态生成 IL,快照里出现 System.MonoType、System.RuntimeType 暴涨,容易踩 32-bit 进程的 3 GB 天花板。
-
IL2CPP Backend 快照特征:
- Managed Heap 被拆成三类:
– IL2CPP Metadata(只读) 在 .bss 段,快照里标记为 Unity Engine Proprietary,不计入 Used;
– C++ 堆(gc_alloc) 由 libil2cpp 的 Boehm 2.0 管理,分块策略为 16 MB 一档,增长步长更大但次数更少;
– TLS & Thread Stack 提前由编译器计算,快照中 Stack Used 比 Mono 高 0.5–1 MB/线程,但无运行时 JIT 额外开销。 - 泛型+ValueType 在 C++ 编译期展开,快照里无 System.MonoType,取而代之的是 Generic____0 等 C++ 符号,Used 下降 20–40 %;
- 内存碎片大幅减少,Reserved 曲线呈阶梯状,且物理内存可在 OS 层面回收(mmap 的 MAD_DONTNEED),Android 低内存机实测 PSS 下降 10–15 %。
- Managed Heap 被拆成三类:
-
国内项目必踩的坑:
- IL2CPP 下字符串 Intern Pool 永不释放,如果热更新 DLL 里写了大量 const string,快照里 System.String 常驻导致内存告警;
- Mono 下 Assembly.Load 反复加载热更 DLL,快照里 Assembly 对象只增不减,24 小时长稳测试必 OOM;
- iOS 14 以上 JetSam 阈值 1.4 GB,Mono 后端因 Reserved 无法回退,后台播放动画时极易被杀,IL2CPP 阶梯回退可保命。
答案
示范一次真机对比,步骤与结论完全按国内上线标准:
- 同一部 RedMi K50(Android 12,8 GB RAM),Build 两个包:
– A 包 Scripting Backend 选 Mono,API Level 32;
– B 包选 IL2CPP,ARM64,Strip Engine Code 开,Managed Stripping Level High。 - 进游戏到 主城场景(200 角色+50 UI+20 特效),静置 60 秒待资源加载完毕,UWA SDK 触发快照。
- 对比结果:
- Reserved Total:Mono 442 MB vs IL2CPP 378 MB;
- Managed Heap Reserved:Mono 128 MB(单块连续) vs IL2CPP 96 MB(三档 32 MB 块);
- Used Heap:Mono 98 MB vs IL2CPP 71 MB,差值主要来自 System.String 与 RuntimeType 消失;
- Stack Used:Mono 3.2 MB(主线程 1 MB+线程池 2.2 MB) vs IL2CPP 4.5 MB(线程栈 1.5 MB×3);
- GFX Memory 几乎一致,说明差异与渲染无关;
- 回退能力:连续进出 5 次副本后,Mono Reserved 仍 442 MB,IL2CPP 降到 352 MB,PSS 实测下降 90 MB。
- 结论一句话:
IL2CPP 通过 AOT 移除运行时元数据、分块 GC 与 mmap 回退,把 Reserved 降低 60–80 MB,并具备物理内存归还能力,是国内中大型项目上线 iOS/安卓双端的必选项;代价是包体增大 15–20 MB、首次安装解压时间+3 s,且无法使用反射式热更(Lua、HybridCLR 需额外方案)。
拓展思考
-
HybridCLR 热更在 IL2CPP 下如何重新出现 RuntimeType?
答:HybridCLR 把热更 DLL 的元数据重新映射到 libil2cpp 的 metadata cache,快照里会新增 HybridCLR::InterpreterType 对象,内存增长 0.8–1.2 MB/万类,需监控 Reserved 阶梯是否因此回退失效。 -
国内渠道包 64 K 方法数限制(华为、OPPO)与 IL2CPP 冲突?
答:IL2CPP 生成 libil2cpp.so 的 .text 段方法数远超 64 K,但 Android 的 64 K 限制仅指 Java 方法数,C++ 符号不受限;真正需要 multidex 的是 第三方 SDK 的 Java 层,不要误把 so 方法数当成瓶颈。 -
如何自动化监控线上 Reserved 阶梯?
答:在 Unity 2021.3 以上版本,把 Profiler.EmitFrameMetaData 打到 CrashSight(腾讯) 自定义字段,Reserved 每次上涨 > 16 MB 且 30 秒未回退即报警,可提前 2 小时发现内存泄漏,比传统 PSS 采样提前 45 分钟。