feat(ui): 实现UI实例池化管理和生命周期优化

- 添加UI实例管理策略枚举(AlwaysCreate、Reuse、Pooled)
- 在GodotUiFactory中实现缓存池和实例回收机制
- 扩展IUiFactory接口支持预加载、回收和缓存管理功能
- 更新UiRouterBase支持实例策略参数传递
- 重构Pop策略将Hide重命名为Cache以明确语义
- 移除项目文件中的冗余文件夹引用
- 添加日志记录便于调试和监控实例状态
- 实现批量预加载和全量缓存清理功能
- 优化页面替换逻辑支持实例复用和池化管理
This commit is contained in:
GeWuYou 2026-01-20 08:32:53 +08:00
parent c2b046e185
commit 760cc71985
8 changed files with 354 additions and 93 deletions

View File

@ -6,27 +6,13 @@
/// </summary>
public enum UiTransitionPolicy
{
/// <summary>
/// 出栈即销毁(一次性页面)
/// 页面从栈中移除时会完全销毁实例
/// </summary>
Destroy,
/// <summary>
/// 出栈隐藏,保留实例
/// 页面从栈中移除时仅隐藏显示,保留实例以便后续重用
/// </summary>
Hide,
/// <summary>
/// 覆盖显示(不影响下层页面)
/// 当前页面覆盖在其他页面之上显示,不影响下层页面的状态
/// </summary>
Overlay,
/// <summary>
/// 独占显示(下层页面 Pause + Hide
/// 当前页面独占显示区域,下层页面会被暂停并隐藏
/// </summary>
Exclusive
Exclusive,
/// <summary>
/// 覆盖显示(下层页面仅 Pause不隐藏
/// </summary>
Overlay
}

View File

@ -8,9 +8,48 @@ namespace GFramework.Game.Abstractions.ui;
public interface IUiFactory : IContextUtility
{
/// <summary>
/// 根据UI键值创建对应的UI页面实例
/// 创建或获取UI页面实例
/// </summary>
/// <param name="uiKey">UI标识键</param>
/// <param name="policy">实例管理策略</param>
/// <returns>UI页面实例</returns>
IUiPageBehavior GetOrCreate(string uiKey, UiInstancePolicy policy = UiInstancePolicy.AlwaysCreate);
/// <summary>
/// 仅创建新实例(不使用缓存)
/// </summary>
/// <param name="uiKey">UI标识键用于确定要创建的具体UI页面类型</param>
/// <returns>创建的UI页面实例实现IUiPage接口</returns>
IUiPageBehavior Create(string uiKey);
/// <summary>
/// 预加载UI资源到缓存池
/// </summary>
/// <param name="uiKey">UI标识键</param>
/// <param name="count">预加载数量默认1个</param>
void Preload(string uiKey, int count = 1);
/// <summary>
/// 批量预加载
/// </summary>
void PreloadBatch(params string[] uiKeys);
/// <summary>
/// 回收实例到缓存池
/// </summary>
/// <param name="page">要回收的页面实例</param>
void Recycle(IUiPageBehavior page);
/// <summary>
/// 清理指定UI的缓存实例
/// </summary>
void ClearCache(string uiKey);
/// <summary>
/// 清理所有缓存
/// </summary>
void ClearAllCache();
/// <summary>
/// 检查是否有缓存的实例
/// </summary>
bool HasCached(string uiKey);
}

View File

@ -25,7 +25,10 @@ public interface IUiRouter : ISystem
/// <param name="uiKey">UI界面的唯一标识符</param>
/// <param name="param">进入界面的参数,可为空</param>
/// <param name="policy">界面切换策略默认为Exclusive独占</param>
void Push(string uiKey, IUiPageEnterParam? param = null, UiTransitionPolicy policy = UiTransitionPolicy.Exclusive);
/// <param name="instancePolicy">实例管理策略默认为Reuse复用</param>
void Push(string uiKey, IUiPageEnterParam? param = null, UiTransitionPolicy policy = UiTransitionPolicy.Exclusive,
UiInstancePolicy instancePolicy = UiInstancePolicy.Reuse);
/// <summary>
/// 将已存在的UI页面压入路由栈
@ -37,6 +40,7 @@ public interface IUiRouter : ISystem
void Push(IUiPageBehavior page, IUiPageEnterParam? param = null,
UiTransitionPolicy policy = UiTransitionPolicy.Exclusive);
/// <summary>
/// 弹出路由栈顶的UI界面返回到上一个界面
/// </summary>
@ -44,21 +48,33 @@ public interface IUiRouter : ISystem
void Pop(UiPopPolicy policy = UiPopPolicy.Destroy);
/// <summary>
/// 替换当前UI界面为指定的新界面
/// 替换当前所有页面为新页面基于uiKey
/// </summary>
/// <param name="uiKey">新UI界面的唯一标识符</param>
/// <param name="param">进入界面的参数,可为空</param>
/// <param name="popPolicy">界面弹出策略,默认为销毁当前界面</param>
/// <param name="pushPolicy">界面过渡策略,默认为独占模式</param>
void Replace(
/// <param name="uiKey">新UI页面标识符</param>
/// <param name="param">页面进入参数,可为空</param>
/// <param name="popPolicy">弹出页面时的销毁策略,默认为销毁</param>
/// <param name="pushPolicy">推入页面时的过渡策略,默认为独占</param>
/// <param name="instancePolicy">实例管理策略</param>
public void Replace(
string uiKey,
IUiPageEnterParam? param = null,
UiPopPolicy popPolicy = UiPopPolicy.Destroy,
UiTransitionPolicy pushPolicy = UiTransitionPolicy.Exclusive
);
UiTransitionPolicy pushPolicy = UiTransitionPolicy.Exclusive,
UiInstancePolicy instancePolicy = UiInstancePolicy.Reuse);
/// <summary>
/// 替换当前所有页面为已存在的页面(基于实例)
/// </summary>
/// <param name="page">已创建的UI页面行为实例</param>
/// <param name="param">页面进入参数,可为空</param>
/// <param name="popPolicy">弹出页面时的销毁策略,默认为销毁</param>
/// <param name="pushPolicy">推入页面时的过渡策略,默认为独占</param>
public void Replace(
IUiPageBehavior page,
IUiPageEnterParam? param = null,
UiPopPolicy popPolicy = UiPopPolicy.Destroy,
UiTransitionPolicy pushPolicy = UiTransitionPolicy.Exclusive);
/// <summary>
/// 清空所有UI界面重置路由状态
/// </summary>
void Clear();

View File

@ -0,0 +1,23 @@

namespace GFramework.Game.Abstractions.ui;
/// <summary>
/// UI页面实例管理策略控制实例的生命周期
/// </summary>
public enum UiInstancePolicy
{
/// <summary>
/// 总是创建新实例
/// </summary>
AlwaysCreate,
/// <summary>
/// 复用已存在的实例(如果有)
/// </summary>
Reuse,
/// <summary>
/// 从预加载池中获取或创建
/// </summary>
Pooled
}

View File

@ -6,12 +6,12 @@
public enum UiPopPolicy
{
/// <summary>
/// 销毁模式关闭时完全销毁UI对象
/// 销毁实例
/// </summary>
Destroy,
/// <summary>
/// 隐藏模式关闭时仅隐藏UI对象保留实例
/// 隐藏但保留实例下次Push可复用
/// </summary>
Hide
Cache
}

View File

@ -13,7 +13,4 @@
<ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="13.0.4"/>
</ItemGroup>
<ItemGroup>
<Folder Include="registry\"/>
</ItemGroup>
</Project>

View File

@ -1,7 +1,3 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using GFramework.Core.Abstractions.logging;
using GFramework.Core.extensions;
using GFramework.Core.logging;
@ -62,18 +58,17 @@ public abstract class UiRouterBase : AbstractSystem, IUiRouter
_uiRoot = root;
Log.Debug("Bind UI Root: {0}", root.GetType().Name);
}
/// <summary>
/// 将指定UI页面压入栈顶并显示
/// 将指定的UI界面压入路由栈显示新的UI界面
/// </summary>
/// <param name="uiKey">UI页面标识符</param>
/// <param name="param">页面进入参数,可为空</param>
/// <param name="policy">页面切换策略</param>
public void Push(
string uiKey,
IUiPageEnterParam? param = null,
UiTransitionPolicy policy = UiTransitionPolicy.Exclusive
)
/// <param name="uiKey">UI界面的唯一标识符</param>
/// <param name="param">进入界面的参数,可为空</param>
/// <param name="policy">界面切换策略默认为Exclusive独占</param>
/// <param name="instancePolicy">实例管理策略默认为Reuse复用</param>
public void Push(string uiKey, IUiPageEnterParam? param = null, UiTransitionPolicy policy = UiTransitionPolicy.Exclusive,
UiInstancePolicy instancePolicy = UiInstancePolicy.Reuse)
{
if (IsTop(uiKey))
{
@ -84,15 +79,15 @@ public abstract class UiRouterBase : AbstractSystem, IUiRouter
var @event = CreateEvent(uiKey, UiTransitionType.Push, policy, param);
Log.Debug(
"Push UI Page: key={0}, policy={1}, stackBefore={2}",
uiKey, policy, _stack.Count
"Push UI Page: key={0}, policy={1}, instancePolicy={2}, stackBefore={3}",
uiKey, policy, instancePolicy, _stack.Count
);
BeforeChange(@event);
// 先创建页面然后使用统一的Push逻辑
var page = _factory.Create(uiKey);
Log.Debug("Create UI Page instance: {0}", page.GetType().Name);
// 使用工厂的增强方法获取实例
var page = _factory.GetOrCreate(uiKey, instancePolicy);
Log.Debug("Get/Create UI Page instance: {0}", page.GetType().Name);
DoPushPageInternal(page, param, policy);
@ -144,8 +139,8 @@ public abstract class UiRouterBase : AbstractSystem, IUiRouter
}
var nextUiKey = _stack.Count > 1
? _stack.ElementAt(1).View.GetType().Name
: string.Empty;
? _stack.ElementAt(1).Key // 使用 Key 而不是 View.GetType().Name
: throw new InvalidOperationException("Stack is empty");
var @event = CreateEvent(nextUiKey, UiTransitionType.Pop);
BeforeChange(@event);
@ -156,33 +151,67 @@ public abstract class UiRouterBase : AbstractSystem, IUiRouter
}
/// <summary>
/// 替换当前所有页面为新页面
/// 替换当前所有页面为新页面基于uiKey
/// </summary>
/// <param name="uiKey">新UI页面标识符</param>
/// <param name="param">页面进入参数,可为空</param>
/// <param name="popPolicy">弹出页面时的销毁策略,默认为销毁</param>
/// <param name="pushPolicy">推入页面时的过渡策略,默认为独占</param>
/// <param name="instancePolicy">实例管理策略</param>
public void Replace(
string uiKey,
IUiPageEnterParam? param = null,
UiPopPolicy popPolicy = UiPopPolicy.Destroy,
UiTransitionPolicy pushPolicy = UiTransitionPolicy.Exclusive
)
UiTransitionPolicy pushPolicy = UiTransitionPolicy.Exclusive,
UiInstancePolicy instancePolicy = UiInstancePolicy.Reuse)
{
var @event = CreateEvent(uiKey, UiTransitionType.Replace, pushPolicy, param);
Log.Debug(
"Replace UI Stack with page: key={0}, popPolicy={1}, pushPolicy={2}",
"Replace UI Stack with page: key={0}, popPolicy={1}, pushPolicy={2}, instancePolicy={3}",
uiKey, popPolicy, pushPolicy, instancePolicy
);
BeforeChange(@event);
// 使用内部方法清空栈避免触发额外的Pipeline
DoClearInternal(popPolicy);
// 使用工厂的增强方法获取实例
var page = _factory.GetOrCreate(uiKey, instancePolicy);
Log.Debug("Get/Create UI Page instance for Replace: {0}", page.GetType().Name);
DoPushPageInternal(page, param, pushPolicy);
AfterChange(@event);
}
/// <summary>
/// 替换当前所有页面为已存在的页面(基于实例)
/// </summary>
/// <param name="page">已创建的UI页面行为实例</param>
/// <param name="param">页面进入参数,可为空</param>
/// <param name="popPolicy">弹出页面时的销毁策略,默认为销毁</param>
/// <param name="pushPolicy">推入页面时的过渡策略,默认为独占</param>
public void Replace(
IUiPageBehavior page,
IUiPageEnterParam? param = null,
UiPopPolicy popPolicy = UiPopPolicy.Destroy,
UiTransitionPolicy pushPolicy = UiTransitionPolicy.Exclusive)
{
var uiKey = page.Key;
var @event = CreateEvent(uiKey, UiTransitionType.Replace, pushPolicy, param);
Log.Debug(
"Replace UI Stack with existing page: key={0}, popPolicy={1}, pushPolicy={2}",
uiKey, popPolicy, pushPolicy
);
BeforeChange(@event);
// 使用内部方法避免触发额外的Pipeline
// 清空栈
DoClearInternal(popPolicy);
var page = _factory.Create(uiKey);
Log.Debug("Create UI Page instance for Replace: {0}", page.GetType().Name);
Log.Debug("Use existing UI Page instance for Replace: {0}", page.GetType().Name);
DoPushPageInternal(page, param, pushPolicy);
AfterChange(@event);
@ -372,11 +401,13 @@ public abstract class UiRouterBase : AbstractSystem, IUiRouter
{
Log.Debug("Destroy UI Page: {0}", top.GetType().Name);
_uiRoot.RemoveUiPage(top);
// 不回收,直接销毁
}
else
else // UiPopPolicy.Cache
{
Log.Debug("Hide UI Page: {0}", top.GetType().Name);
top.OnHide();
Log.Debug("Cache UI Page: {0}", top.GetType().Name);
_uiRoot.RemoveUiPage(top);
_factory.Recycle(top); // 回收到池中
}
if (_stack.Count > 0)

View File

@ -1,6 +1,10 @@
using GFramework.Core.extensions;
using GFramework.Core.Abstractions.logging;
using GFramework.Core.extensions;
using GFramework.Core.logging;
using GFramework.Core.utility;
using GFramework.Game.Abstractions.ui;
using GFramework.Godot.extensions;
using Godot;
namespace GFramework.Godot.ui;
@ -10,39 +14,204 @@ namespace GFramework.Godot.ui;
/// </summary>
public class GodotUiFactory : AbstractContextUtility, IUiFactory
{
/// <summary>
/// UI注册表用于存储和获取PackedScene类型的UI资源
/// </summary>
private static readonly ILogger Log = LoggerFactoryResolver.Provider.CreateLogger("GodotUiFactory");
private IGodotUiRegistry _registry = null!;
/// <summary>
/// 缓存池uiKey -> 实例队列
/// </summary>
private readonly Dictionary<string, Queue<IUiPageBehavior>> _cachedInstances = new();
/// <summary>
/// 追踪所有创建的实例(用于清理)
/// </summary>
private readonly Dictionary<string, HashSet<IUiPageBehavior>> _allInstances = new();
/// <summary>
/// 根据指定的UI键创建UI页面实例
/// 创建或获取UI页面实例
/// </summary>
/// <param name="uiKey">UI资源的唯一标识键</param>
/// <returns>实现IUiPage接口的UI页面实例</returns>
/// <exception cref="InvalidCastException">当UI场景没有继承IUiPage接口时抛出</exception>
public IUiPageBehavior Create(string uiKey)
public IUiPageBehavior GetOrCreate(string uiKey, UiInstancePolicy policy = UiInstancePolicy.AlwaysCreate)
{
// 从注册表中获取对应的场景资源
var scene = _registry.Get(uiKey);
// 实例化场景节点
var node = scene.Instantiate();
// 验证节点是否实现了IUiPageProvider接口
if (node is not IUiPageBehaviorProvider provider)
throw new InvalidCastException(
$"UI scene {uiKey} must implement IUiPageBehaviorProvider");
// 获取并返回页面行为实例
return provider.GetPage();
return policy switch
{
UiInstancePolicy.Reuse => GetCachedOrCreate(uiKey),
UiInstancePolicy.Pooled => GetFromPoolOrCreate(uiKey),
_ => Create(uiKey)
};
}
/// <summary>
/// 初始化方法获取UI注册表实例
/// 仅创建新实例
/// </summary>
public IUiPageBehavior Create(string uiKey)
{
var scene = _registry.Get(uiKey);
var node = scene.Instantiate();
if (node is not IUiPageBehaviorProvider provider)
throw new InvalidCastException($"UI scene {uiKey} must implement IUiPageBehaviorProvider");
var page = provider.GetPage();
// 追踪实例
if (!_allInstances.ContainsKey(uiKey))
_allInstances[uiKey] = new HashSet<IUiPageBehavior>();
_allInstances[uiKey].Add(page);
Log.Debug("Created new UI instance: {0}", uiKey);
return page;
}
/// <summary>
/// 预加载UI资源
/// </summary>
public void Preload(string uiKey, int count = 1)
{
Log.Debug("Preloading UI: {0}, count={1}", uiKey, count);
if (!_cachedInstances.ContainsKey(uiKey))
_cachedInstances[uiKey] = new Queue<IUiPageBehavior>();
var queue = _cachedInstances[uiKey];
for (int i = 0; i < count; i++)
{
var instance = Create(uiKey);
// 预加载的实例初始状态为隐藏
instance.OnHide();
queue.Enqueue(instance);
}
Log.Debug("Preloaded {0} instances of {1}", count, uiKey);
}
/// <summary>
/// 批量预加载
/// </summary>
public void PreloadBatch(params string[] uiKeys)
{
foreach (var uiKey in uiKeys)
{
Preload(uiKey);
}
}
/// <summary>
/// 回收实例到缓存池
/// </summary>
public void Recycle(IUiPageBehavior page)
{
var uiKey = page.Key;
if (!_cachedInstances.ContainsKey(uiKey))
_cachedInstances[uiKey] = new Queue<IUiPageBehavior>();
// 确保实例处于隐藏状态
page.OnHide();
_cachedInstances[uiKey].Enqueue(page);
Log.Debug("Recycled UI instance to pool: {0}, poolSize={1}", uiKey, _cachedInstances[uiKey].Count);
}
/// <summary>
/// 清理指定UI的缓存
/// </summary>
public void ClearCache(string uiKey)
{
if (!_cachedInstances.TryGetValue(uiKey, out var queue))
return;
int count = queue.Count;
while (queue.Count > 0)
{
var instance = queue.Dequeue();
DestroyInstance(instance);
}
_cachedInstances.Remove(uiKey);
Log.Debug("Cleared cache for UI: {0}, destroyed {1} instances", uiKey, count);
}
/// <summary>
/// 清理所有缓存
/// </summary>
public void ClearAllCache()
{
foreach (var uiKey in _cachedInstances.Keys)
{
ClearCache(uiKey);
}
Log.Debug("Cleared all UI caches");
}
/// <summary>
/// 检查是否有缓存的实例
/// </summary>
public bool HasCached(string uiKey)
{
return _cachedInstances.TryGetValue(uiKey, out var queue) && queue.Count > 0;
}
protected override void OnInit()
{
_registry = this.GetUtility<IGodotUiRegistry>()!;
}
#region Private Methods
/// <summary>
/// 获取缓存实例或创建新实例Reuse策略
/// </summary>
private IUiPageBehavior GetCachedOrCreate(string uiKey)
{
// 优先从缓存池获取
if (_cachedInstances.TryGetValue(uiKey, out var queue) && queue.Count > 0)
{
var cached = queue.Dequeue();
Log.Debug("Reused cached UI instance: {0}, remainingInPool={1}", uiKey, queue.Count);
return cached;
}
// 没有缓存则创建新实例
Log.Debug("No cached instance, creating new: {0}", uiKey);
return Create(uiKey);
}
/// <summary>
/// 从池中获取或创建Pooled策略
/// 如果池为空,自动创建并填充
/// </summary>
private IUiPageBehavior GetFromPoolOrCreate(string uiKey)
{
// 如果池为空,先预加载一个
if (HasCached(uiKey)) return GetCachedOrCreate(uiKey);
Log.Debug("Pool empty, preloading instance: {0}", uiKey);
Preload(uiKey);
return GetCachedOrCreate(uiKey);
}
/// <summary>
/// 销毁实例
/// </summary>
private void DestroyInstance(IUiPageBehavior page)
{
var uiKey = page.Key;
// 从追踪列表移除
if (_allInstances.TryGetValue(uiKey, out var set))
{
set.Remove(page);
}
// 销毁Godot节点
if (page.View is Node node)
{
node.QueueFreeX();
}
}
#endregion
}