修复预加载释放中的异步写回

This commit is contained in:
JSD\13999
2026-05-21 15:31:39 +08:00
parent e211a7169d
commit 2a88dad6f4
4 changed files with 317 additions and 1 deletions

View File

@@ -13,6 +13,8 @@ namespace FlowScope.UI
private readonly IResourceService _resources;
private readonly Dictionary<Type, IResourceHandle<GameObject>> _handles = new();
private readonly Dictionary<Type, InFlightPreload> _inFlightLoads = new();
private int _releaseGeneration;
private bool _disposed;
public UIPreloadService(IResourceService resources)
{
@@ -29,6 +31,8 @@ namespace FlowScope.UI
string path = null;
lock (_gate)
{
ThrowIfDisposed();
if (TryGetPreloadedHandle(panelType, out _))
{
return;
@@ -39,6 +43,7 @@ namespace FlowScope.UI
var attribute = UIManager.GetPanelAttribute(panelType);
path = UIManager.ResolvePath(panelType, attribute);
inFlightLoad = new InFlightPreload();
inFlightLoad.ReleaseGeneration = _releaseGeneration;
_inFlightLoads.Add(panelType, inFlightLoad);
startLoad = true;
}
@@ -65,6 +70,11 @@ namespace FlowScope.UI
{
lock (_gate)
{
if (_disposed)
{
return false;
}
return TryGetPreloadedHandle(typeof(TPanel), out _);
}
}
@@ -73,6 +83,12 @@ namespace FlowScope.UI
{
lock (_gate)
{
if (_disposed)
{
handle = null;
return false;
}
return TryGetPreloadedHandle(typeof(TPanel), out handle);
}
}
@@ -83,6 +99,11 @@ namespace FlowScope.UI
IResourceHandle<GameObject> handle;
lock (_gate)
{
if (_disposed)
{
return;
}
if (!_handles.TryGetValue(panelType, out handle))
{
return;
@@ -99,6 +120,7 @@ namespace FlowScope.UI
List<IResourceHandle<GameObject>> handles;
lock (_gate)
{
_releaseGeneration++;
handles = new List<IResourceHandle<GameObject>>(_handles.Values);
_handles.Clear();
}
@@ -111,6 +133,16 @@ namespace FlowScope.UI
public void Dispose()
{
lock (_gate)
{
if (_disposed)
{
return;
}
_disposed = true;
}
ReleaseAll();
}
@@ -133,7 +165,9 @@ namespace FlowScope.UI
lock (_gate)
{
_inFlightLoads.Remove(panelType);
if (inFlightLoad.WaiterCount > 0)
if (!_disposed &&
inFlightLoad.ReleaseGeneration == _releaseGeneration &&
inFlightLoad.WaiterCount > 0)
{
_handles[panelType] = handle;
shouldCache = true;
@@ -179,6 +213,14 @@ namespace FlowScope.UI
handle.Asset != null;
}
private void ThrowIfDisposed()
{
if (_disposed)
{
throw new ObjectDisposedException(nameof(UIPreloadService));
}
}
private static async Task WaitForSharedLoadAsync(
Task loadTask,
CancellationToken cancellationToken)
@@ -206,6 +248,7 @@ namespace FlowScope.UI
{
public Task Task { get; set; }
public int WaiterCount { get; set; }
public int ReleaseGeneration { get; set; }
}
}
}

View File

@@ -35,6 +35,9 @@ namespace FlowScope.Samples.MainMenuP0
private bool _shutdown;
public int ReleasedResourceCount => _sampleBackend?.ReleaseCount ?? 0;
/// <summary>
/// True after runtime cleanup has completed; shutdown sub-step failures are still reported by ShutdownAsync.
/// </summary>
public bool IsShutdown => _shutdown;
public Task StartupTask { get; private set; }

View File

@@ -155,6 +155,57 @@ namespace FlowScope.Tests.PlayMode.UI
Assert.That(preload.IsPreloaded<ExplicitPathPanel>(), Is.False);
}
[UnityTest]
public IEnumerator ReleaseAll_WhenLoadInFlight_DoesNotCacheAndDisposesLoadedHandle()
{
var prefab = CreatePrefab<ExplicitPathPanel>("ExplicitPathPrefab");
var resources = new GatedUITestResources("Custom/Explicit", prefab);
var preload = new UIPreloadService(resources);
var task = preload.PreloadAsync<ExplicitPathPanel>(CancellationToken.None);
Assert.That(resources.LoadCalls, Is.EqualTo(1));
preload.ReleaseAll();
resources.Complete();
yield return UITestAsync.Await(task);
Assert.That(resources.Handle.IsDisposed, Is.True);
Assert.That(preload.IsPreloaded<ExplicitPathPanel>(), Is.False);
}
[UnityTest]
public IEnumerator Dispose_WhenLoadInFlight_DoesNotCacheAndDisposesLoadedHandle()
{
var prefab = CreatePrefab<ExplicitPathPanel>("ExplicitPathPrefab");
var resources = new GatedUITestResources("Custom/Explicit", prefab);
var preload = new UIPreloadService(resources);
var task = preload.PreloadAsync<ExplicitPathPanel>(CancellationToken.None);
Assert.That(resources.LoadCalls, Is.EqualTo(1));
preload.Dispose();
resources.Complete();
yield return UITestAsync.Await(task);
Assert.That(resources.Handle.IsDisposed, Is.True);
Assert.That(preload.IsPreloaded<ExplicitPathPanel>(), Is.False);
}
[UnityTest]
public IEnumerator PreloadAsync_AfterDispose_ThrowsObjectDisposedException()
{
var prefab = CreatePrefab<ExplicitPathPanel>("ExplicitPathPrefab");
var resources = new UITestResources();
resources.Register("Custom/Explicit", prefab);
var preload = new UIPreloadService(resources);
preload.Dispose();
yield return UITestAsync.AwaitExpected<ObjectDisposedException>(
preload.PreloadAsync<ExplicitPathPanel>(CancellationToken.None));
Assert.That(resources.LoadedKeys, Is.Empty);
}
[UnityTest]
public IEnumerator OpenAsync_WhenPanelIsPreloaded_ReusesPreloadedHandle()
{