使用Memory Snapshot对比Mono与IL2CPP差异

解读

国内一线/二线游戏公司面试时,这道题常被放在“性能调优”环节,用来快速判断候选人是否真正踩过线上项目内存坑
面试官不是想听“IL2CPP比Mono快”这种口号,而是想看:

  1. 你是否亲手用Unity ProfilerUPA(腾讯)/UWA抓过Memory Snapshot;
  2. 能否把快照里的Reserved、Used、Heap、Stack、Boehm vs IL2CPP GC四条线讲清楚;
  3. 能否把差异映射到包体、启动速度、热更新限制、Crash 率等国内项目最关心的 KPI。
    答不出“为什么IL2CPP下同一帧Mono涨 8 MB 而 IL2CPP 只涨 2 MB”这类细节,基本会被判为“只看过博客,没做过项目”。

知识点

  1. 内存快照抓取姿势

    • 国内项目普遍接腾讯 UPA 或 UWA SDK,Release 包勾选 Script Debugging + Development Build,否则快照里无 C# 对象详情。
    • 抓快照前必须手动调用 System.GC.Collect() 并等待 1 帧,排除 GC 延迟噪声,否则对比结果失真。
  2. Mono Backend 快照特征

    • Managed Heap 只有一块,由 Boehm 托管,Reserved 连续向上增长不会回退给系统
    • Stack Trace 与对象地址连续,内存碎片表现为“空洞”,Profiler 里能看到 Dark Gray Unused 块;
    • 泛型+反射在运行时动态生成 IL,快照里出现 System.MonoTypeSystem.RuntimeType 暴涨,容易踩 32-bit 进程的 3 GB 天花板
  3. 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 %
  4. 国内项目必踩的坑

    • IL2CPP 下字符串 Intern Pool 永不释放,如果热更新 DLL 里写了大量 const string,快照里 System.String 常驻导致内存告警;
    • Mono 下 Assembly.Load 反复加载热更 DLL,快照里 Assembly 对象只增不减,24 小时长稳测试必 OOM;
    • iOS 14 以上 JetSam 阈值 1.4 GB,Mono 后端因 Reserved 无法回退,后台播放动画时极易被杀,IL2CPP 阶梯回退可保命。

答案

示范一次真机对比,步骤与结论完全按国内上线标准:

  1. 同一部 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
  2. 进游戏到 主城场景(200 角色+50 UI+20 特效),静置 60 秒待资源加载完毕,UWA SDK 触发快照
  3. 对比结果:
    • 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 MBPSS 实测下降 90 MB
  4. 结论一句话:
    IL2CPP 通过 AOT 移除运行时元数据、分块 GC 与 mmap 回退,把 Reserved 降低 60–80 MB,并具备物理内存归还能力,是国内中大型项目上线 iOS/安卓双端的必选项;代价是包体增大 15–20 MB、首次安装解压时间+3 s,且无法使用反射式热更(Lua、HybridCLR 需额外方案)。

拓展思考

  1. HybridCLR 热更在 IL2CPP 下如何重新出现 RuntimeType?
    答:HybridCLR 把热更 DLL 的元数据重新映射到 libil2cpp 的 metadata cache,快照里会新增 HybridCLR::InterpreterType 对象,内存增长 0.8–1.2 MB/万类,需监控 Reserved 阶梯是否因此回退失效。

  2. 国内渠道包 64 K 方法数限制(华为、OPPO)与 IL2CPP 冲突?
    答:IL2CPP 生成 libil2cpp.so.text 段方法数远超 64 K,但 Android 的 64 K 限制仅指 Java 方法数,C++ 符号不受限;真正需要 multidex 的是 第三方 SDK 的 Java 层,不要误把 so 方法数当成瓶颈。

  3. 如何自动化监控线上 Reserved 阶梯?
    答:在 Unity 2021.3 以上版本,把 Profiler.EmitFrameMetaData 打到 CrashSight(腾讯) 自定义字段,Reserved 每次上涨 > 16 MB 且 30 秒未回退即报警,可提前 2 小时发现内存泄漏,比传统 PSS 采样提前 45 分钟。