103 lines
3.2 KiB
Markdown
103 lines
3.2 KiB
Markdown
# AudioService 取消语义修复说明
|
||
|
||
日期:2026-05-20
|
||
范围:`My project/Assets/FlowScope/Runtime/Audio/AudioService.cs`
|
||
优先级:P0 小修复
|
||
结论:建议立即顺手修复
|
||
|
||
## 问题概述
|
||
|
||
`AudioService.PlayBgmAsync` 和 `AudioService.PlaySfxAsync` 支持传入 `CancellationToken`,但当前内部 `LoadClipAsync` 会捕获所有异常,并统一包装为 `InvalidOperationException`。
|
||
|
||
这会导致调用方主动取消音频加载时,拿到的不是 `OperationCanceledException`,而是普通失败异常。
|
||
|
||
## 影响
|
||
|
||
取消语义被破坏后,调用方无法可靠区分以下两类情况:
|
||
|
||
- 生命周期或用户操作导致的正常取消。
|
||
- 资源缺失、Addressables 失败、类型不匹配等真实加载错误。
|
||
|
||
可能造成的问题:
|
||
|
||
- 正常取消被记录成错误日志。
|
||
- UI 或业务层误判为资源加载失败。
|
||
- 后续重试、错误兜底、统计埋点出现噪音。
|
||
- 框架内不同服务的取消语义不一致,增加异步生命周期排查成本。
|
||
|
||
## 推荐修改
|
||
|
||
在 `AudioService.LoadClipAsync` 中单独保留 `OperationCanceledException`,不要包装。
|
||
|
||
目标代码形态:
|
||
|
||
```csharp
|
||
private async Task<IResourceHandle<AudioClip>> LoadClipAsync(
|
||
string key,
|
||
CancellationToken cancellationToken)
|
||
{
|
||
try
|
||
{
|
||
var handle = await _resources.LoadAsync<AudioClip>(key, cancellationToken);
|
||
return ValidateLoadedClip(key, handle);
|
||
}
|
||
catch (OperationCanceledException)
|
||
{
|
||
throw;
|
||
}
|
||
catch (Exception exception)
|
||
{
|
||
throw new InvalidOperationException($"Failed to load audio clip '{key}'.", exception);
|
||
}
|
||
}
|
||
```
|
||
|
||
## 测试建议
|
||
|
||
在 `AudioServiceTests` 中补充异步取消测试。
|
||
|
||
建议覆盖:
|
||
|
||
1. `PlayBgmAsync_WhenCanceled_ThrowsOperationCanceledException`
|
||
2. `PlaySfxAsync_WhenCanceled_ThrowsOperationCanceledException`
|
||
|
||
测试要点:
|
||
|
||
- 创建 `CancellationTokenSource`。
|
||
- 调用 `Cancel()`。
|
||
- 调用 `PlayBgmAsync` 或 `PlaySfxAsync`。
|
||
- 断言抛出 `OperationCanceledException`。
|
||
- 断言没有创建新的 `AudioSource`。
|
||
|
||
示例结构:
|
||
|
||
```csharp
|
||
[UnityTest]
|
||
public IEnumerator PlayBgmAsync_WhenCanceled_ThrowsOperationCanceledException()
|
||
{
|
||
var service = CreateService();
|
||
using var cts = new CancellationTokenSource();
|
||
cts.Cancel();
|
||
var sourceCountBeforePlay = Object.FindObjectsOfType<AudioSource>().Length;
|
||
|
||
yield return ThrowsAsync<OperationCanceledException>(
|
||
service.PlayBgmAsync("bgm-a", cancellationToken: cts.Token));
|
||
|
||
Assert.That(Object.FindObjectsOfType<AudioSource>().Length, Is.EqualTo(sourceCountBeforePlay));
|
||
}
|
||
```
|
||
|
||
如果现有测试工具方法只支持 `Func<Task>`,也可以按当前测试文件风格新增一个接收 `Task` 的 `ThrowsAsync<TException>` helper。
|
||
|
||
## 验收标准
|
||
|
||
- `PlayBgmAsync` 在取消时抛出 `OperationCanceledException`。
|
||
- `PlaySfxAsync` 在取消时抛出 `OperationCanceledException`。
|
||
- 取消时不创建 `AudioSource`。
|
||
- 取消时不把异常包装成 `InvalidOperationException`。
|
||
- 现有 PlayMode 音频测试继续通过。
|
||
|
||
## 后续备注
|
||
|
||
该问题不是架构阻塞项,但修复成本很低。建议本轮直接修掉,避免后续异步服务取消语义不一致。
|