diff --git a/GFramework.Core.Abstractions/resource/IResourceHandle.cs b/GFramework.Core.Abstractions/resource/IResourceHandle.cs new file mode 100644 index 0000000..bfdfc50 --- /dev/null +++ b/GFramework.Core.Abstractions/resource/IResourceHandle.cs @@ -0,0 +1,38 @@ +namespace GFramework.Core.Abstractions.resource; + +/// +/// 资源句柄接口,用于管理资源的生命周期和引用 +/// +/// 资源类型 +public interface IResourceHandle : IDisposable where T : class +{ + /// + /// 获取资源实例 + /// + T? Resource { get; } + + /// + /// 获取资源路径 + /// + string Path { get; } + + /// + /// 获取资源是否有效(未被释放) + /// + bool IsValid { get; } + + /// + /// 获取当前引用计数 + /// + int ReferenceCount { get; } + + /// + /// 增加引用计数 + /// + void AddReference(); + + /// + /// 减少引用计数 + /// + void RemoveReference(); +} \ No newline at end of file diff --git a/GFramework.Core.Abstractions/resource/IResourceLoader.cs b/GFramework.Core.Abstractions/resource/IResourceLoader.cs new file mode 100644 index 0000000..01e8834 --- /dev/null +++ b/GFramework.Core.Abstractions/resource/IResourceLoader.cs @@ -0,0 +1,35 @@ +namespace GFramework.Core.Abstractions.resource; + +/// +/// 资源加载器接口,用于加载特定类型的资源 +/// +/// 资源类型 +public interface IResourceLoader where T : class +{ + /// + /// 同步加载资源 + /// + /// 资源路径 + /// 加载的资源实例 + T Load(string path); + + /// + /// 异步加载资源 + /// + /// 资源路径 + /// 加载的资源实例 + Task LoadAsync(string path); + + /// + /// 卸载资源 + /// + /// 要卸载的资源 + void Unload(T resource); + + /// + /// 检查是否支持加载指定路径的资源 + /// + /// 资源路径 + /// 如果支持返回 true,否则返回 false + bool CanLoad(string path); +} \ No newline at end of file diff --git a/GFramework.Core.Abstractions/resource/IResourceManager.cs b/GFramework.Core.Abstractions/resource/IResourceManager.cs new file mode 100644 index 0000000..dbdc5bd --- /dev/null +++ b/GFramework.Core.Abstractions/resource/IResourceManager.cs @@ -0,0 +1,89 @@ +using GFramework.Core.Abstractions.utility; + +namespace GFramework.Core.Abstractions.resource; + +/// +/// 资源管理器接口,提供资源加载、缓存和卸载功能 +/// 线程安全:所有方法都是线程安全的 +/// +public interface IResourceManager : IUtility +{ + /// + /// 获取已加载资源的数量 + /// + int LoadedResourceCount { get; } + + /// + /// 同步加载资源 + /// + /// 资源类型 + /// 资源路径 + /// 资源实例,如果加载失败返回 null + T? Load(string path) where T : class; + + /// + /// 异步加载资源 + /// + /// 资源类型 + /// 资源路径 + /// 资源实例,如果加载失败返回 null + Task LoadAsync(string path) where T : class; + + /// + /// 获取资源句柄(增加引用计数) + /// + /// 资源类型 + /// 资源路径 + /// 资源句柄 + IResourceHandle? GetHandle(string path) where T : class; + + /// + /// 卸载指定路径的资源 + /// + /// 资源路径 + /// 如果成功卸载返回 true,否则返回 false + bool Unload(string path); + + /// + /// 卸载所有资源 + /// + void UnloadAll(); + + /// + /// 检查资源是否已加载 + /// + /// 资源路径 + /// 如果已加载返回 true,否则返回 false + bool IsLoaded(string path); + + /// + /// 注册资源加载器 + /// + /// 资源类型 + /// 资源加载器 + void RegisterLoader(IResourceLoader loader) where T : class; + + /// + /// 取消注册资源加载器 + /// + /// 资源类型 + void UnregisterLoader() where T : class; + + /// + /// 预加载资源(加载但不返回) + /// + /// 资源类型 + /// 资源路径 + Task PreloadAsync(string path) where T : class; + + /// + /// 获取所有已加载资源的路径 + /// + IEnumerable GetLoadedResourcePaths(); + + /// + /// 设置资源释放策略 + /// + /// 资源释放策略 + void SetReleaseStrategy(IResourceReleaseStrategy strategy); +} \ No newline at end of file diff --git a/GFramework.Core.Abstractions/resource/IResourceReleaseStrategy.cs b/GFramework.Core.Abstractions/resource/IResourceReleaseStrategy.cs new file mode 100644 index 0000000..c3ae8f8 --- /dev/null +++ b/GFramework.Core.Abstractions/resource/IResourceReleaseStrategy.cs @@ -0,0 +1,16 @@ +namespace GFramework.Core.Abstractions.resource; + +/// +/// 资源释放策略接口 +/// 定义当资源引用计数变化时的处理行为 +/// +public interface IResourceReleaseStrategy +{ + /// + /// 判断是否应该释放资源 + /// + /// 资源路径 + /// 当前引用计数 + /// 如果应该释放返回 true,否则返回 false + bool ShouldRelease(string path, int refCount); +} \ No newline at end of file diff --git a/GFramework.Core.Tests/resource/ResourceManagerTests.cs b/GFramework.Core.Tests/resource/ResourceManagerTests.cs new file mode 100644 index 0000000..61a12ea --- /dev/null +++ b/GFramework.Core.Tests/resource/ResourceManagerTests.cs @@ -0,0 +1,407 @@ +using System.IO; +using GFramework.Core.Abstractions.resource; +using GFramework.Core.resource; +using NUnit.Framework; + +namespace GFramework.Core.Tests.resource; + +/// +/// 测试用的简单资源类 +/// +public class TestResource +{ + public string Content { get; set; } = string.Empty; + public bool IsDisposed { get; set; } +} + +/// +/// 测试用的资源加载器 +/// +public class TestResourceLoader : IResourceLoader +{ + private readonly Dictionary _resourceData = new(); + + public TestResource Load(string path) + { + if (_resourceData.TryGetValue(path, out var content)) + { + return new TestResource { Content = content }; + } + + throw new FileNotFoundException($"Resource not found: {path}"); + } + + public async Task LoadAsync(string path) + { + await Task.Delay(10); // 模拟异步加载 + return Load(path); + } + + public void Unload(TestResource resource) + { + resource.IsDisposed = true; + } + + public bool CanLoad(string path) + { + return _resourceData.ContainsKey(path); + } + + public void AddTestData(string path, string content) + { + _resourceData[path] = content; + } +} + +/// +/// ResourceManager 功能测试类 +/// +[TestFixture] +public class ResourceManagerTests +{ + [SetUp] + public void SetUp() + { + _resourceManager = new ResourceManager(); + _testLoader = new TestResourceLoader(); + _testLoader.AddTestData("test/resource1.txt", "Content 1"); + _testLoader.AddTestData("test/resource2.txt", "Content 2"); + _resourceManager.RegisterLoader(_testLoader); + } + + [TearDown] + public void TearDown() + { + _resourceManager.UnloadAll(); + } + + private ResourceManager _resourceManager = null!; + private TestResourceLoader _testLoader = null!; + + [Test] + public void Load_Should_Load_Resource() + { + var resource = _resourceManager.Load("test/resource1.txt"); + + Assert.That(resource, Is.Not.Null); + Assert.That(resource!.Content, Is.EqualTo("Content 1")); + } + + [Test] + public void Load_Should_Return_Cached_Resource() + { + var resource1 = _resourceManager.Load("test/resource1.txt"); + var resource2 = _resourceManager.Load("test/resource1.txt"); + + Assert.That(resource1, Is.SameAs(resource2)); + } + + [Test] + public async Task LoadAsync_Should_Load_Resource() + { + var resource = await _resourceManager.LoadAsync("test/resource1.txt"); + + Assert.That(resource, Is.Not.Null); + Assert.That(resource!.Content, Is.EqualTo("Content 1")); + } + + [Test] + public void Load_Should_Throw_When_No_Loader_Registered() + { + _resourceManager.UnregisterLoader(); + + Assert.Throws(() => { _resourceManager.Load("test/resource1.txt"); }); + } + + [Test] + public void IsLoaded_Should_Return_True_When_Resource_Loaded() + { + _resourceManager.Load("test/resource1.txt"); + + Assert.That(_resourceManager.IsLoaded("test/resource1.txt"), Is.True); + } + + [Test] + public void IsLoaded_Should_Return_False_When_Resource_Not_Loaded() + { + Assert.That(_resourceManager.IsLoaded("test/nonexistent.txt"), Is.False); + } + + [Test] + public void Unload_Should_Remove_Resource() + { + _resourceManager.Load("test/resource1.txt"); + + var unloaded = _resourceManager.Unload("test/resource1.txt"); + + Assert.That(unloaded, Is.True); + Assert.That(_resourceManager.IsLoaded("test/resource1.txt"), Is.False); + } + + [Test] + public void UnloadAll_Should_Remove_All_Resources() + { + _resourceManager.Load("test/resource1.txt"); + _resourceManager.Load("test/resource2.txt"); + + _resourceManager.UnloadAll(); + + Assert.That(_resourceManager.LoadedResourceCount, Is.EqualTo(0)); + } + + [Test] + public void LoadedResourceCount_Should_Return_Correct_Count() + { + _resourceManager.Load("test/resource1.txt"); + _resourceManager.Load("test/resource2.txt"); + + Assert.That(_resourceManager.LoadedResourceCount, Is.EqualTo(2)); + } + + [Test] + public void GetLoadedResourcePaths_Should_Return_All_Paths() + { + _resourceManager.Load("test/resource1.txt"); + _resourceManager.Load("test/resource2.txt"); + + var paths = _resourceManager.GetLoadedResourcePaths().ToList(); + + Assert.That(paths, Has.Count.EqualTo(2)); + Assert.That(paths, Contains.Item("test/resource1.txt")); + Assert.That(paths, Contains.Item("test/resource2.txt")); + } + + [Test] + public void GetHandle_Should_Return_Valid_Handle() + { + _resourceManager.Load("test/resource1.txt"); + + var handle = _resourceManager.GetHandle("test/resource1.txt"); + + Assert.That(handle, Is.Not.Null); + Assert.That(handle!.IsValid, Is.True); + Assert.That(handle.Resource, Is.Not.Null); + Assert.That(handle.Path, Is.EqualTo("test/resource1.txt")); + } + + [Test] + public void GetHandle_Should_Return_Null_When_Resource_Not_Loaded() + { + var handle = _resourceManager.GetHandle("test/nonexistent.txt"); + + Assert.That(handle, Is.Null); + } + + [Test] + public void Handle_Dispose_Should_Decrease_Reference_Count() + { + _resourceManager.Load("test/resource1.txt"); + + var handle = _resourceManager.GetHandle("test/resource1.txt"); + handle!.Dispose(); + + // 资源应该仍然存在,因为还有一个初始引用 + Assert.That(_resourceManager.IsLoaded("test/resource1.txt"), Is.True); + } + + [Test] + public async Task PreloadAsync_Should_Load_Resource() + { + await _resourceManager.PreloadAsync("test/resource1.txt"); + + Assert.That(_resourceManager.IsLoaded("test/resource1.txt"), Is.True); + } + + [Test] + public void Load_Should_Throw_When_Path_Is_Null_Or_Empty() + { + Assert.Throws(() => _resourceManager.Load(null!)); + Assert.Throws(() => _resourceManager.Load("")); + Assert.Throws(() => _resourceManager.Load(" ")); + } + + [Test] + public void RegisterLoader_Should_Throw_When_Loader_Is_Null() + { + Assert.Throws(() => { _resourceManager.RegisterLoader(null!); }); + } + + [Test] + public void Concurrent_Load_Should_Be_Thread_Safe() + { + const int threadCount = 10; + var tasks = new Task[threadCount]; + var exceptions = new List(); + var resources = new TestResource?[threadCount]; + + for (var i = 0; i < threadCount; i++) + { + var index = i; + tasks[i] = Task.Run(() => + { + try + { + resources[index] = _resourceManager.Load("test/resource1.txt"); + } + catch (Exception ex) + { + lock (exceptions) + { + exceptions.Add(ex); + } + } + }); + } + + Task.WaitAll(tasks); + + Assert.That(exceptions, Is.Empty); + Assert.That(resources.All(r => r != null), Is.True); + Assert.That(resources.All(r => r == resources[0]), Is.True, "所有线程应该获得同一个资源实例"); + } + + [Test] + public async Task Concurrent_LoadAsync_Should_Be_Thread_Safe() + { + const int threadCount = 10; + var tasks = new Task[threadCount]; + + for (var i = 0; i < threadCount; i++) + { + tasks[i] = _resourceManager.LoadAsync("test/resource1.txt"); + } + + var resources = await Task.WhenAll(tasks); + + Assert.That(resources.All(r => r != null), Is.True); + Assert.That(resources.All(r => r == resources[0]), Is.True, "所有任务应该获得同一个资源实例"); + } + + [Test] + public void Concurrent_Load_Different_Resources_Should_Work() + { + const int threadCount = 2; + var tasks = new Task[threadCount]; + var exceptions = new List(); + + tasks[0] = Task.Run(() => + { + try + { + for (var i = 0; i < 100; i++) + { + _resourceManager.Load("test/resource1.txt"); + } + } + catch (Exception ex) + { + lock (exceptions) + { + exceptions.Add(ex); + } + } + }); + + tasks[1] = Task.Run(() => + { + try + { + for (var i = 0; i < 100; i++) + { + _resourceManager.Load("test/resource2.txt"); + } + } + catch (Exception ex) + { + lock (exceptions) + { + exceptions.Add(ex); + } + } + }); + + Task.WaitAll(tasks); + + Assert.That(exceptions, Is.Empty); + Assert.That(_resourceManager.LoadedResourceCount, Is.EqualTo(2)); + } + + [Test] + public void ManualReleaseStrategy_Should_Not_Auto_Unload() + { + _resourceManager.SetReleaseStrategy(new ManualReleaseStrategy()); + _resourceManager.Load("test/resource1.txt"); + + using (var handle = _resourceManager.GetHandle("test/resource1.txt")) + { + handle?.Dispose(); + } + + // 验证资源仍然在缓存中 + Assert.That(_resourceManager.LoadedResourceCount, Is.EqualTo(1)); + Assert.That(_resourceManager.IsLoaded("test/resource1.txt"), Is.True); + + // 手动获取资源验证其未被卸载 + var cachedResource = _resourceManager.GetHandle("test/resource1.txt"); + Assert.That(cachedResource, Is.Not.Null); + Assert.That(cachedResource!.Resource, Is.Not.Null); + } + + [Test] + public void AutoReleaseStrategy_Should_Auto_Unload_When_RefCount_Zero() + { + _resourceManager.SetReleaseStrategy(new AutoReleaseStrategy()); + _resourceManager.Load("test/resource1.txt"); + var handle = _resourceManager.GetHandle("test/resource1.txt"); + + // 释放句柄 + handle!.Dispose(); + + // 自动释放策略:资源应该被自动卸载 + Assert.That(_resourceManager.IsLoaded("test/resource1.txt"), Is.False); + } + + [Test] + public void AutoReleaseStrategy_Should_Not_Unload_With_Active_References() + { + _resourceManager.SetReleaseStrategy(new AutoReleaseStrategy()); + _resourceManager.Load("test/resource1.txt"); + var handle1 = _resourceManager.GetHandle("test/resource1.txt"); + var handle2 = _resourceManager.GetHandle("test/resource1.txt"); + + // 释放一个句柄 + handle1!.Dispose(); + + // 还有一个活跃引用,资源不应该被卸载 + Assert.That(_resourceManager.IsLoaded("test/resource1.txt"), Is.True); + + // 释放第二个句柄 + handle2!.Dispose(); + + // 现在资源应该被自动卸载 + Assert.That(_resourceManager.IsLoaded("test/resource1.txt"), Is.False); + } + + [Test] + public void SetReleaseStrategy_Should_Throw_When_Strategy_Is_Null() + { + Assert.Throws(() => { _resourceManager.SetReleaseStrategy(null!); }); + } + + [Test] + public void Can_Switch_Between_Release_Strategies() + { + // 开始使用手动释放策略 + _resourceManager.Load("test/resource1.txt"); + var handle1 = _resourceManager.GetHandle("test/resource1.txt"); + handle1!.Dispose(); + Assert.That(_resourceManager.IsLoaded("test/resource1.txt"), Is.True); + + // 切换到自动释放策略 + _resourceManager.SetReleaseStrategy(new AutoReleaseStrategy()); + _resourceManager.Load("test/resource2.txt"); + var handle2 = _resourceManager.GetHandle("test/resource2.txt"); + handle2!.Dispose(); + Assert.That(_resourceManager.IsLoaded("test/resource2.txt"), Is.False); + } +} \ No newline at end of file diff --git a/GFramework.Core/configuration/ConfigurationManager.cs b/GFramework.Core/configuration/ConfigurationManager.cs index 3bf749c..bc515de 100644 --- a/GFramework.Core/configuration/ConfigurationManager.cs +++ b/GFramework.Core/configuration/ConfigurationManager.cs @@ -3,6 +3,8 @@ using System.IO; using System.Text.Json; using GFramework.Core.Abstractions.configuration; using GFramework.Core.Abstractions.events; +using GFramework.Core.Abstractions.logging; +using GFramework.Core.logging; namespace GFramework.Core.configuration; @@ -32,6 +34,8 @@ public class ConfigurationManager : IConfigurationManager /// private readonly ConcurrentDictionary _configs = new(); + private readonly ILogger _logger = LoggerFactoryResolver.Provider.CreateLogger(nameof(ConfigurationManager)); + /// /// 用于保护监听器列表的锁 /// @@ -289,7 +293,7 @@ public class ConfigurationManager : IConfigurationManager catch (Exception ex) { // 防止监听器异常影响其他监听器 - Console.Error.WriteLine( + _logger.Error( $"[ConfigurationManager] Error in config watcher for key '{key}': {ex.Message}"); } } diff --git a/GFramework.Core/coroutine/CoroutineScheduler.cs b/GFramework.Core/coroutine/CoroutineScheduler.cs index eb7e04a..6b9d7f2 100644 --- a/GFramework.Core/coroutine/CoroutineScheduler.cs +++ b/GFramework.Core/coroutine/CoroutineScheduler.cs @@ -1,5 +1,7 @@ using GFramework.Core.Abstractions.coroutine; +using GFramework.Core.Abstractions.logging; using GFramework.Core.coroutine.instructions; +using GFramework.Core.logging; namespace GFramework.Core.coroutine; @@ -15,6 +17,7 @@ public sealed class CoroutineScheduler( byte instanceId = 1, int initialCapacity = 256) { + private readonly ILogger _logger = LoggerFactoryResolver.Provider.CreateLogger(nameof(CoroutineScheduler)); private readonly Dictionary _metadata = new(); private readonly Dictionary> _tagged = new(); private readonly ITimeSource _timeSource = timeSource ?? throw new ArgumentNullException(nameof(timeSource)); @@ -400,13 +403,13 @@ public sealed class CoroutineScheduler( catch (Exception callbackEx) { // 防止回调异常传播,记录到控制台 - Console.Error.WriteLine($"[CoroutineScheduler] Exception in error callback: {callbackEx}"); + _logger.Error($"[CoroutineScheduler] Exception in error callback: {callbackEx}"); } }); } // 输出到控制台作为后备 - Console.Error.WriteLine($"[CoroutineScheduler] Coroutine {handle} failed with exception: {ex}"); + _logger.Error($"[CoroutineScheduler] Coroutine {handle} failed with exception: {ex}"); // 完成协程 Complete(slotIndex); diff --git a/GFramework.Core/resource/AutoReleaseStrategy.cs b/GFramework.Core/resource/AutoReleaseStrategy.cs new file mode 100644 index 0000000..6325e85 --- /dev/null +++ b/GFramework.Core/resource/AutoReleaseStrategy.cs @@ -0,0 +1,19 @@ +using GFramework.Core.Abstractions.resource; + +namespace GFramework.Core.resource; + +/// +/// 自动释放策略 +/// 当资源引用计数降为 0 时自动卸载资源 +/// +public class AutoReleaseStrategy : IResourceReleaseStrategy +{ + /// + /// 判断是否应该释放资源 + /// 当引用计数降为 0 时返回 true + /// + public bool ShouldRelease(string path, int refCount) + { + return refCount <= 0; + } +} \ No newline at end of file diff --git a/GFramework.Core/resource/ManualReleaseStrategy.cs b/GFramework.Core/resource/ManualReleaseStrategy.cs new file mode 100644 index 0000000..dd50757 --- /dev/null +++ b/GFramework.Core/resource/ManualReleaseStrategy.cs @@ -0,0 +1,19 @@ +using GFramework.Core.Abstractions.resource; + +namespace GFramework.Core.resource; + +/// +/// 手动释放策略 +/// 引用计数降为 0 时不自动卸载资源,需要手动调用 Unload +/// +public class ManualReleaseStrategy : IResourceReleaseStrategy +{ + /// + /// 判断是否应该释放资源(始终返回 false) + /// + public bool ShouldRelease(string path, int refCount) + { + // 手动释放策略:永远不自动释放,由用户显式调用 Unload + return false; + } +} \ No newline at end of file diff --git a/GFramework.Core/resource/ResourceCache.cs b/GFramework.Core/resource/ResourceCache.cs new file mode 100644 index 0000000..745f4fc --- /dev/null +++ b/GFramework.Core/resource/ResourceCache.cs @@ -0,0 +1,211 @@ +using System.Collections.Concurrent; + +namespace GFramework.Core.resource; + +/// +/// 资源缓存条目 +/// +internal sealed class ResourceCacheEntry(object resource, Type resourceType) +{ + public object Resource { get; } = resource ?? throw new ArgumentNullException(nameof(resource)); + public Type ResourceType { get; } = resourceType ?? throw new ArgumentNullException(nameof(resourceType)); + public int ReferenceCount { get; set; } + public DateTime LastAccessTime { get; set; } = DateTime.UtcNow; +} + +/// +/// 资源缓存系统,管理已加载资源的缓存和引用计数 +/// 线程安全:所有公共方法都是线程安全的 +/// +internal sealed class ResourceCache +{ + /// + /// Path 参数验证错误消息常量 + /// + private const string PathCannotBeNullOrEmptyMessage = "Path cannot be null or whitespace."; + + private readonly ConcurrentDictionary _cache = new(); + private readonly object _lock = new(); + + /// + /// 获取已缓存资源的数量 + /// + public int Count => _cache.Count; + + /// + /// 添加资源到缓存 + /// + /// 资源类型 + /// 资源路径 + /// 资源实例 + /// 如果成功添加返回 true,如果已存在返回 false + public bool Add(string path, T resource) where T : class + { + if (string.IsNullOrWhiteSpace(path)) + throw new ArgumentException(PathCannotBeNullOrEmptyMessage, nameof(path)); + + ArgumentNullException.ThrowIfNull(resource); + + var entry = new ResourceCacheEntry(resource, typeof(T)); + return _cache.TryAdd(path, entry); + } + + /// + /// 从缓存中获取资源 + /// + /// 资源类型 + /// 资源路径 + /// 资源实例,如果不存在或类型不匹配返回 null + public T? Get(string path) where T : class + { + if (string.IsNullOrWhiteSpace(path)) + throw new ArgumentException(PathCannotBeNullOrEmptyMessage, nameof(path)); + + if (_cache.TryGetValue(path, out var entry)) + { + lock (_lock) + { + entry.LastAccessTime = DateTime.UtcNow; + } + + if (entry.Resource is T typedResource) + { + return typedResource; + } + } + + return null; + } + + /// + /// 检查资源是否在缓存中 + /// + /// 资源路径 + /// 如果存在返回 true,否则返回 false + public bool Contains(string path) + { + if (string.IsNullOrWhiteSpace(path)) + throw new ArgumentException(PathCannotBeNullOrEmptyMessage, nameof(path)); + + return _cache.ContainsKey(path); + } + + /// + /// 从缓存中移除资源 + /// + /// 资源路径 + /// 如果成功移除返回 true,否则返回 false + public bool Remove(string path) + { + if (string.IsNullOrWhiteSpace(path)) + throw new ArgumentException(PathCannotBeNullOrEmptyMessage, nameof(path)); + + return _cache.TryRemove(path, out _); + } + + /// + /// 清空所有缓存 + /// + public void Clear() + { + _cache.Clear(); + } + + /// + /// 增加资源的引用计数 + /// + /// 资源路径 + /// 如果成功增加返回 true,如果资源不存在返回 false + public bool AddReference(string path) + { + if (string.IsNullOrWhiteSpace(path)) + throw new ArgumentException(PathCannotBeNullOrEmptyMessage, nameof(path)); + + if (_cache.TryGetValue(path, out var entry)) + { + lock (_lock) + { + entry.ReferenceCount++; + entry.LastAccessTime = DateTime.UtcNow; + } + + return true; + } + + return false; + } + + /// + /// 减少资源的引用计数 + /// + /// 资源路径 + /// 减少后的引用计数,如果资源不存在返回 -1 + public int RemoveReference(string path) + { + if (string.IsNullOrWhiteSpace(path)) + throw new ArgumentException(PathCannotBeNullOrEmptyMessage, nameof(path)); + + if (_cache.TryGetValue(path, out var entry)) + { + lock (_lock) + { + entry.ReferenceCount--; + return entry.ReferenceCount; + } + } + + return -1; + } + + /// + /// 获取资源的引用计数 + /// + /// 资源路径 + /// 引用计数,如果资源不存在返回 -1 + public int GetReferenceCount(string path) + { + if (string.IsNullOrWhiteSpace(path)) + throw new ArgumentException(PathCannotBeNullOrEmptyMessage, nameof(path)); + + if (_cache.TryGetValue(path, out var entry)) + { + lock (_lock) + { + return entry.ReferenceCount; + } + } + + return -1; + } + + /// + /// 获取所有已缓存资源的路径 + /// + /// 资源路径集合 + public IEnumerable GetAllPaths() + { + return _cache.Keys.ToList(); + } + + /// + /// 获取所有引用计数为 0 的资源路径 + /// + /// 资源路径集合 + public IEnumerable GetUnreferencedPaths() + { + var unreferencedPaths = new List(); + + foreach (var kvp in _cache) + { + lock (_lock) + { + if (kvp.Value.ReferenceCount <= 0) + { + unreferencedPaths.Add(kvp.Key); + } + } + } + + return unreferencedPaths; + } +} \ No newline at end of file diff --git a/GFramework.Core/resource/ResourceHandle.cs b/GFramework.Core/resource/ResourceHandle.cs new file mode 100644 index 0000000..208b1cd --- /dev/null +++ b/GFramework.Core/resource/ResourceHandle.cs @@ -0,0 +1,144 @@ +using GFramework.Core.Abstractions.logging; +using GFramework.Core.Abstractions.resource; +using GFramework.Core.logging; + +namespace GFramework.Core.resource; + +/// +/// 资源句柄实现,管理资源的生命周期和引用计数 +/// 线程安全:所有公共方法都是线程安全的 +/// +/// 资源类型 +internal sealed class ResourceHandle : IResourceHandle where T : class +{ + private readonly object _lock = new(); + private readonly ILogger _logger = LoggerFactoryResolver.Provider.CreateLogger(nameof(ResourceHandle)); + private readonly Action _onDispose; + private bool _disposed; + private int _referenceCount; + + /// + /// 创建资源句柄 + /// + /// 资源实例 + /// 资源路径 + /// 释放时的回调 + public ResourceHandle(T resource, string path, Action onDispose) + { + Resource = resource ?? throw new ArgumentNullException(nameof(resource)); + Path = path ?? throw new ArgumentNullException(nameof(path)); + _onDispose = onDispose ?? throw new ArgumentNullException(nameof(onDispose)); + _referenceCount = 1; + } + + /// + /// 获取资源实例 + /// + public T? Resource { get; private set; } + + /// + /// 获取资源路径 + /// + public string Path { get; } + + /// + /// 获取资源是否有效 + /// + public bool IsValid + { + get + { + lock (_lock) + { + return !_disposed && Resource != null; + } + } + } + + /// + /// 获取当前引用计数 + /// + public int ReferenceCount + { + get + { + lock (_lock) + { + return _referenceCount; + } + } + } + + /// + /// 增加引用计数 + /// + public void AddReference() + { + lock (_lock) + { + if (_disposed) + throw new ObjectDisposedException(nameof(ResourceHandle)); + + _referenceCount++; + } + } + + /// + /// 减少引用计数 + /// + public void RemoveReference() + { + lock (_lock) + { + if (_disposed) + return; + + _referenceCount--; + + if (_referenceCount <= 0) + { + DisposeInternal(); + } + } + } + + /// + /// 释放资源句柄 + /// + public void Dispose() + { + lock (_lock) + { + if (_disposed) + return; + + _referenceCount--; + + if (_referenceCount <= 0) + { + DisposeInternal(); + } + } + } + + /// + /// 内部释放方法(必须在锁内调用) + /// + private void DisposeInternal() + { + if (_disposed) + return; + + _disposed = true; + Resource = null; + + try + { + _onDispose(Path); + } + catch (Exception ex) + { + _logger.Error($"[ResourceHandle] Error disposing resource '{Path}': {ex.Message}"); + } + } +} \ No newline at end of file diff --git a/GFramework.Core/resource/ResourceManager.cs b/GFramework.Core/resource/ResourceManager.cs new file mode 100644 index 0000000..3801a14 --- /dev/null +++ b/GFramework.Core/resource/ResourceManager.cs @@ -0,0 +1,334 @@ +using System.Collections.Concurrent; +using GFramework.Core.Abstractions.logging; +using GFramework.Core.Abstractions.resource; +using GFramework.Core.logging; + +namespace GFramework.Core.resource; + +/// +/// 资源管理器实现,提供资源加载、缓存和卸载功能 +/// 线程安全:所有公共方法都是线程安全的 +/// +public class ResourceManager : IResourceManager +{ + /// + /// Path 参数验证错误消息常量 + /// + private const string PathCannotBeNullOrEmptyMessage = "Path cannot be null or whitespace."; + + private readonly ResourceCache _cache = new(); + private readonly ConcurrentDictionary _loaders = new(); + private readonly object _loadLock = new(); + private readonly ILogger _logger = LoggerFactoryResolver.Provider.CreateLogger(nameof(ResourceManager)); + private IResourceReleaseStrategy _releaseStrategy; + + /// + /// 创建资源管理器 + /// 默认使用手动释放策略 + /// + public ResourceManager() + { + _releaseStrategy = new ManualReleaseStrategy(); + } + + /// + /// 获取已加载资源的数量 + /// + public int LoadedResourceCount => _cache.Count; + + /// + /// 同步加载资源 + /// + public T? Load(string path) where T : class + { + if (string.IsNullOrWhiteSpace(path)) + throw new ArgumentException(PathCannotBeNullOrEmptyMessage, nameof(path)); + + // 检查缓存 + var cached = _cache.Get(path); + if (cached != null) + { + _cache.AddReference(path); + return cached; + } + + // 加载资源 + lock (_loadLock) + { + // 双重检查 + cached = _cache.Get(path); + if (cached != null) + { + _cache.AddReference(path); + return cached; + } + + var loader = GetLoader(); + if (loader == null) + { + throw new InvalidOperationException($"No loader registered for type {typeof(T).Name}"); + } + + try + { + var resource = loader.Load(path); + _cache.Add(path, resource); + _cache.AddReference(path); + return resource; + } + catch (Exception ex) + { + _logger.Error($"[ResourceManager] Failed to load resource '{path}': {ex.Message}"); + return null; + } + } + } + + /// + /// 异步加载资源 + /// + public async Task LoadAsync(string path) where T : class + { + if (string.IsNullOrWhiteSpace(path)) + throw new ArgumentException(PathCannotBeNullOrEmptyMessage, nameof(path)); + + // 检查缓存 + var cached = _cache.Get(path); + if (cached != null) + { + _cache.AddReference(path); + return cached; + } + + var loader = GetLoader(); + if (loader == null) + { + throw new InvalidOperationException($"No loader registered for type {typeof(T).Name}"); + } + + try + { + var resource = await loader.LoadAsync(path); + lock (_loadLock) + { + // 双重检查 + cached = _cache.Get(path); + if (cached != null) + { + // 已经被其他线程加载了,卸载当前加载的资源 + loader.Unload(resource); + _cache.AddReference(path); + return cached; + } + + _cache.Add(path, resource); + _cache.AddReference(path); + } + + return resource; + } + catch (Exception ex) + { + _logger.Error($"[ResourceManager] Failed to load resource '{path}': {ex.Message}"); + return null; + } + } + + /// + /// 获取资源句柄 + /// + public IResourceHandle? GetHandle(string path) where T : class + { + if (string.IsNullOrWhiteSpace(path)) + throw new ArgumentException(PathCannotBeNullOrEmptyMessage, nameof(path)); + + var resource = _cache.Get(path); + if (resource == null) + return null; + + _cache.AddReference(path); + return new ResourceHandle(resource, path, HandleDispose); + } + + /// + /// 卸载指定路径的资源 + /// + public bool Unload(string path) + { + if (string.IsNullOrWhiteSpace(path)) + throw new ArgumentException(PathCannotBeNullOrEmptyMessage, nameof(path)); + + lock (_loadLock) + { + var resource = _cache.Get(path); + if (resource == null) + return false; + + // 检查引用计数 + var refCount = _cache.GetReferenceCount(path); + if (refCount > 0) + { + _logger.Error( + $"[ResourceManager] Cannot unload resource '{path}' with {refCount} active references"); + return false; + } + + // 卸载资源 + UnloadResource(resource); + + // 从缓存中移除 + return _cache.Remove(path); + } + } + + /// + /// 卸载所有资源 + /// + public void UnloadAll() + { + lock (_loadLock) + { + var paths = _cache.GetAllPaths().ToList(); + + foreach (var path in paths) + { + var resource = _cache.Get(path); + if (resource != null) + { + UnloadResource(resource); + } + } + + _cache.Clear(); + } + } + + /// + /// 检查资源是否已加载 + /// + public bool IsLoaded(string path) + { + if (string.IsNullOrWhiteSpace(path)) + throw new ArgumentException(PathCannotBeNullOrEmptyMessage, nameof(path)); + + return _cache.Contains(path); + } + + /// + /// 注册资源加载器 + /// + public void RegisterLoader(IResourceLoader loader) where T : class + { + if (loader == null) + throw new ArgumentNullException(nameof(loader)); + + _loaders[typeof(T)] = loader; + } + + /// + /// 取消注册资源加载器 + /// + public void UnregisterLoader() where T : class + { + _loaders.TryRemove(typeof(T), out _); + } + + /// + /// 预加载资源 + /// + public async Task PreloadAsync(string path) where T : class + { + await LoadAsync(path); + } + + /// + /// 获取所有已加载资源的路径 + /// + public IEnumerable GetLoadedResourcePaths() + { + return _cache.GetAllPaths(); + } + + /// + /// 设置资源释放策略 + /// + /// 资源释放策略 + public void SetReleaseStrategy(IResourceReleaseStrategy strategy) + { + _releaseStrategy = strategy ?? throw new ArgumentNullException(nameof(strategy)); + } + + /// + /// 获取指定类型的资源加载器 + /// + private IResourceLoader? GetLoader() where T : class + { + if (_loaders.TryGetValue(typeof(T), out var loader)) + { + return loader as IResourceLoader; + } + + return null; + } + + /// + /// 卸载资源实例 + /// + private void UnloadResource(object resource) + { + var resourceType = resource.GetType(); + + if (_loaders.TryGetValue(resourceType, out var loaderObj)) + { + try + { + var unloadMethod = loaderObj.GetType().GetMethod("Unload"); + unloadMethod?.Invoke(loaderObj, new[] { resource }); + } + catch (Exception ex) + { + _logger.Error($"[ResourceManager] Error unloading resource: {ex.Message}"); + } + } + + // 如果资源实现了 IDisposable,调用 Dispose + if (resource is IDisposable disposable) + { + try + { + disposable.Dispose(); + } + catch (Exception ex) + { + _logger.Error($"[ResourceManager] Error disposing resource: {ex.Message}"); + } + } + } + + /// + /// 句柄释放时的回调 + /// + private void HandleDispose(string path) + { + var refCount = _cache.RemoveReference(path); + + // 使用策略模式判断是否应该释放资源 + if (_releaseStrategy.ShouldRelease(path, refCount)) + { + lock (_loadLock) + { + // 双重检查引用计数,避免竞态条件 + var currentRefCount = _cache.GetReferenceCount(path); + if (currentRefCount <= 0) + { + var resource = _cache.Get(path); + if (resource != null) + { + UnloadResource(resource); + _cache.Remove(path); + } + } + } + } + } +} \ No newline at end of file