From 7919c93f448f52b73959228768b9367d80ed5a2f Mon Sep 17 00:00:00 2001
From: GeWuYou <95328647+GeWuYou@users.noreply.github.com>
Date: Wed, 4 Mar 2026 13:30:42 +0800
Subject: [PATCH] =?UTF-8?q?feat(resource):=20=E6=B7=BB=E5=8A=A0=E8=B5=84?=
=?UTF-8?q?=E6=BA=90=E7=AE=A1=E7=90=86=E7=B3=BB=E7=BB=9F=E5=92=8C=E6=97=A5?=
=?UTF-8?q?=E5=BF=97=E9=9B=86=E6=88=90?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 实现了完整的资源管理系统,包括资源加载、缓存和卸载功能
- 添加了 IResourceHandle、IResourceLoader、IResourceManager 和 IResourceReleaseStrategy 接口定义
- 实现了 AutoReleaseStrategy 和 ManualReleaseStrategy 两种资源释放策略
- 创建了 ResourceCache 缓存系统和 ResourceHandle 资源句柄管理
- 在 ConfigurationManager 和 CoroutineScheduler 中集成了 ILogger 日志功能
- 添加了全面的 ResourceManagerTests 单元测试覆盖各种使用场景
---
.../resource/IResourceHandle.cs | 38 ++
.../resource/IResourceLoader.cs | 35 ++
.../resource/IResourceManager.cs | 89 ++++
.../resource/IResourceReleaseStrategy.cs | 16 +
.../resource/ResourceManagerTests.cs | 407 ++++++++++++++++++
.../configuration/ConfigurationManager.cs | 6 +-
.../coroutine/CoroutineScheduler.cs | 7 +-
.../resource/AutoReleaseStrategy.cs | 19 +
.../resource/ManualReleaseStrategy.cs | 19 +
GFramework.Core/resource/ResourceCache.cs | 211 +++++++++
GFramework.Core/resource/ResourceHandle.cs | 144 +++++++
GFramework.Core/resource/ResourceManager.cs | 334 ++++++++++++++
12 files changed, 1322 insertions(+), 3 deletions(-)
create mode 100644 GFramework.Core.Abstractions/resource/IResourceHandle.cs
create mode 100644 GFramework.Core.Abstractions/resource/IResourceLoader.cs
create mode 100644 GFramework.Core.Abstractions/resource/IResourceManager.cs
create mode 100644 GFramework.Core.Abstractions/resource/IResourceReleaseStrategy.cs
create mode 100644 GFramework.Core.Tests/resource/ResourceManagerTests.cs
create mode 100644 GFramework.Core/resource/AutoReleaseStrategy.cs
create mode 100644 GFramework.Core/resource/ManualReleaseStrategy.cs
create mode 100644 GFramework.Core/resource/ResourceCache.cs
create mode 100644 GFramework.Core/resource/ResourceHandle.cs
create mode 100644 GFramework.Core/resource/ResourceManager.cs
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