Files
FlowScope/docs/fixes/audio-cancellation-semantics-fix.md
2026-05-20 11:57:00 +08:00

3.2 KiB
Raw Blame History

AudioService 取消语义修复说明

日期2026-05-20
范围:My project/Assets/FlowScope/Runtime/Audio/AudioService.cs
优先级P0 小修复
结论:建议立即顺手修复

问题概述

AudioService.PlayBgmAsyncAudioService.PlaySfxAsync 支持传入 CancellationToken,但当前内部 LoadClipAsync 会捕获所有异常,并统一包装为 InvalidOperationException

这会导致调用方主动取消音频加载时,拿到的不是 OperationCanceledException,而是普通失败异常。

影响

取消语义被破坏后,调用方无法可靠区分以下两类情况:

  • 生命周期或用户操作导致的正常取消。
  • 资源缺失、Addressables 失败、类型不匹配等真实加载错误。

可能造成的问题:

  • 正常取消被记录成错误日志。
  • UI 或业务层误判为资源加载失败。
  • 后续重试、错误兜底、统计埋点出现噪音。
  • 框架内不同服务的取消语义不一致,增加异步生命周期排查成本。

推荐修改

AudioService.LoadClipAsync 中单独保留 OperationCanceledException,不要包装。

目标代码形态:

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()
  • 调用 PlayBgmAsyncPlaySfxAsync
  • 断言抛出 OperationCanceledException
  • 断言没有创建新的 AudioSource

示例结构:

[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>,也可以按当前测试文件风格新增一个接收 TaskThrowsAsync<TException> helper。

验收标准

  • PlayBgmAsync 在取消时抛出 OperationCanceledException
  • PlaySfxAsync 在取消时抛出 OperationCanceledException
  • 取消时不创建 AudioSource
  • 取消时不把异常包装成 InvalidOperationException
  • 现有 PlayMode 音频测试继续通过。

后续备注

该问题不是架构阻塞项,但修复成本很低。建议本轮直接修掉,避免后续异步服务取消语义不一致。