# 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> LoadClipAsync( string key, CancellationToken cancellationToken) { try { var handle = await _resources.LoadAsync(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().Length; yield return ThrowsAsync( service.PlayBgmAsync("bgm-a", cancellationToken: cts.Token)); Assert.That(Object.FindObjectsOfType().Length, Is.EqualTo(sourceCountBeforePlay)); } ``` 如果现有测试工具方法只支持 `Func`,也可以按当前测试文件风格新增一个接收 `Task` 的 `ThrowsAsync` helper。 ## 验收标准 - `PlayBgmAsync` 在取消时抛出 `OperationCanceledException`。 - `PlaySfxAsync` 在取消时抛出 `OperationCanceledException`。 - 取消时不创建 `AudioSource`。 - 取消时不把异常包装成 `InvalidOperationException`。 - 现有 PlayMode 音频测试继续通过。 ## 后续备注 该问题不是架构阻塞项,但修复成本很低。建议本轮直接修掉,避免后续异步服务取消语义不一致。