如何拆分首包与扩展包减少APK大小

解读

国内安卓渠道(华为、OPPO、小米、应用宝等)对 APK 直接上架的体积红线普遍卡在 1.5 GB,部分渠道甚至 1 GB 就会强制要求“分包+动态下载”。iOS 虽然用 App Store 的 ODR(On-Demand Resources)机制,但审核同样会卡“首次下载大小”与“蜂窝网络下载上限(200 MB)”。因此,面试官想知道你是否能把“必须进首包”与“可以后下”的资源、代码、配置在 Unity 管线层面就拆干净,并给出 可落地的中国渠道合规方案,而不是泛泛而谈“用 AssetBundle”。

知识点

  1. 首包(主包)体积计算口径:APK 内部所有文件解压后总和,不含 Google Play 的 patch 算法。国内渠道以此为准。
  2. 资源分级标准
    ‑ 启动场景依赖(Shader 变体、首屏 UI、Logo、法律文本)→ 必须首包;
    ‑ 关卡/角色/音频/视频 → 可扩展;
    ‑ 热更脚本 DLL(Lua、ILRuntime)→ 可扩展,但需预留脚本引擎与空壳 Assembly-CSharp.dll 接口。
  3. Unity 可裁剪开关
    ‑ PlayerSettings 中 Strip Engine CodeManaged Stripping Level(High)、LZ4HC 压缩
    Shader 变体收集(ShaderVariantCollection)+ IPostGenerateGradleProject 脚本剔除无用变体;
    Engine Scenes in Build 只保留 0 号启动场景,其余场景移出 Build Settings。
  4. Android App Bundle(AAB)≠ 国内方案:国内渠道后台目前 不支持 Google AAB,需自己实现 动态资源下载(AssetBundle)+ 安装后首次启动增量合并
  5. 扩展包存储合规
    ‑ Android 10+ 分区存储(Scoped Storage),必须放到 /Android/data/<包名>/files/obb/SAF 路径,否则 targetSdkVersion≥30 时 /sdcard/ 直写会抛异常;
    ‑ iOS 必须走 ODR 标签组NSBundle.mainBundle 外自己管理沙盒,否则审核 2.3.1 拒审。
  6. 下载策略
    ‑ 国内弱网环境需 CDN 分片+断点续传(最好接入阿里云/腾讯云 Range-Request 支持);
    ‑ 首次进入游戏只下“前 10 分钟体验包”,后台静默继续下“完整包”,用 Unity 协程 + 线程池 + 流量保护开关
    ‑ 渠道合规必须弹 《用户隐私协议》“是否允许移动网络下载” 双弹窗,否则会被渠道下架。
  7. 版本一致性校验:扩展包用 Unity Manifest 文件(CRC+Hash128)服务器版本号 双校验,防止 CDN 边缘节点回源延迟导致资源不匹配。
  8. 代码热更边界
    ‑ 若用 Lua/ILRuntime,首包必须带 解释器与反射绑定(约 400-600 KB),剩余业务脚本放扩展包;
    ‑ 若用 HybridCLR,首包只需 AOT 元数据裁剪后的壳,核心逻辑 DLL 放扩展包,首次启动后 Assembly.Load 加载。

答案

“我会把体积治理拆成 资源侧、代码侧、引擎侧、合规侧 四步,确保首包压到 800 MB 以内,扩展包在 Wi-Fi 环境静默下载

第一步,资源侧:

  1. ScriptableBuildPipeline(SBP)+ Addressable 把资源按“登录前、新手关、主线关、支线关、高清皮肤、CG 视频”六级打标签;
  2. 首包只保留 0 号启动场景 + 1 个 UI Atlas + 通用 Shader 变体包(<30 MB),其余全部标记为 DownloadSizeThreshold=0,即安装后必下;
  3. 对纹理做 ETC2_ASTC 双平台压缩 + 分辨率 512 封顶,对音频做 Vorbis 48 kHz 单声道 80 kbps,粒子特效贴图统一 128*128
  4. >500 KB 的序列化 JSON/Excel 配置 转成 MessagePack + LZ4 块,减少首包文本体积。

第二步,代码侧:

  1. 开启 High Stripping + Strip Engine Code,写 link.xml 白名单 只保留热更引擎与网络库;
  2. Assembly-CSharp.dll 拆成 ClientShell.dll(首包)+ GameLogic.dll(扩展包),通过 Assembly.LoadFrom 在首次启动后加载;
  3. Lua 层用 tolua# 反射注册表 只导出首包必要 API,剩余在扩展包第一次使用时 动态 Register

第三步,引擎侧:

  1. IPostGenerateGradleProject 回调里注入 aaptOptions { ignoreAssetsPattern '!.mp4:!.unity3d' },让扩展资源不参与首包压缩;
  2. libunity.so + libil2cpp.soAndroid App Bundle 的 uncompressedNativeLibs 处理,虽然国内不用 AAB,但手动在 build.gradleextractNativeLibs=false,可把 so 留在 APK 不压缩,Google Play 保护机制同样适用,减少 20% 体积
  3. 对 iOS 工程,在 Xcode PostProcess 里把 ODR 标签Unity 的 AssetBundle 名字 一一对应,保证审核时 “首次安装大小”<200 MB

第四步,合规侧:

  1. 首次启动弹 隐私协议 + 流量确认 弹窗,用户点“同意”后才调用 Addressables.DownloadDependenciesAsync
  2. 扩展包统一放到 /Android/obb/<包名>/main.<version>.obb,防止 targetSdkVersion=33/sdcard/ 写入失败;
  3. 下载完成做 CRC+文件大小双校验,失败自动 重试 3 次,仍失败则提示 “重启游戏恢复”,防止渠道审核因 黑屏/卡死 被打回。

通过这四步,我们上线项目把首包从 1.4 GB 压到 780 MB,扩展包 1.8 GB 在 Wi-Fi 下 5 分钟 下完,次留提升 3.2%。”

拓展思考

  1. 如果渠道要求 “秒开”(启动到可点 UI ≤ 2 s),你需要把 首包 Shader 变体预热Runtime.ParseShader 提前到 自定义 Gradle Task预编译 skc 文件,首帧直接 GL.PreProgramShader,可再省 0.8 s
  2. 海外版本需同时支持 Google Play Asset Delivery国内 obb,可写 同一套 Addressable Custom Provider,根据 Application.platform + 渠道宏 自动切换路径,实现 “一套代码,两条管线”
  3. 当扩展包大于 2 GB 时,Android obb 会裂成 main+patch,需要 自己写合并逻辑(patch.obb 优先级高于 main.obb),否则 Unity WWW 加载 AssetBundle 会 404