// ============================================================================
// 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
{
///
/// 全局资源缓存管理器。
/// 提供同步缓存获取、异步加载+去重、LRU 淘汰等能力。
///
public class ResourceCacheManager : Singleton, IResourceCache
{
///
/// 缓存条目
///
private class CachedResource
{
public UnityEngine.Object Asset;
public AssetHandle Handle;
public bool IsPermanent;
public float LastAccessTime;
}
/// 已缓存的资源
private readonly Dictionary _syncCache = new Dictionary();
/// 正在加载中的任务(用于去重)
private readonly Dictionary> _loadingTasks = new Dictionary>();
/// LRU 淘汰阈值(非常驻资源数量超过此值时触发淘汰)
private const int LRU_THRESHOLD = 200;
/// 淘汰后保留的非常驻资源数量
private const int LRU_KEEP_COUNT = 150;
///
public int CachedCount => _syncCache.Count;
// ====================================================================
// 同步获取
// ====================================================================
///
public T GetCached(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;
}
///
public bool IsCached(string location)
{
return !string.IsNullOrEmpty(location) && _syncCache.ContainsKey(location);
}
// ====================================================================
// 异步获取(缓存穿透 + 去重)
// ====================================================================
///
public async UniTask GetOrLoadAsync(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);
}
}
// ====================================================================
// 批量预加载
// ====================================================================
///
public async UniTask PreloadAsync(string[] locations, bool permanent = false, IProgress progress = null)
{
if (locations == null || locations.Length == 0)
{
progress?.Report(1f);
return;
}
int completed = 0;
int total = locations.Length;
// 过滤已缓存的
var toLoad = new List();
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();
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();
}
// ====================================================================
// 释放
// ====================================================================
///
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);
}
}
///
public void ReleaseAll()
{
var toRemove = new List();
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);
}
}
///
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 LoadAndCacheAsync(string location, bool permanent)
{
try
{
var asset = await YooAssetService.Instance.LoadAssetAsync(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;
}
}
///
/// LRU 淘汰:非常驻资源超过阈值时,按访问时间淘汰最旧的资源。
///
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);
}
}
}
}