feat(ui): 添加UI缓存统计和路由守卫功能

- 新增IUiCacheStatistics接口用于UI缓存统计信息
- 为IUiFactory添加缓存策略管理和统计信息获取功能
- 将IUiRouter中的层级管理改为路由守卫功能
- 实现路由守卫的注册、移除和执行逻辑
- 添加缓存配置管理支持
- [skip ci]
This commit is contained in:
GeWuYou 2026-01-20 10:24:23 +08:00
parent e972d926a7
commit ad061bba46
6 changed files with 592 additions and 53 deletions

View File

@ -1,7 +1,40 @@
using GFramework.Core.Abstractions.utility;
using System;
using System.Collections.Generic;
using GFramework.Core.Abstractions.utility;
namespace GFramework.Game.Abstractions.ui;
/// <summary>
/// UI缓存统计信息接口
/// </summary>
public interface IUiCacheStatistics
{
/// <summary>
/// 缓存总数
/// </summary>
int CacheSize { get; }
/// <summary>
/// 缓存命中次数
/// </summary>
int HitCount { get; }
/// <summary>
/// 缓存未命中次数
/// </summary>
int MissCount { get; }
/// <summary>
/// 命中率
/// </summary>
double HitRate { get; }
/// <summary>
/// 最近访问时间
/// </summary>
DateTime? LastAccessTime { get; }
}
/// <summary>
/// UI工厂接口用于创建UI页面实例
/// </summary>
@ -52,4 +85,34 @@ public interface IUiFactory : IContextUtility
/// 检查是否有缓存的实例
/// </summary>
bool HasCached(string uiKey);
#region
/// <summary>
/// 获取UI的缓存配置
/// </summary>
/// <param name="uiKey">UI标识符</param>
/// <returns>缓存配置,如果未设置则返回默认配置</returns>
UiCacheConfig GetCacheConfig(string uiKey);
/// <summary>
/// 设置UI的缓存配置
/// </summary>
/// <param name="uiKey">UI标识符</param>
/// <param name="config">缓存配置</param>
void SetCacheConfig(string uiKey, UiCacheConfig config);
/// <summary>
/// 移除UI的缓存配置恢复默认配置
/// </summary>
/// <param name="uiKey">UI标识符</param>
void RemoveCacheConfig(string uiKey);
/// <summary>
/// 获取所有UI的缓存统计信息
/// </summary>
/// <returns>缓存统计字典</returns>
IDictionary<string, IUiCacheStatistics> GetCacheStatistics();
#endregion
}

View File

@ -0,0 +1,36 @@
using System.Threading.Tasks;
namespace GFramework.Game.Abstractions.ui;
/// <summary>
/// UI路由守卫接口
/// 用于拦截和处理UI路由切换实现业务逻辑解耦
/// </summary>
public interface IUiRouteGuard
{
/// <summary>
/// 守卫优先级,数值越小越先执行
/// </summary>
int Priority { get; }
/// <summary>
/// 是否可中断后续守卫
/// 如果返回 true当该守卫返回 false 时,将停止执行后续守卫
/// </summary>
bool CanInterrupt { get; }
/// <summary>
/// 进入UI前的检查
/// </summary>
/// <param name="uiKey">目标UI标识符</param>
/// <param name="param">进入参数</param>
/// <returns>true表示允许进入false表示拦截</returns>
Task<bool> CanEnterAsync(string uiKey, IUiPageEnterParam? param);
/// <summary>
/// 离开UI前的检查
/// </summary>
/// <param name="uiKey">当前UI标识符</param>
/// <returns>true表示允许离开false表示拦截</returns>
Task<bool> CanLeaveAsync(string uiKey);
}

View File

@ -121,57 +121,25 @@ public interface IUiRouter : ISystem
/// </summary>
bool Contains(string uiKey);
#region
#region
/// <summary>
/// 在指定层级显示UI非栈管理
/// 注册路由守卫
/// </summary>
/// <param name="uiKey">UI标识符</param>
/// <param name="layer">UI层级</param>
/// <param name="param">进入参数</param>
/// <param name="instancePolicy">实例策略</param>
void Show(
string uiKey,
UiLayer layer,
IUiPageEnterParam? param = null,
UiInstancePolicy instancePolicy = UiInstancePolicy.Reuse);
/// <param name="guard">守卫实例</param>
void AddGuard(IUiRouteGuard guard);
/// <summary>
/// 在指定层级显示UI基于实例
/// 移除路由守卫
/// </summary>
/// <param name="page">UI页面实例</param>
/// <param name="layer">UI层级</param>
void Show(IUiPageBehavior page, UiLayer layer);
/// <param name="guard">守卫实例</param>
void RemoveGuard(IUiRouteGuard guard);
/// <summary>
/// 隐藏指定层级的UI
/// 注册路由守卫(泛型方法)
/// </summary>
/// <param name="uiKey">UI标识符</param>
/// <param name="layer">UI层级</param>
/// <param name="destroy">是否销毁实例</param>
void Hide(string uiKey, UiLayer layer, bool destroy = false);
/// <summary>
/// 清空指定层级的所有UI
/// </summary>
/// <param name="layer">UI层级</param>
/// <param name="destroy">是否销毁实例</param>
void ClearLayer(UiLayer layer, bool destroy = false);
/// <summary>
/// 获取指定层级的UI实例
/// </summary>
/// <param name="uiKey">UI标识符</param>
/// <param name="layer">UI层级</param>
/// <returns>UI实例不存在则返回null</returns>
IUiPageBehavior? GetFromLayer(string uiKey, UiLayer layer);
/// <summary>
/// 判断指定层级是否有UI显示
/// </summary>
/// <param name="layer">UI层级</param>
/// <returns>是否有UI显示</returns>
bool HasVisibleInLayer(UiLayer layer);
/// <typeparam name="T">守卫类型,必须实现 IUiRouteGuard 且有无参构造函数</typeparam>
void AddGuard<T>() where T : IUiRouteGuard, new();
#endregion
}

View File

@ -0,0 +1,77 @@
using System;
namespace GFramework.Game.Abstractions.ui;
/// <summary>
/// UI缓存配置
/// 用于配置UI实例的缓存行为
/// </summary>
public class UiCacheConfig
{
/// <summary>
/// 最大缓存数量
/// </summary>
public int MaxCacheSize { get; set; } = 10;
/// <summary>
/// 缓存淘汰策略
/// </summary>
public CacheEvictionPolicy EvictionPolicy { get; set; } = CacheEvictionPolicy.LRU;
/// <summary>
/// 访问后过期时间可选null 表示不启用)
/// </summary>
public TimeSpan? ExpireAfterAccess { get; set; } = null;
/// <summary>
/// 创建默认配置LRU 策略,最大 10 个实例)
/// </summary>
public static UiCacheConfig Default => new UiCacheConfig
{
MaxCacheSize = 10,
EvictionPolicy = CacheEvictionPolicy.LRU,
ExpireAfterAccess = null
};
/// <summary>
/// 创建 LRU 策略配置
/// </summary>
/// <param name="maxSize">最大缓存数量</param>
/// <param name="expireAfter">访问后过期时间</param>
public static UiCacheConfig Lru(int maxSize = 10, TimeSpan? expireAfter = null)
=> new UiCacheConfig
{
MaxCacheSize = maxSize,
EvictionPolicy = CacheEvictionPolicy.LRU,
ExpireAfterAccess = expireAfter
};
/// <summary>
/// 创建 LFU 策略配置
/// </summary>
/// <param name="maxSize">最大缓存数量</param>
/// <param name="expireAfter">访问后过期时间</param>
public static UiCacheConfig Lfu(int maxSize = 10, TimeSpan? expireAfter = null)
=> new UiCacheConfig
{
MaxCacheSize = maxSize,
EvictionPolicy = CacheEvictionPolicy.LFU,
ExpireAfterAccess = expireAfter
};
}
/// <summary>
/// 缓存淘汰策略枚举
/// </summary>
public enum CacheEvictionPolicy
{
/// <summary>
/// 最近最少使用
/// </summary>
LRU,
/// <summary>
/// 最少使用频率
/// </summary>
LFU
}

View File

@ -37,6 +37,11 @@ public abstract class UiRouterBase : AbstractSystem, IUiRouter
private IUiRoot _uiRoot = null!;
/// <summary>
/// 路由守卫列表
/// </summary>
private readonly List<IUiRouteGuard> _guards = new();
/// <summary>
/// 注册UI切换处理器
/// </summary>
@ -92,13 +97,7 @@ public abstract class UiRouterBase : AbstractSystem, IUiRouter
);
BeforeChange(@event);
// 使用工厂的增强方法获取实例
var page = _factory.GetOrCreate(uiKey, instancePolicy);
Log.Debug("Get/Create UI Page instance: {0}", page.GetType().Name);
DoPushPageInternal(page, param, policy);
DoPushPageInternal(uiKey, param, policy, instancePolicy, animationPolicy);
AfterChange(@event);
}
@ -149,6 +148,15 @@ public abstract class UiRouterBase : AbstractSystem, IUiRouter
return;
}
var topUiKey = _stack.Peek().Key;
// 执行离开守卫
if (!ExecuteLeaveGuardsAsync(topUiKey).GetAwaiter().GetResult())
{
Log.Warn("Pop blocked by guard: {0}", topUiKey);
return;
}
var nextUiKey = _stack.Count > 1
? _stack.ElementAt(1).Key // 使用 Key 而不是 View.GetType().Name
: throw new InvalidOperationException("Stack is empty");
@ -362,8 +370,27 @@ public abstract class UiRouterBase : AbstractSystem, IUiRouter
}
/// <summary>
/// 执行Push页面的核心逻辑统一处理
/// 这个方法同时服务于工厂创建和已存在页面两种情况
/// 执行Push页面的核心逻辑基于 uiKey
/// </summary>
private void DoPushPageInternal(string uiKey, IUiPageEnterParam? param, UiTransitionPolicy policy,
UiInstancePolicy instancePolicy, UiAnimationPolicy? animationPolicy)
{
// 执行进入守卫
if (!ExecuteEnterGuardsAsync(uiKey, param).GetAwaiter().GetResult())
{
Log.Warn("Push blocked by guard: {0}", uiKey);
return;
}
// 使用工厂的增强方法获取实例
var page = _factory.GetOrCreate(uiKey, instancePolicy);
Log.Debug("Get/Create UI Page instance: {0}", page.GetType().Name);
DoPushPageInternal(page, param, policy);
}
/// <summary>
/// 执行Push页面的核心逻辑基于 page
/// </summary>
private void DoPushPageInternal(IUiPageBehavior page, IUiPageEnterParam? param, UiTransitionPolicy policy)
{
@ -582,4 +609,124 @@ public abstract class UiRouterBase : AbstractSystem, IUiRouter
}
#endregion
#region
/// <summary>
/// 注册路由守卫
/// </summary>
public void AddGuard(IUiRouteGuard guard)
{
ArgumentNullException.ThrowIfNull(guard);
if (_guards.Contains(guard))
{
Log.Debug("Guard already registered: {0}", guard.GetType().Name);
return;
}
_guards.Add(guard);
// 按优先级排序
_guards.Sort((a, b) => a.Priority.CompareTo(b.Priority));
Log.Debug("Guard registered: {0}, Priority={1}", guard.GetType().Name, guard.Priority);
}
/// <summary>
/// 移除路由守卫
/// </summary>
public void RemoveGuard(IUiRouteGuard guard)
{
ArgumentNullException.ThrowIfNull(guard);
if (_guards.Remove(guard))
{
Log.Debug("Guard removed: {0}", guard.GetType().Name);
}
}
/// <summary>
/// 注册路由守卫(泛型方法)
/// </summary>
public void AddGuard<T>() where T : IUiRouteGuard, new()
{
var guard = new T();
AddGuard(guard);
}
/// <summary>
/// 执行进入守卫
/// </summary>
private async Task<bool> ExecuteEnterGuardsAsync(string uiKey, IUiPageEnterParam? param)
{
foreach (var guard in _guards)
{
try
{
Log.Debug("Executing enter guard: {0} for {1}", guard.GetType().Name, uiKey);
var canEnter = await guard.CanEnterAsync(uiKey, param);
if (!canEnter)
{
Log.Debug("Enter guard blocked: {0}", guard.GetType().Name);
return false;
}
if (guard.CanInterrupt)
{
Log.Debug("Enter guard {0} passed, can interrupt = true", guard.GetType().Name);
return true;
}
}
catch (Exception ex)
{
Log.Error("Enter guard {0} failed: {1}", guard.GetType().Name, ex.Message);
if (guard.CanInterrupt)
{
return false;
}
}
}
return true;
}
/// <summary>
/// 执行离开守卫
/// </summary>
private async Task<bool> ExecuteLeaveGuardsAsync(string uiKey)
{
foreach (var guard in _guards)
{
try
{
Log.Debug("Executing leave guard: {0} for {1}", guard.GetType().Name, uiKey);
var canLeave = await guard.CanLeaveAsync(uiKey);
if (!canLeave)
{
Log.Debug("Leave guard blocked: {0}", guard.GetType().Name);
return false;
}
if (guard.CanInterrupt)
{
Log.Debug("Leave guard {0} passed, can interrupt = true", guard.GetType().Name);
return true;
}
}
catch (Exception ex)
{
Log.Error("Leave guard {0} failed: {1}", guard.GetType().Name, ex.Message);
if (guard.CanInterrupt)
{
return false;
}
}
}
return true;
}
#endregion
}

View File

@ -1,4 +1,5 @@
using GFramework.Core.Abstractions.logging;
using System;
using GFramework.Core.Abstractions.logging;
using GFramework.Core.extensions;
using GFramework.Core.logging;
using GFramework.Core.utility;
@ -14,6 +15,18 @@ namespace GFramework.Godot.ui;
/// </summary>
public class GodotUiFactory : AbstractContextUtility, IUiFactory
{
/// <summary>
/// 缓存统计信息实现类
/// </summary>
private class CacheStatisticsInfo : IUiCacheStatistics
{
public int CacheSize { get; set; }
public int HitCount { get; set; }
public int MissCount { get; set; }
public double HitRate => HitCount + MissCount > 0 ? (double)HitCount / (HitCount + MissCount) : 0;
public DateTime? LastAccessTime { get; set; }
}
private static readonly ILogger Log = LoggerFactoryResolver.Provider.CreateLogger("GodotUiFactory");
private IGodotUiRegistry _registry = null!;
@ -27,6 +40,26 @@ public class GodotUiFactory : AbstractContextUtility, IUiFactory
/// 追踪所有创建的实例(用于清理)
/// </summary>
private readonly Dictionary<string, HashSet<IUiPageBehavior>> _allInstances = new();
/// <summary>
/// 缓存配置uiKey -> 配置
/// </summary>
private readonly Dictionary<string, UiCacheConfig> _cacheConfigs = new();
/// <summary>
/// 缓存统计uiKey -> 统计信息
/// </summary>
private readonly Dictionary<string, CacheStatisticsInfo> _cacheStatistics = new();
/// <summary>
/// LRU访问时间队列uiKey -> 按访问时间排序的实例列表
/// </summary>
private readonly Dictionary<string, List<(IUiPageBehavior instance, DateTime accessTime)>> _accessTimeQueue = new();
/// <summary>
/// LFU访问计数instance -> 访问次数
/// </summary>
private readonly Dictionary<IUiPageBehavior, int> _accessCount = new();
/// <summary>
/// 创建或获取UI页面实例
@ -110,10 +143,63 @@ public class GodotUiFactory : AbstractContextUtility, IUiFactory
// 确保实例处于隐藏状态
page.OnHide();
// 更新统计信息
UpdateStatisticsOnRecycle(uiKey);
// 更新访问追踪
UpdateAccessTracking(uiKey, page);
_cachedInstances[uiKey].Enqueue(page);
Log.Debug("Recycled UI instance to pool: {0}, poolSize={1}", uiKey, _cachedInstances[uiKey].Count);
// 检查是否需要淘汰
CheckAndEvict(uiKey);
}
/// <summary>
/// 获取UI的缓存配置
/// </summary>
public UiCacheConfig GetCacheConfig(string uiKey)
{
return _cacheConfigs.TryGetValue(uiKey, out var config) ? config : UiCacheConfig.Default;
}
/// <summary>
/// 设置UI的缓存配置
/// </summary>
public void SetCacheConfig(string uiKey, UiCacheConfig config)
{
_cacheConfigs[uiKey] = config;
Log.Debug("Set cache config for UI: {0}, MaxSize={1}, Policy={2}", uiKey, config.MaxCacheSize, config.EvictionPolicy);
// 检查是否需要淘汰
CheckAndEvict(uiKey);
}
/// <summary>
/// 移除UI的缓存配置
/// </summary>
public void RemoveCacheConfig(string uiKey)
{
if (_cacheConfigs.Remove(uiKey))
{
Log.Debug("Removed cache config for UI: {0}", uiKey);
}
}
/// <summary>
/// 获取所有UI的缓存统计信息
/// </summary>
public IDictionary<string, IUiCacheStatistics> GetCacheStatistics()
{
var result = new Dictionary<string, IUiCacheStatistics>();
foreach (var kvp in _cacheStatistics)
{
result[kvp.Key] = kvp.Value;
}
return result;
}
/// <summary>
/// 清理指定UI的缓存
/// </summary>
@ -170,11 +256,19 @@ public class GodotUiFactory : AbstractContextUtility, IUiFactory
if (_cachedInstances.TryGetValue(uiKey, out var queue) && queue.Count > 0)
{
var cached = queue.Dequeue();
// 更新统计:缓存命中
UpdateStatisticsOnHit(uiKey);
// 更新访问追踪
UpdateAccessTracking(uiKey, cached);
Log.Debug("Reused cached UI instance: {0}, remainingInPool={1}", uiKey, queue.Count);
return cached;
}
// 没有缓存则创建新实例
UpdateStatisticsOnMiss(uiKey);
Log.Debug("No cached instance, creating new: {0}", uiKey);
return Create(uiKey);
}
@ -205,6 +299,13 @@ public class GodotUiFactory : AbstractContextUtility, IUiFactory
{
set.Remove(page);
}
// 从访问追踪移除
_accessCount.Remove(page);
if (_accessTimeQueue.TryGetValue(uiKey, out var queue))
{
queue.RemoveAll(x => x.instance == page);
}
// 销毁Godot节点
if (page.View is Node node)
@ -212,6 +313,153 @@ public class GodotUiFactory : AbstractContextUtility, IUiFactory
node.QueueFreeX();
}
}
/// <summary>
/// 更新统计信息:回收
/// </summary>
private void UpdateStatisticsOnRecycle(string uiKey)
{
if (!_cacheStatistics.ContainsKey(uiKey))
_cacheStatistics[uiKey] = new CacheStatisticsInfo();
var stats = _cacheStatistics[uiKey];
stats.CacheSize = _cachedInstances[uiKey].Count + 1;
stats.LastAccessTime = DateTime.Now;
}
/// <summary>
/// 更新统计信息:命中
/// </summary>
private void UpdateStatisticsOnHit(string uiKey)
{
if (!_cacheStatistics.ContainsKey(uiKey))
_cacheStatistics[uiKey] = new CacheStatisticsInfo();
var stats = _cacheStatistics[uiKey];
stats.HitCount++;
stats.CacheSize = _cachedInstances[uiKey].Count;
stats.LastAccessTime = DateTime.Now;
}
/// <summary>
/// 更新统计信息:未命中
/// </summary>
private void UpdateStatisticsOnMiss(string uiKey)
{
if (!_cacheStatistics.ContainsKey(uiKey))
_cacheStatistics[uiKey] = new CacheStatisticsInfo();
var stats = _cacheStatistics[uiKey];
stats.MissCount++;
stats.CacheSize = _cachedInstances.TryGetValue(uiKey, out var queue) ? queue.Count : 0;
}
/// <summary>
/// 更新访问追踪
/// </summary>
private void UpdateAccessTracking(string uiKey, IUiPageBehavior instance)
{
var now = DateTime.Now;
// LRU: 更新访问时间队列
if (!_accessTimeQueue.ContainsKey(uiKey))
_accessTimeQueue[uiKey] = new List<(IUiPageBehavior, DateTime)>();
var timeQueue = _accessTimeQueue[uiKey];
timeQueue.RemoveAll(x => x.instance == instance);
timeQueue.Add((instance, now));
// LFU: 更新访问计数
_accessCount.TryGetValue(instance, out var count);
_accessCount[instance] = count + 1;
}
/// <summary>
/// 检查并执行淘汰
/// </summary>
private void CheckAndEvict(string uiKey)
{
var config = GetCacheConfig(uiKey);
var currentSize = _cachedInstances.TryGetValue(uiKey, out var queue) ? queue.Count : 0;
if (currentSize > config.MaxCacheSize)
{
var toEvict = currentSize - config.MaxCacheSize;
for (int i = 0; i < toEvict; i++)
{
if (config.EvictionPolicy == CacheEvictionPolicy.LRU)
EvictLRU(uiKey);
else
EvictLFU(uiKey);
}
Log.Debug("Evicted {0} instances for UI: {1}", toEvict, uiKey);
}
}
/// <summary>
/// LRU淘汰策略
/// </summary>
private void EvictLRU(string uiKey)
{
if (!_accessTimeQueue.TryGetValue(uiKey, out var timeQueue) || timeQueue.Count == 0)
return;
var oldest = timeQueue.OrderBy(x => x.accessTime).First();
// 从队列中移除
if (_cachedInstances.TryGetValue(uiKey, out var queue))
{
var tempQueue = new Queue<IUiPageBehavior>();
var removed = false;
while (queue.Count > 0)
{
var item = queue.Dequeue();
if (!removed && item == oldest.instance)
{
DestroyInstance(item);
removed = true;
}
else
{
tempQueue.Enqueue(item);
}
}
// 重新填充队列
while (tempQueue.Count > 0)
queue.Enqueue(tempQueue.Dequeue());
}
}
/// <summary>
/// LFU淘汰策略
/// </summary>
private void EvictLFU(string uiKey)
{
if (!_cachedInstances.TryGetValue(uiKey, out var queue) || queue.Count == 0)
return;
// 找到访问次数最少的实例
IUiPageBehavior? toRemove = null;
var minCount = int.MaxValue;
foreach (var instance in queue)
{
if (_accessCount.TryGetValue(instance, out var count) && count < minCount)
{
minCount = count;
toRemove = instance;
}
}
if (toRemove != null)
{
DestroyInstance(toRemove);
}
}
#endregion
}