收束 P1 资源后端适配层

This commit is contained in:
JSD\13999
2026-05-20 20:49:29 +08:00
parent 1c53319961
commit 6e45152250
4 changed files with 129 additions and 31 deletions

View File

@@ -2,7 +2,7 @@ using System;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using FlowScope.Resources; using FlowScope.Resources;
using UnityEngine.AddressableAssets; using UnityAddressables = UnityEngine.AddressableAssets.Addressables;
namespace FlowScope.Addressables namespace FlowScope.Addressables
{ {
@@ -13,7 +13,7 @@ namespace FlowScope.Addressables
public async Task<T> LoadAsync<T>(string key, CancellationToken cancellationToken) public async Task<T> LoadAsync<T>(string key, CancellationToken cancellationToken)
where T : class where T : class
{ {
var handle = Addressables.LoadAssetAsync<T>(key); var handle = UnityAddressables.LoadAssetAsync<T>(key);
await WaitWithCancellation(handle.Task, cancellationToken); await WaitWithCancellation(handle.Task, cancellationToken);
return handle.Result; return handle.Result;
} }
@@ -21,7 +21,7 @@ namespace FlowScope.Addressables
public void Release<T>(string key, T asset) public void Release<T>(string key, T asset)
where T : class where T : class
{ {
Addressables.Release(asset); UnityAddressables.Release(asset);
} }
private static async Task WaitWithCancellation(Task task, CancellationToken cancellationToken) private static async Task WaitWithCancellation(Task task, CancellationToken cancellationToken)

View File

@@ -3,7 +3,8 @@
"rootNamespace": "FlowScope.Addressables", "rootNamespace": "FlowScope.Addressables",
"references": [ "references": [
"FlowScope.Runtime", "FlowScope.Runtime",
"Unity.Addressables" "Unity.Addressables",
"Unity.ResourceManager"
], ],
"includePlatforms": [], "includePlatforms": [],
"excludePlatforms": [], "excludePlatforms": [],

View File

@@ -1,15 +1,18 @@
using System; using System;
using System.Collections;
using System.Runtime.ExceptionServices;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using FlowScope.Resources; using FlowScope.Resources;
using NUnit.Framework; using NUnit.Framework;
using UnityEngine.TestTools;
namespace FlowScope.Tests.EditMode.Resources namespace FlowScope.Tests.EditMode.Resources
{ {
public sealed class ResourceServiceBackendTests public sealed class ResourceServiceBackendTests
{ {
[Test] [UnityTest]
public async Task LoadAsync_WhenSameKeyLoadsConcurrently_UsesOneBackendLoad() public IEnumerator LoadAsync_WhenSameKeyLoadsConcurrently_UsesOneBackendLoad()
{ {
var backend = new FakeResourceBackend(); var backend = new FakeResourceBackend();
var service = new ResourceService(backend); var service = new ResourceService(backend);
@@ -18,15 +21,17 @@ namespace FlowScope.Tests.EditMode.Resources
var second = service.LoadAsync<TestAsset>("shared", CancellationToken.None); var second = service.LoadAsync<TestAsset>("shared", CancellationToken.None);
backend.Gate.SetResult(new TestAsset()); backend.Gate.SetResult(new TestAsset());
using var firstHandle = await first; yield return Await(first);
using var secondHandle = await second; yield return Await(second);
using var firstHandle = Result(first);
using var secondHandle = Result(second);
Assert.AreEqual(1, backend.LoadCalls); Assert.AreEqual(1, backend.LoadCalls);
Assert.AreSame(firstHandle.Asset, secondHandle.Asset); Assert.AreSame(firstHandle.Asset, secondHandle.Asset);
} }
[Test] [UnityTest]
public async Task LoadAsync_WhenCanceled_DoesNotCreateHandle() public IEnumerator LoadAsync_WhenCanceled_DoesNotCreateHandle()
{ {
var backend = new FakeResourceBackend(); var backend = new FakeResourceBackend();
var service = new ResourceService(backend); var service = new ResourceService(backend);
@@ -35,13 +40,20 @@ namespace FlowScope.Tests.EditMode.Resources
var task = service.LoadAsync<TestAsset>("canceled", cts.Token); var task = service.LoadAsync<TestAsset>("canceled", cts.Token);
cts.Cancel(); cts.Cancel();
await ExpectThrowsAsync<OperationCanceledException>(task); try
Assert.IsFalse(service.TryLoadCompleted<TestAsset>("canceled", out var handle)); {
Assert.IsNull(handle); yield return AwaitExpected<OperationCanceledException>(task);
Assert.IsFalse(service.TryLoadCompleted<TestAsset>("canceled", out var handle));
Assert.IsNull(handle);
}
finally
{
backend.Gate.TrySetResult(new TestAsset());
}
} }
[Test] [UnityTest]
public async Task DisposeLastHandle_ReleasesBackendAsset() public IEnumerator DisposeLastHandle_ReleasesBackendAsset()
{ {
var backend = new FakeResourceBackend(); var backend = new FakeResourceBackend();
var service = new ResourceService(backend); var service = new ResourceService(backend);
@@ -49,8 +61,10 @@ namespace FlowScope.Tests.EditMode.Resources
var first = service.LoadAsync<TestAsset>("shared", CancellationToken.None); var first = service.LoadAsync<TestAsset>("shared", CancellationToken.None);
var second = service.LoadAsync<TestAsset>("shared", CancellationToken.None); var second = service.LoadAsync<TestAsset>("shared", CancellationToken.None);
backend.Gate.SetResult(new TestAsset()); backend.Gate.SetResult(new TestAsset());
var firstHandle = await first; yield return Await(first);
var secondHandle = await second; yield return Await(second);
var firstHandle = Result(first);
var secondHandle = Result(second);
firstHandle.Dispose(); firstHandle.Dispose();
Assert.AreEqual(0, backend.ReleaseCalls); Assert.AreEqual(0, backend.ReleaseCalls);
@@ -59,15 +73,16 @@ namespace FlowScope.Tests.EditMode.Resources
Assert.AreEqual(1, backend.ReleaseCalls); Assert.AreEqual(1, backend.ReleaseCalls);
} }
[Test] [UnityTest]
public async Task DisposeLastHandle_RemovesCompletedCacheEntry() public IEnumerator DisposeLastHandle_RemovesCompletedCacheEntry()
{ {
var backend = new FakeResourceBackend(); var backend = new FakeResourceBackend();
var service = new ResourceService(backend); var service = new ResourceService(backend);
var load = service.LoadAsync<TestAsset>("cached", CancellationToken.None); var load = service.LoadAsync<TestAsset>("cached", CancellationToken.None);
backend.Gate.SetResult(new TestAsset()); backend.Gate.SetResult(new TestAsset());
var handle = await load; yield return Await(load);
var handle = Result(load);
Assert.IsTrue(service.TryLoadCompleted<TestAsset>("cached", out var cached)); Assert.IsTrue(service.TryLoadCompleted<TestAsset>("cached", out var cached));
cached.Dispose(); cached.Dispose();
@@ -76,8 +91,8 @@ namespace FlowScope.Tests.EditMode.Resources
Assert.IsFalse(service.TryLoadCompleted<TestAsset>("cached", out _)); Assert.IsFalse(service.TryLoadCompleted<TestAsset>("cached", out _));
} }
[Test] [UnityTest]
public async Task CreateGroup_DisposesAllHandles() public IEnumerator CreateGroup_DisposesAllHandles()
{ {
var backend = new FakeResourceBackend(); var backend = new FakeResourceBackend();
var service = new ResourceService(backend); var service = new ResourceService(backend);
@@ -85,13 +100,15 @@ namespace FlowScope.Tests.EditMode.Resources
var first = service.LoadAsync<TestAsset>("first", CancellationToken.None); var first = service.LoadAsync<TestAsset>("first", CancellationToken.None);
backend.Gate.SetResult(new TestAsset()); backend.Gate.SetResult(new TestAsset());
var firstHandle = await first; yield return Await(first);
var firstHandle = Result(first);
group.Add(firstHandle); group.Add(firstHandle);
backend.Gate = new TaskCompletionSource<object>(); backend.ResetGate();
var second = service.LoadAsync<TestAsset>("second", CancellationToken.None); var second = service.LoadAsync<TestAsset>("second", CancellationToken.None);
backend.Gate.SetResult(new TestAsset()); backend.Gate.SetResult(new TestAsset());
var secondHandle = await second; yield return Await(second);
var secondHandle = Result(second);
group.Add(secondHandle); group.Add(secondHandle);
group.Dispose(); group.Dispose();
@@ -106,7 +123,13 @@ namespace FlowScope.Tests.EditMode.Resources
public string Name => "Fake"; public string Name => "Fake";
public int LoadCalls { get; private set; } public int LoadCalls { get; private set; }
public int ReleaseCalls { get; private set; } public int ReleaseCalls { get; private set; }
public TaskCompletionSource<object> Gate { get; set; } = new(); public TaskCompletionSource<object> Gate { get; private set; } =
new(TaskCreationOptions.RunContinuationsAsynchronously);
public void ResetGate()
{
Gate = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously);
}
public async Task<T> LoadAsync<T>(string key, CancellationToken cancellationToken) public async Task<T> LoadAsync<T>(string key, CancellationToken cancellationToken)
where T : class where T : class
@@ -126,19 +149,72 @@ namespace FlowScope.Tests.EditMode.Resources
{ {
} }
private static async Task ExpectThrowsAsync<TException>(Task task) private static IEnumerator Await(Task task)
{
var deadline = DateTime.UtcNow + TimeSpan.FromSeconds(2);
while (!task.IsCompleted)
{
if (DateTime.UtcNow > deadline)
{
Assert.Fail("Async operation did not complete within 2 seconds.");
}
yield return null;
}
ThrowIfFailed(task);
}
private static IEnumerator AwaitExpected<TException>(Task task)
where TException : Exception where TException : Exception
{ {
try var deadline = DateTime.UtcNow + TimeSpan.FromSeconds(2);
while (!task.IsCompleted)
{ {
await task; if (DateTime.UtcNow > deadline)
{
Assert.Fail("Async operation did not complete within 2 seconds.");
}
yield return null;
} }
catch (TException)
if (task.IsCanceled && typeof(TException) == typeof(OperationCanceledException))
{ {
return; yield break;
}
var exception = task.Exception?.InnerException;
if (exception is TException)
{
yield break;
}
if (exception != null)
{
ExceptionDispatchInfo.Capture(exception).Throw();
} }
Assert.Fail($"Expected {typeof(TException).Name}."); Assert.Fail($"Expected {typeof(TException).Name}.");
} }
private static T Result<T>(Task<T> task)
{
ThrowIfFailed(task);
return task.Result;
}
private static void ThrowIfFailed(Task task)
{
if (task.IsCanceled)
{
throw new OperationCanceledException();
}
if (task.Exception?.InnerException != null)
{
ExceptionDispatchInfo.Capture(task.Exception.InnerException).Throw();
}
}
} }
} }

View File

@@ -9,6 +9,20 @@
}, },
"hash": "329ca7915713417f2e0837b0e0a80b4da074db4a" "hash": "329ca7915713417f2e0837b0e0a80b4da074db4a"
}, },
"com.unity.addressables": {
"version": "1.21.21",
"depth": 0,
"source": "registry",
"dependencies": {
"com.unity.scriptablebuildpipeline": "1.21.23",
"com.unity.modules.assetbundle": "1.0.0",
"com.unity.modules.imageconversion": "1.0.0",
"com.unity.modules.jsonserialize": "1.0.0",
"com.unity.modules.unitywebrequest": "1.0.0",
"com.unity.modules.unitywebrequestassetbundle": "1.0.0"
},
"url": "https://packages.unity.cn"
},
"com.unity.collab-proxy": { "com.unity.collab-proxy": {
"version": "2.12.4", "version": "2.12.4",
"depth": 0, "depth": 0,
@@ -76,6 +90,13 @@
"dependencies": {}, "dependencies": {},
"url": "https://packages.unity.cn" "url": "https://packages.unity.cn"
}, },
"com.unity.scriptablebuildpipeline": {
"version": "1.21.25",
"depth": 1,
"source": "registry",
"dependencies": {},
"url": "https://packages.unity.cn"
},
"com.unity.settings-manager": { "com.unity.settings-manager": {
"version": "2.1.0", "version": "2.1.0",
"depth": 2, "depth": 2,