如何拆分首包与扩展包减少APK大小
解读
国内安卓渠道(华为、OPPO、小米、应用宝等)对 APK 直接上架的体积红线普遍卡在 1.5 GB,部分渠道甚至 1 GB 就会强制要求“分包+动态下载”。iOS 虽然用 App Store 的 ODR(On-Demand Resources)机制,但审核同样会卡“首次下载大小”与“蜂窝网络下载上限(200 MB)”。因此,面试官想知道你是否能把“必须进首包”与“可以后下”的资源、代码、配置在 Unity 管线层面就拆干净,并给出 可落地的中国渠道合规方案,而不是泛泛而谈“用 AssetBundle”。
知识点
- 首包(主包)体积计算口径:APK 内部所有文件解压后总和,不含 Google Play 的 patch 算法。国内渠道以此为准。
- 资源分级标准:
‑ 启动场景依赖(Shader 变体、首屏 UI、Logo、法律文本)→ 必须首包;
‑ 关卡/角色/音频/视频 → 可扩展;
‑ 热更脚本 DLL(Lua、ILRuntime)→ 可扩展,但需预留脚本引擎与空壳 Assembly-CSharp.dll 接口。 - Unity 可裁剪开关:
‑ PlayerSettings 中 Strip Engine Code、Managed Stripping Level(High)、LZ4HC 压缩;
‑ Shader 变体收集(ShaderVariantCollection)+ IPostGenerateGradleProject 脚本剔除无用变体;
‑ Engine Scenes in Build 只保留 0 号启动场景,其余场景移出 Build Settings。 - Android App Bundle(AAB)≠ 国内方案:国内渠道后台目前 不支持 Google AAB,需自己实现 动态资源下载(AssetBundle)+ 安装后首次启动增量合并。
- 扩展包存储合规:
‑ Android 10+ 分区存储(Scoped Storage),必须放到 /Android/data/<包名>/files/obb/ 或 SAF 路径,否则 targetSdkVersion≥30 时 /sdcard/ 直写会抛异常;
‑ iOS 必须走 ODR 标签组 或 NSBundle.mainBundle 外自己管理沙盒,否则审核 2.3.1 拒审。 - 下载策略:
‑ 国内弱网环境需 CDN 分片+断点续传(最好接入阿里云/腾讯云 Range-Request 支持);
‑ 首次进入游戏只下“前 10 分钟体验包”,后台静默继续下“完整包”,用 Unity 协程 + 线程池 + 流量保护开关;
‑ 渠道合规必须弹 《用户隐私协议》 与 “是否允许移动网络下载” 双弹窗,否则会被渠道下架。 - 版本一致性校验:扩展包用 Unity Manifest 文件(CRC+Hash128) 与 服务器版本号 双校验,防止 CDN 边缘节点回源延迟导致资源不匹配。
- 代码热更边界:
‑ 若用 Lua/ILRuntime,首包必须带 解释器与反射绑定(约 400-600 KB),剩余业务脚本放扩展包;
‑ 若用 HybridCLR,首包只需 AOT 元数据裁剪后的壳,核心逻辑 DLL 放扩展包,首次启动后 Assembly.Load 加载。
答案
“我会把体积治理拆成 资源侧、代码侧、引擎侧、合规侧 四步,确保首包压到 800 MB 以内,扩展包在 Wi-Fi 环境静默下载。
第一步,资源侧:
- 用 ScriptableBuildPipeline(SBP)+ Addressable 把资源按“登录前、新手关、主线关、支线关、高清皮肤、CG 视频”六级打标签;
- 首包只保留 0 号启动场景 + 1 个 UI Atlas + 通用 Shader 变体包(<30 MB),其余全部标记为 DownloadSizeThreshold=0,即安装后必下;
- 对纹理做 ETC2_ASTC 双平台压缩 + 分辨率 512 封顶,对音频做 Vorbis 48 kHz 单声道 80 kbps,粒子特效贴图统一 128*128;
- 把 >500 KB 的序列化 JSON/Excel 配置 转成 MessagePack + LZ4 块,减少首包文本体积。
第二步,代码侧:
- 开启 High Stripping + Strip Engine Code,写 link.xml 白名单 只保留热更引擎与网络库;
- 把 Assembly-CSharp.dll 拆成 ClientShell.dll(首包)+ GameLogic.dll(扩展包),通过 Assembly.LoadFrom 在首次启动后加载;
- Lua 层用 tolua# 反射注册表 只导出首包必要 API,剩余在扩展包第一次使用时 动态 Register。
第三步,引擎侧:
- 在 IPostGenerateGradleProject 回调里注入 aaptOptions { ignoreAssetsPattern '!.mp4:!.unity3d' },让扩展资源不参与首包压缩;
- 把 libunity.so + libil2cpp.so 做 Android App Bundle 的 uncompressedNativeLibs 处理,虽然国内不用 AAB,但手动在 build.gradle 加 extractNativeLibs=false,可把 so 留在 APK 不压缩,Google Play 保护机制同样适用,减少 20% 体积;
- 对 iOS 工程,在 Xcode PostProcess 里把 ODR 标签 与 Unity 的 AssetBundle 名字 一一对应,保证审核时 “首次安装大小”<200 MB。
第四步,合规侧:
- 首次启动弹 隐私协议 + 流量确认 弹窗,用户点“同意”后才调用 Addressables.DownloadDependenciesAsync;
- 扩展包统一放到 /Android/obb/<包名>/main.<version>.obb,防止 targetSdkVersion=33 时 /sdcard/ 写入失败;
- 下载完成做 CRC+文件大小双校验,失败自动 重试 3 次,仍失败则提示 “重启游戏恢复”,防止渠道审核因 黑屏/卡死 被打回。
通过这四步,我们上线项目把首包从 1.4 GB 压到 780 MB,扩展包 1.8 GB 在 Wi-Fi 下 5 分钟 下完,次留提升 3.2%。”
拓展思考
- 如果渠道要求 “秒开”(启动到可点 UI ≤ 2 s),你需要把 首包 Shader 变体预热 从 Runtime.ParseShader 提前到 自定义 Gradle Task 里 预编译 skc 文件,首帧直接 GL.PreProgramShader,可再省 0.8 s。
- 海外版本需同时支持 Google Play Asset Delivery 与 国内 obb,可写 同一套 Addressable Custom Provider,根据 Application.platform + 渠道宏 自动切换路径,实现 “一套代码,两条管线”。
- 当扩展包大于 2 GB 时,Android obb 会裂成 main+patch,需要 自己写合并逻辑(patch.obb 优先级高于 main.obb),否则 Unity WWW 加载 AssetBundle 会 404。