diff --git a/GFramework.Game.Abstractions/ui/IUiFactory.cs b/GFramework.Game.Abstractions/ui/IUiFactory.cs index b7e1199..70bcf56 100644 --- a/GFramework.Game.Abstractions/ui/IUiFactory.cs +++ b/GFramework.Game.Abstractions/ui/IUiFactory.cs @@ -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; +/// +/// UI缓存统计信息接口 +/// +public interface IUiCacheStatistics +{ + /// + /// 缓存总数 + /// + int CacheSize { get; } + + /// + /// 缓存命中次数 + /// + int HitCount { get; } + + /// + /// 缓存未命中次数 + /// + int MissCount { get; } + + /// + /// 命中率 + /// + double HitRate { get; } + + /// + /// 最近访问时间 + /// + DateTime? LastAccessTime { get; } +} + /// /// UI工厂接口,用于创建UI页面实例 /// @@ -52,4 +85,34 @@ public interface IUiFactory : IContextUtility /// 检查是否有缓存的实例 /// bool HasCached(string uiKey); + + #region 缓存策略管理 + + /// + /// 获取UI的缓存配置 + /// + /// UI标识符 + /// 缓存配置,如果未设置则返回默认配置 + UiCacheConfig GetCacheConfig(string uiKey); + + /// + /// 设置UI的缓存配置 + /// + /// UI标识符 + /// 缓存配置 + void SetCacheConfig(string uiKey, UiCacheConfig config); + + /// + /// 移除UI的缓存配置,恢复默认配置 + /// + /// UI标识符 + void RemoveCacheConfig(string uiKey); + + /// + /// 获取所有UI的缓存统计信息 + /// + /// 缓存统计字典 + IDictionary GetCacheStatistics(); + + #endregion } \ No newline at end of file diff --git a/GFramework.Game.Abstractions/ui/IUiRouteGuard.cs b/GFramework.Game.Abstractions/ui/IUiRouteGuard.cs new file mode 100644 index 0000000..bdff7b8 --- /dev/null +++ b/GFramework.Game.Abstractions/ui/IUiRouteGuard.cs @@ -0,0 +1,36 @@ +using System.Threading.Tasks; + +namespace GFramework.Game.Abstractions.ui; + +/// +/// UI路由守卫接口 +/// 用于拦截和处理UI路由切换,实现业务逻辑解耦 +/// +public interface IUiRouteGuard +{ + /// + /// 守卫优先级,数值越小越先执行 + /// + int Priority { get; } + + /// + /// 是否可中断后续守卫 + /// 如果返回 true,当该守卫返回 false 时,将停止执行后续守卫 + /// + bool CanInterrupt { get; } + + /// + /// 进入UI前的检查 + /// + /// 目标UI标识符 + /// 进入参数 + /// true表示允许进入,false表示拦截 + Task CanEnterAsync(string uiKey, IUiPageEnterParam? param); + + /// + /// 离开UI前的检查 + /// + /// 当前UI标识符 + /// true表示允许离开,false表示拦截 + Task CanLeaveAsync(string uiKey); +} diff --git a/GFramework.Game.Abstractions/ui/IUiRouter.cs b/GFramework.Game.Abstractions/ui/IUiRouter.cs index c12172c..08bd15d 100644 --- a/GFramework.Game.Abstractions/ui/IUiRouter.cs +++ b/GFramework.Game.Abstractions/ui/IUiRouter.cs @@ -121,57 +121,25 @@ public interface IUiRouter : ISystem /// bool Contains(string uiKey); - #region 层级管理 + #region 路由守卫 /// - /// 在指定层级显示UI(非栈管理) + /// 注册路由守卫 /// - /// UI标识符 - /// UI层级 - /// 进入参数 - /// 实例策略 - void Show( - string uiKey, - UiLayer layer, - IUiPageEnterParam? param = null, - UiInstancePolicy instancePolicy = UiInstancePolicy.Reuse); + /// 守卫实例 + void AddGuard(IUiRouteGuard guard); /// - /// 在指定层级显示UI(基于实例) + /// 移除路由守卫 /// - /// UI页面实例 - /// UI层级 - void Show(IUiPageBehavior page, UiLayer layer); + /// 守卫实例 + void RemoveGuard(IUiRouteGuard guard); /// - /// 隐藏指定层级的UI + /// 注册路由守卫(泛型方法) /// - /// UI标识符 - /// UI层级 - /// 是否销毁实例 - void Hide(string uiKey, UiLayer layer, bool destroy = false); - - /// - /// 清空指定层级的所有UI - /// - /// UI层级 - /// 是否销毁实例 - void ClearLayer(UiLayer layer, bool destroy = false); - - /// - /// 获取指定层级的UI实例 - /// - /// UI标识符 - /// UI层级 - /// UI实例,不存在则返回null - IUiPageBehavior? GetFromLayer(string uiKey, UiLayer layer); - - /// - /// 判断指定层级是否有UI显示 - /// - /// UI层级 - /// 是否有UI显示 - bool HasVisibleInLayer(UiLayer layer); + /// 守卫类型,必须实现 IUiRouteGuard 且有无参构造函数 + void AddGuard() where T : IUiRouteGuard, new(); #endregion } \ No newline at end of file diff --git a/GFramework.Game.Abstractions/ui/UiCacheConfig.cs b/GFramework.Game.Abstractions/ui/UiCacheConfig.cs new file mode 100644 index 0000000..c5e14ca --- /dev/null +++ b/GFramework.Game.Abstractions/ui/UiCacheConfig.cs @@ -0,0 +1,77 @@ +using System; + +namespace GFramework.Game.Abstractions.ui; + +/// +/// UI缓存配置 +/// 用于配置UI实例的缓存行为 +/// +public class UiCacheConfig +{ + /// + /// 最大缓存数量 + /// + public int MaxCacheSize { get; set; } = 10; + + /// + /// 缓存淘汰策略 + /// + public CacheEvictionPolicy EvictionPolicy { get; set; } = CacheEvictionPolicy.LRU; + + /// + /// 访问后过期时间(可选,null 表示不启用) + /// + public TimeSpan? ExpireAfterAccess { get; set; } = null; + + /// + /// 创建默认配置(LRU 策略,最大 10 个实例) + /// + public static UiCacheConfig Default => new UiCacheConfig + { + MaxCacheSize = 10, + EvictionPolicy = CacheEvictionPolicy.LRU, + ExpireAfterAccess = null + }; + + /// + /// 创建 LRU 策略配置 + /// + /// 最大缓存数量 + /// 访问后过期时间 + public static UiCacheConfig Lru(int maxSize = 10, TimeSpan? expireAfter = null) + => new UiCacheConfig + { + MaxCacheSize = maxSize, + EvictionPolicy = CacheEvictionPolicy.LRU, + ExpireAfterAccess = expireAfter + }; + + /// + /// 创建 LFU 策略配置 + /// + /// 最大缓存数量 + /// 访问后过期时间 + public static UiCacheConfig Lfu(int maxSize = 10, TimeSpan? expireAfter = null) + => new UiCacheConfig + { + MaxCacheSize = maxSize, + EvictionPolicy = CacheEvictionPolicy.LFU, + ExpireAfterAccess = expireAfter + }; +} + +/// +/// 缓存淘汰策略枚举 +/// +public enum CacheEvictionPolicy +{ + /// + /// 最近最少使用 + /// + LRU, + + /// + /// 最少使用频率 + /// + LFU +} diff --git a/GFramework.Game/ui/UiRouterBase.cs b/GFramework.Game/ui/UiRouterBase.cs index 4ffd2fe..70b5a79 100644 --- a/GFramework.Game/ui/UiRouterBase.cs +++ b/GFramework.Game/ui/UiRouterBase.cs @@ -37,6 +37,11 @@ public abstract class UiRouterBase : AbstractSystem, IUiRouter private IUiRoot _uiRoot = null!; + /// + /// 路由守卫列表 + /// + private readonly List _guards = new(); + /// /// 注册UI切换处理器 /// @@ -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 } /// - /// 执行Push页面的核心逻辑(统一处理) - /// 这个方法同时服务于工厂创建和已存在页面两种情况 + /// 执行Push页面的核心逻辑(基于 uiKey) + /// + 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); + } + + /// + /// 执行Push页面的核心逻辑(基于 page) /// private void DoPushPageInternal(IUiPageBehavior page, IUiPageEnterParam? param, UiTransitionPolicy policy) { @@ -582,4 +609,124 @@ public abstract class UiRouterBase : AbstractSystem, IUiRouter } #endregion + + #region 路由守卫 + + +/// +/// 注册路由守卫 +/// +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); +} + +/// +/// 移除路由守卫 +/// +public void RemoveGuard(IUiRouteGuard guard) +{ + ArgumentNullException.ThrowIfNull(guard); + + if (_guards.Remove(guard)) + { + Log.Debug("Guard removed: {0}", guard.GetType().Name); + } +} + +/// +/// 注册路由守卫(泛型方法) +/// +public void AddGuard() where T : IUiRouteGuard, new() +{ + var guard = new T(); + AddGuard(guard); +} + +/// +/// 执行进入守卫 +/// +private async Task 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; +} + +/// +/// 执行离开守卫 +/// +private async Task 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 } \ No newline at end of file diff --git a/GFramework.Godot/ui/GodotUiFactory.cs b/GFramework.Godot/ui/GodotUiFactory.cs index a5f57dd..db6c568 100644 --- a/GFramework.Godot/ui/GodotUiFactory.cs +++ b/GFramework.Godot/ui/GodotUiFactory.cs @@ -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; /// public class GodotUiFactory : AbstractContextUtility, IUiFactory { + /// + /// 缓存统计信息实现类 + /// + 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 /// 追踪所有创建的实例(用于清理) /// private readonly Dictionary> _allInstances = new(); + + /// + /// 缓存配置:uiKey -> 配置 + /// + private readonly Dictionary _cacheConfigs = new(); + + /// + /// 缓存统计:uiKey -> 统计信息 + /// + private readonly Dictionary _cacheStatistics = new(); + + /// + /// LRU访问时间队列:uiKey -> 按访问时间排序的实例列表 + /// + private readonly Dictionary> _accessTimeQueue = new(); + + /// + /// LFU访问计数:instance -> 访问次数 + /// + private readonly Dictionary _accessCount = new(); /// /// 创建或获取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); } + /// + /// 获取UI的缓存配置 + /// + public UiCacheConfig GetCacheConfig(string uiKey) + { + return _cacheConfigs.TryGetValue(uiKey, out var config) ? config : UiCacheConfig.Default; + } + + /// + /// 设置UI的缓存配置 + /// + 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); + } + + /// + /// 移除UI的缓存配置 + /// + public void RemoveCacheConfig(string uiKey) + { + if (_cacheConfigs.Remove(uiKey)) + { + Log.Debug("Removed cache config for UI: {0}", uiKey); + } + } + + /// + /// 获取所有UI的缓存统计信息 + /// + public IDictionary GetCacheStatistics() + { + var result = new Dictionary(); + foreach (var kvp in _cacheStatistics) + { + result[kvp.Key] = kvp.Value; + } + return result; + } + /// /// 清理指定UI的缓存 /// @@ -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(); } } + + /// + /// 更新统计信息:回收 + /// + 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; + } + + /// + /// 更新统计信息:命中 + /// + 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; + } + + /// + /// 更新统计信息:未命中 + /// + 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; + } + + /// + /// 更新访问追踪 + /// + 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; + } + + /// + /// 检查并执行淘汰 + /// + 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); + } + } + + /// + /// LRU淘汰策略 + /// + 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(); + 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()); + } + } + + /// + /// LFU淘汰策略 + /// + 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 } \ No newline at end of file