如何裁剪AOT DLL减少包体

解读

面试官问“如何裁剪AOT DLL减少包体”,并不是想听你背“把 IL2CPP 的 Code Stripping Level 调成 High”这么简单。国内上线包体动辄被渠道卡 150 MB(Android 渠道包)或 200 MB(iOS App Store 蜂窝下载阈值),AOT 后 native 代码段膨胀是包体失控的主凶之一。他真正想确认的是:

  1. 你能不能把“引擎代码、业务代码、第三方库”这三类 DLL 的膨胀根因拆清楚;
  2. 能否用 Unity 官方 + 自研 的组合拳,把 IL2CPP 生成的 libil2cpp.a / libil2cpp.so 从 80 MB 压到 30 MB 以下,同时保证 版本可灰度、符号可回溯、热更不击穿
  3. 是否了解国内合规风险(IMEI、蓝牙、录音等模块)与裁剪的耦合,避免“剪多了被法务打回”。

知识点

  • IL2CPP 管线四阶段:C# → IL → ILPostProcessor → C++ → Clang/GCC AOT;膨胀发生在 IL→C++ 的泛型实例化与虚函数表爆炸。
  • Managed Stripping Level 三级:Low、Medium、High;High 之上还有 Custom PlayerBuildCodeStripper(Unity 2021.3 以后官方实验性 API)。
  • Linker Script & Sections Garbage Collection:Xcode 的 -dead_strip、Android NDK 的 -ffunction-sections + --gc-sections;Unity 2022.2 起在 Gradle 模板默认打开,但 iOS 需要手动在 Xcode Build Setting 里加 OTHER_LDFLAGS=-dead_strip
  • Root Assembly 标记规则:被 [Preserve]link.xmlRuntimeInitializeOnLoadMethod 直接或间接引用的类型会成为 Root,任何反射路径必须在 link.xml 白名单显式声明,否则 High 级别下直接剪废。
  • 泛型共享陷阱List<T> 在 AOT 里会为每个 值类型 T 单独生成一份 native 代码;Dictionary<Enum,Struct> 是最常见的大头。
  • 引擎模块级裁剪:Unity 2021 LTS 开始支持 “Modules.xml”白名单,可把 UnityWebRequest、UnityAnalytics、IMGUIModule 等整模块剔除,单个模块约 400-600 KB 静态库体积
  • 第三方库瘦身
    • Newtonsoft.Json 改用 Unity 官方包 com.unity.nuget.newtonsoft-json(已裁剪 Expression 编译路径);
    • protobuf-net 改用 protobuf-unity-codegen,关闭动态序列化,全部预生成;
    • DOTween 关闭 DOTWEEN_DISABLED 宏,剔除 DOTween43.dll 等遗留扩展。
  • 符号裁剪与回溯平衡:iOS 的 -exported_symbols_list 只导出 il2cpp_*UnityFramework_*把 1.2 MB 符号表压到 200 KB;同时保留 .dSYM 供 Bugly 符号化,上线前用 strip -S -x 二次裁剪。
  • 热更新耦合:若使用 HybridCLR(国内主流),必须在 link.xml 保留 System.Reflection.Emit.*HybridCLR.* 命名空间,否则 AOT 剪完后补充元数据会崩溃。
  • 合规与裁剪冲突:工信部 164 号文要求提供 “隐私权限关闭入口”,若把 UnityAnalytics 模块整包裁掉,需在代码层 mock 空实现,否则渠道检测 SDK 缺失直接驳回。

答案

“我实战中的裁剪分五步,把 AOT so 从 78 MB 压到 29 MB,Android 包体下降 42 MB,iOS 裸二进制下降 35 MB,且通过华为、小米、应用宝三重审核

第一步,模块级骨架裁剪。在 Assets/Plugins/Android/Modules.xmliOS/Modules.xcconfig 里关闭 UnityAnalytics、UnityPurchasing、UnityWebRequestWWW、LegacyIMGUIModule,直接抠掉 5 个引擎模块,静态库体积减少 2.3 MB

第二步,托管层根标记收敛。把项目里散落的 200 多个 [Preserve] 全部干掉,统一维护 link.xml + 裁剪配置化脚本。脚本在 CI 里扫描所有 Type.GetTypeAssembly.Load 的字符串常量,自动生成 600 行白名单,确保 High Stripping 下 0 反射报错。

第三步,泛型爆炸治理。用 Mono.Cecil 自研工具扫描 IL,找出 1 万+ 个值类型泛型实例,把 List<Vector3/Quaternion/Color32> 等高频类型在 Assembly-CSharp 里显式声明 dummy 方法,强制让 IL2CPP 复用共享版本,C++ 代码量减少 18 %

第四步,Native 段二次裁符号。Android 侧在 libil2cpp.soAndroid.mk-ffunction-sections -fdata-sections--gc-sections;iOS 侧用 -dead_strip + -exported_symbols_list符号表从 1.1 MB 压到 180 KB,同时把 libiPhone-lib.a 里未引用的 WebGL 后端纹理压缩代码整段剔除。

第五步,热更新兜底。项目用 HybridCLR,在 link.xml 保留最小元数据 1.8 MB,通过 HybridCLR/Generate/LinkXml 自动合并,既保证 AOT 裁剪极限,又让补充元数据阶段 0 缺失,线上灰度 50 万用户无裁剪击穿崩溃。

最终包体:Android 渠道包 142 MB → 100 MB;iOS App Store 裸二进制 92 MB → 57 MB,全部低于渠道审核红线,且 Crash 率无上涨。”

拓展思考

  1. Unity 2023 的 CoreCLR 实验分支 已支持 ReadyToRun + OSR,未来 AOT 不再走 IL2CPP,而是直接 R2R 镜像,届时“裁剪”概念会从 IL 层转移到 ReadyToRun 元数据层,需要提前研究 crossgen2 --composite --trim 命令。
  2. 国内小游戏平台(抖音、微信) 要求首包 20 MB 以内,不能把 libil2cpp.so 打包进去,需要用 AssetBundle 首包拆分 + 按需下载 so 的方案;此时裁剪目标不再是“小”,而是 “可延迟”,需要把非首场景用到的泛型全部迁到热更脚本,避免 AOT 期生成。
  3. 法律合规进一步收紧,工信部正在讨论“启动阶段禁止任何数据上报”,UnityAnalytics 模块即使保留也不能初始化。下一步裁剪策略需要把 “空壳模块” 做成 Gradle/Xcode 的 dummy framework,既满足渠道检测,又保证 0 代码体积,实现“合规级零成本”