yyl
2026-02-11 3f2cd27c5dfb3b450245bf1a37fc1b3414031c7c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
// ============================================================================
// 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);
            }
        }
    }
}