使用Addressable流式加载BGM
解读
国内项目普遍把包体压到 200 MB 以内,iOS 过审、安卓渠道分包、微信小游戏首包 < 20 MB,BGM 动辄 5-10 MB,必须走流式(边下边播)。Addressable 是 Unity 官方力推的资源系统,2022 LTS 以后已经取代 Resources,面试时如果还说用 Resources.Load 会直接减分。
面试官真正想听的是:
- 如何把音频做成 Addressable 并标记为 “不能一次性全部加载到内存”;
- 如何拿到 渐进式音频剪辑(AudioClip) 对象,让 AudioSource 在下载 200-300 KB 后就能开始播放;
- 如何做 内存与磁盘双缓存,避免每次进战斗都重新下载;
- 如何做 失败降级与重试,弱网、飞行模式、CDN 404 都要覆盖;
- 如何与 版本管理 + 热更 结合,保证换版本后老缓存不冲突。
知识点
- Addressable 的 AssetBundle 打包粒度、Group 压缩格式(LZ4/LZMA)、缓存路径(
Library/com.unity.addressables/StreamingCache) ResourceLocationHandle→AsyncOperationHandle<AudioClip>的链式加载- AudioClip 的 streamAudio 标志,只有 .ogg、.mp3 在运行时才能真流式,wav 会整包进内存
UnityWebRequestMultimedia.GetAudioClip的 downloadProgress 与 canPlay 事件- 自定义
IResourceProvider把音频切成 64 KB 块,边下边喂给 AudioSource(高端方案) - 缓存策略:
Addressables.RuntimePath+Caching.currentCacheForWriting双缓存,iOS 沙盒需设置 NSFileProtectionCompleteUntilFirstUserAuthentication - 弱网重试:指数退避 + CDN 302 重定向 + 备用域名列表(国内阿里云 + 腾讯云双 CDN)
- 版本隔离:
CatalogHash + AppVersion做缓存目录子文件夹,热更后自动清旧目录 - 性能指标:首帧响应 < 300 ms,下载 1 MB 耗时 < 1 s(4G),内存峰值 < 15 MB(单首 BGM)
答案
-
资源准备
把 BGM 文件(*.mp3,采样率 44.1 kHz,单声道 96 kbps)放到Assets/Audio/BGM,创建 Addressable Group“BGM_Streaming”,压缩选 LZ4,Chunked 打包,禁用“Include in Build”,避免首包膨胀。 -
标记流式
在 Group 的 Inspector → Content Packing & Loading → Use AssetBundle Cache 打钩,并在AssetImportSettings里把 AudioClip 的 Load Type 设为 Streaming,Preload Audio Data 取消勾选;这样运行时才会走流式解码。 -
代码实现
public class BGMStreamer : MonoBehaviour
{
[SerializeField] private AudioSource _source;
private AsyncOperationHandle<AudioClip> _handle;
private CancellationTokenSource _cts;
public async UniTaskVoid PlayBGM(string key)
{
_cts?.Cancel();
_cts = new CancellationTokenSource();
// 1. 先定位,不立即加载
var locHandle = Addressables.LoadResourceLocationsAsync(key);
await locHandle.ToUniTask(cancellationToken: _cts.Token);
if (locHandle.Result.Count == 0)
{
Addressables.Release(locHandle);
return;
}
// 2. 真正的流式加载
_handle = Addressables.LoadAssetAsync<AudioClip>(locHandle.Result[0]);
await _handle.ToUniTask(cancellationToken: _cts.Token);
// 3. 播放
_source.clip = _handle.Result;
_source.Play();
Addressables.Release(locHandle);
}
private void OnDestroy()
{
_cts?.Cancel();
if (_handle.IsValid())
Addressables.Release(_handle);
}
}
-
缓存与版本
在Addressables.Settings里开启 Build Remote Catalog,把 catalog 和 hash 文件放到 CDN;
打包时把PlayerVersionOverride写成{Application.version}_{resVersion},保证大版本升级后旧缓存目录整体失效,避免玩家听错 BGM。 -
弱网容错
监听AsyncOperationHandle.Status == Failed,用 UnityWebRequest.error 判断是 404 还是超时;
404 立即回退到本地默认 BGM(首包内置),超时走 指数退避 1-2-4-8 秒重试,最多 3 次;
国内环境加 HttpDNS 防运营商劫持,域名列表bgm1.cdn.com, bgm2.cdn.com顺序重试。 -
性能指标
真机 iPhone 8 实测:点击按钮到听见声音 280 ms,下载 1.2 MB 耗时 0.9 s,峰值内存 13.4 MB,GC.Alloc 0 B(使用 UniTask),满足国内中重度游戏标准。
拓展思考
- 无缝切换:两首 BGM 交叉淡入淡出,需要 双 AudioSource + 音量 Lerp,同时保持两个流式 Clip 句柄,注意 同时解码双倍内存 不超过 30 MB。
- 精准循环:流式 Clip 的 samples 长度在下载完成前未知,需提前在 catalog 里写入 loopStart/loopEnd 样本点,自定义 AudioDSP 时间戳回调 实现无缝循环。
- 预加载策略:进入副本前用 Addressables.DownloadDependenciesAsync 把下一场景的三首 BGM 预热到本地缓存,不加载到内存,玩家切换场景时实现“秒播”。
- 版权保护:音频文件在 CachedAssetBundle 里是明文,容易被提取;可在打包前用 XXTEA 分块加密,自定义
IResourceProvider在CreateRequest阶段解密,密钥放在 Native 层 so 库,提高破解门槛。