// ============================================================================ // 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); } } } }