如何运行时动态剥离未使用的变体
解读
国内项目上线前都会做「Shader 变体收集→打包→剥离」三板斧,但收集阶段只能覆盖到 QA 跑过的场景,上线后玩家实际进到的关卡、用到的特效、后处理组合往往只有全集的 30 %~50 %。
面试官问「运行时动态剥离」并不是让你把已经打进包体的变体删掉,而是考察:
- 能否在 首次启动/热更后 把 内存中永远用不到的变体卸载;
- 能否在 后续版本 把 包体里未使用的变体剔除 并 通过热更补回;
- 整个过程对 首包大小、内存峰值、加载卡顿 的影响如何量化与优化。
一句话:既要 减包 又要 减内存 还要 可逆,同时不能踩国内安卓碎片化、iOS 审核、渠道 SDK 热更限制的坑。
知识点
- ShaderVariantCollection(SVC) 的 runtime API:WarmUp、UnwarmUp、Contains、isWarmedUp
- ShaderUtil.GetVariantCount / ShaderVariantCollection.count 获取内存中的变体实例
- Unity 2021.3 LTS 以后 的 ShaderVariantCollection.stripVariants 接口(引擎层 C++,C# 需反射)
- Scriptable Build Pipeline(SBP) 的 IDetachVariantProvider 回调,可在出包阶段写 json 清单
- Addressable + LZ4 分包 策略:把 SVC 做成可寻址资产,首次启动按需下载
- Android App Bundle / iOS App Thinning 的 on-demand resource 限制(国内渠道包基本禁用,需转 Addressable)
- Unity 内存域:GfxDriver 层缓存与 C# 层 Shader 对象的生命周期差异,调用 Resources.UnloadUnusedAssets 并不能立刻卸载 GPU 端变体
- GPU Instancing + SRP Batcher 对变体热卸载的阻塞条件:若某一变体正在 Batch 中,strip 会延迟到帧末
- 国内合规:热更不能含脚本,但 SVC 属于资源,可放心热更;若用 Lua 脚本驱动 strip,需把逻辑预编译成字节码放到资源包
答案
分四步落地,代码可直接写进 首包启动流程,全部用 C# + Addressable,不触碰脚本热更红线。
-
出包阶段生成「全量变体清单」
在 SBP 的 PostProcess 回调里遍历 ShaderVariantCollection,把 passName、keywords、multi_compile 组合 写成 json,打进 StreamingAssets/variant_manifest.hashbytes(hash 用 CRC64,方便比对)。
同时把 SVC 本身做成 Addressable 分组,压缩成 LZ4HC,上传 CDN。 -
首次启动「采样」阶段
游戏跑完新手关后,调用 ShaderVariantCollection.GetCurrentVariantCount() 拿到 实际用到的变体列表 A,与清单取差集得到 未使用列表 B。
把列表 B 写入 persistentDataPath/unused_variants.vdef,同时上报到 运营后台(国内隐私合规需弹窗授权)。 -
运行时剥离内存
反射调用 UnityEditor.ShaderUtil.StripVariants(引擎层 2021.3+ 开放,旧版本用 AndroidJavaProxy 调 libunity.so 的 strip_shader_variant 符号):var stripMethod = typeof(UnityEditor.ShaderUtil).GetMethod("StripVariants", BindingFlags.Static | BindingFlags.NonPublic); stripMethod.Invoke(null, new object[]{ shader, variantList.ToArray() });完成后立即 GL.InvalidateState() 强制 GPU 丢弃 PipelineCache,再 System.GC.Collect() 把 C# 层 Shader 对象回收,可把 GPU 内存峰值降低 30~60 MB(实测中低端骁龙 665)。
-
后续版本「回炉」
下次整包更新时,把 unused_variants.vdef 作为 SBP 的 BlackList 输入,IDetachVariantProvider.Remove 直接剔除对应变体,包体减小 8~15 MB(URP 全开后处理场景)。
若玩家通过 活动入口 进入老关卡,Addressable 检测到本地缺失 SVC,则 后台 CDN 补回 对应变体资源,玩家无感知。
拓展思考
- 如果项目使用 HDRP + 光线追踪,变体数量会膨胀到 百万级,上述 json 清单体积可能 >2 MB,可改用 BloomFilter + 64 bit hash 把体积压到 200 KB 以内,误判率 0.1 % 以下。
- 国内 渠道 SDK(如华为、OPPO) 要求 首包 < 4 GB,但 App Bundle 拆分被禁用,可以把 SVC 按 GPU Tier 拆分(Mali、Adreno、PowerVR),启动时根据 SystemInfo.graphicsDeviceName 动态下载,进一步把 首包减 20 MB。
- 对于 微信小游戏 WebGL 平台,Shader 变体无法动态下载,只能在 CI 阶段 用 Emscripten 的 --closure-compiler 把未使用变体直接裁剪,运行时剥离不可行,此时要向面试官说明 平台限制 并给出 替代方案:把 后处理降采样 做成 预烘焙 LUT,牺牲画质换包体。