// ============================================================================
|
// ResourceCacheManager.cs — 全局资源缓存管理器
|
// Feature: 001-async-resource-loading
|
// ============================================================================
|
|
using System;
|
using System.Collections.Generic;
|
using System.Linq;
|
using UnityEngine;
|
using Cysharp.Threading.Tasks;
|
using YooAsset;
|
|
namespace ProjSG.Resource
|
{
|
/// <summary>
|
/// 全局资源缓存管理器。
|
/// 提供同步缓存获取、异步加载+去重、LRU 淘汰等能力。
|
/// </summary>
|
public class ResourceCacheManager : Singleton<ResourceCacheManager>, IResourceCache
|
{
|
/// <summary>
|
/// 缓存条目
|
/// </summary>
|
private class CachedResource
|
{
|
public UnityEngine.Object Asset;
|
public AssetHandle Handle;
|
public bool IsPermanent;
|
public float LastAccessTime;
|
}
|
|
/// <summary>已缓存的资源</summary>
|
private readonly Dictionary<string, CachedResource> _syncCache = new Dictionary<string, CachedResource>();
|
|
/// <summary>正在加载中的任务(用于去重)</summary>
|
private readonly Dictionary<string, UniTask<UnityEngine.Object>> _loadingTasks = new Dictionary<string, UniTask<UnityEngine.Object>>();
|
|
/// <summary>LRU 淘汰阈值(非常驻资源数量超过此值时触发淘汰)</summary>
|
private const int LRU_THRESHOLD = 200;
|
|
/// <summary>淘汰后保留的非常驻资源数量</summary>
|
private const int LRU_KEEP_COUNT = 150;
|
|
/// <inheritdoc/>
|
public int CachedCount => _syncCache.Count;
|
|
// ====================================================================
|
// 同步获取
|
// ====================================================================
|
|
/// <inheritdoc/>
|
public T GetCached<T>(string location) where T : UnityEngine.Object
|
{
|
if (string.IsNullOrEmpty(location)) return null;
|
|
if (_syncCache.TryGetValue(location, out var cached))
|
{
|
cached.LastAccessTime = Time.unscaledTime;
|
return cached.Asset as T;
|
}
|
|
return null;
|
}
|
|
/// <inheritdoc/>
|
public bool IsCached(string location)
|
{
|
return !string.IsNullOrEmpty(location) && _syncCache.ContainsKey(location);
|
}
|
|
// ====================================================================
|
// 异步获取(缓存穿透 + 去重)
|
// ====================================================================
|
|
/// <inheritdoc/>
|
public async UniTask<T> GetOrLoadAsync<T>(string location) where T : UnityEngine.Object
|
{
|
if (string.IsNullOrEmpty(location))
|
{
|
Debug.LogError("ResourceCacheManager.GetOrLoadAsync: location is null or empty");
|
return null;
|
}
|
|
// 1. 缓存命中
|
if (_syncCache.TryGetValue(location, out var cached))
|
{
|
cached.LastAccessTime = Time.unscaledTime;
|
return cached.Asset as T;
|
}
|
|
// 2. 正在加载中 → 等待已有任务(去重)
|
if (_loadingTasks.TryGetValue(location, out var loadingTask))
|
{
|
var result = await loadingTask;
|
return result as T;
|
}
|
|
// 3. 发起新加载
|
var task = LoadAndCacheAsync(location, false);
|
_loadingTasks[location] = task;
|
|
try
|
{
|
var asset = await task;
|
return asset as T;
|
}
|
finally
|
{
|
_loadingTasks.Remove(location);
|
}
|
}
|
|
// ====================================================================
|
// 批量预加载
|
// ====================================================================
|
|
/// <inheritdoc/>
|
public async UniTask PreloadAsync(string[] locations, bool permanent = false, IProgress<float> progress = null)
|
{
|
if (locations == null || locations.Length == 0)
|
{
|
progress?.Report(1f);
|
return;
|
}
|
|
int completed = 0;
|
int total = locations.Length;
|
|
// 过滤已缓存的
|
var toLoad = new List<string>();
|
foreach (var loc in locations)
|
{
|
if (_syncCache.ContainsKey(loc))
|
{
|
// 已缓存,更新常驻标记
|
if (permanent)
|
{
|
_syncCache[loc].IsPermanent = true;
|
}
|
completed++;
|
}
|
else
|
{
|
toLoad.Add(loc);
|
}
|
}
|
|
progress?.Report((float)completed / total);
|
|
// 并行加载(限制并发数)
|
const int maxConcurrency = 8;
|
for (int i = 0; i < toLoad.Count; i += maxConcurrency)
|
{
|
var batch = new List<UniTask>();
|
int batchEnd = Mathf.Min(i + maxConcurrency, toLoad.Count);
|
|
for (int j = i; j < batchEnd; j++)
|
{
|
var loc = toLoad[j];
|
batch.Add(LoadAndCacheAsync(loc, permanent).ContinueWith(_ =>
|
{
|
completed++;
|
progress?.Report((float)completed / total);
|
}));
|
}
|
|
await UniTask.WhenAll(batch);
|
}
|
|
progress?.Report(1f);
|
TryLRUEviction();
|
}
|
|
// ====================================================================
|
// 释放
|
// ====================================================================
|
|
/// <inheritdoc/>
|
public void Release(string location, bool forceRelease = false)
|
{
|
if (string.IsNullOrEmpty(location)) return;
|
|
if (_syncCache.TryGetValue(location, out var cached))
|
{
|
if (cached.IsPermanent && !forceRelease) return;
|
|
if (cached.Handle != null && cached.Handle.IsValid)
|
{
|
cached.Handle.Release();
|
}
|
|
_syncCache.Remove(location);
|
}
|
}
|
|
/// <inheritdoc/>
|
public void ReleaseAll()
|
{
|
var toRemove = new List<string>();
|
|
foreach (var kvp in _syncCache)
|
{
|
if (!kvp.Value.IsPermanent)
|
{
|
if (kvp.Value.Handle != null && kvp.Value.Handle.IsValid)
|
{
|
kvp.Value.Handle.Release();
|
}
|
toRemove.Add(kvp.Key);
|
}
|
}
|
|
foreach (var key in toRemove)
|
{
|
_syncCache.Remove(key);
|
}
|
}
|
|
/// <inheritdoc/>
|
public void ForceReleaseAll()
|
{
|
foreach (var kvp in _syncCache)
|
{
|
if (kvp.Value.Handle != null && kvp.Value.Handle.IsValid)
|
{
|
kvp.Value.Handle.Release();
|
}
|
}
|
|
_syncCache.Clear();
|
_loadingTasks.Clear();
|
}
|
|
// ====================================================================
|
// 内部方法
|
// ====================================================================
|
|
private async UniTask<UnityEngine.Object> LoadAndCacheAsync(string location, bool permanent)
|
{
|
try
|
{
|
var asset = await YooAssetService.Instance.LoadAssetAsync<UnityEngine.Object>(location);
|
|
if (asset != null && !_syncCache.ContainsKey(location))
|
{
|
_syncCache[location] = new CachedResource
|
{
|
Asset = asset,
|
Handle = null, // Handle 由 YooAssetService 管理
|
IsPermanent = permanent,
|
LastAccessTime = Time.unscaledTime,
|
};
|
}
|
|
return asset;
|
}
|
catch (Exception e)
|
{
|
Debug.LogError($"ResourceCacheManager.LoadAndCacheAsync failed for '{location}': {e.Message}");
|
return null;
|
}
|
}
|
|
/// <summary>
|
/// LRU 淘汰:非常驻资源超过阈值时,按访问时间淘汰最旧的资源。
|
/// </summary>
|
private void TryLRUEviction()
|
{
|
var nonPermanent = _syncCache
|
.Where(kvp => !kvp.Value.IsPermanent)
|
.ToList();
|
|
if (nonPermanent.Count <= LRU_THRESHOLD) return;
|
|
// 按访问时间排序,移除最旧的
|
var sorted = nonPermanent
|
.OrderBy(kvp => kvp.Value.LastAccessTime)
|
.ToList();
|
|
int toRemove = sorted.Count - LRU_KEEP_COUNT;
|
for (int i = 0; i < toRemove; i++)
|
{
|
var kvp = sorted[i];
|
if (kvp.Value.Handle != null && kvp.Value.Handle.IsValid)
|
{
|
kvp.Value.Handle.Release();
|
}
|
_syncCache.Remove(kvp.Key);
|
}
|
}
|
}
|
}
|