using System; using System.Collections.Generic; using System.Linq; using Framework; using PhxhSDK; using UnityEngine; /// /// 音频停止池 - 使用 LRU 算法管理停止的 AudioPlayer /// - 按 key 索引,提高复用率 /// - 分类型限制,防止内存泄漏 /// - LRU 淘汰,自动清理长期不用的 AudioPlayer /// public class AudioStoppedPool { #region Fields // 按 key 索引(用于快速查找匹配的 AudioPlayer) // key 格式: "音频类型_配置key",如 "Music_bgm_01" private Dictionary> _poolByKey; // 按 ID 索引(用于快速查找和移除) private Dictionary _poolById; // 当前各类型的数量 private Dictionary _currentCount; // 各类型的上限 private Dictionary _maxLimit; // 记录每个 AudioPlayer 进入停止池的时间戳(毫秒) private Dictionary _stoppedTimeById; // 各类型的超时时间(秒)- 长时间未使用则释放 private Dictionary _timeoutByType; // 超时检查定时器(降低检查频率) private float _checkTimer; // 临时列表(复用,避免频繁分配) private readonly List _tempKeysToRemove; #endregion #region Init public AudioStoppedPool() { _poolByKey = new Dictionary>(); _poolById = new Dictionary(); _currentCount = new Dictionary { { EAudioType.Music, 0 }, { EAudioType.Sound, 0 }, { EAudioType.Speak, 0 } }; _maxLimit = new Dictionary { { EAudioType.Music, 5 }, // Music 停止池上限 5 { EAudioType.Sound, 50 }, // Sound 停止池上限 50 { EAudioType.Speak, 3 } // Speak 停止池上限 3 }; _stoppedTimeById = new Dictionary(); _timeoutByType = new Dictionary { { EAudioType.Music, AudioConstant.MusicStoppedTimeout }, // 60 秒 { EAudioType.Sound, AudioConstant.SoundStoppedTimeout }, // 30 秒 { EAudioType.Speak, AudioConstant.SpeakStoppedTimeout } // 20 秒 }; _checkTimer = 0f; _tempKeysToRemove = new List(); } #endregion #region Public Methods /// /// 设置停止池上限 /// public void SetLimit(EAudioType type, int limit) { _maxLimit[type] = limit; } /// /// 设置停止池超时时间(秒) /// 长时间未使用的音频会在超时后自动释放 /// /// 音频类型 /// 超时时间(秒) public void SetTimeout(EAudioType type, float timeout) { if (timeout < 0) { Debug.LogWarning($"[AudioStoppedPool] Invalid timeout {timeout} for {type}, using 0"); timeout = 0; } _timeoutByType[type] = timeout; } /// /// 添加 AudioPlayer 到停止池(带 LRU 淘汰) /// 如果超过该类型的上限,自动淘汰最久未使用的 AudioPlayer /// /// 音频类型 /// 音频配置 key /// 要添加的 AudioPlayer /// 被淘汰的 AudioPlayer 列表(需要调用方 Dispose) public List AddStopped(EAudioType type, string key, AudioPlayer audio) { if (audio == null || string.IsNullOrEmpty(key)) return new List(); var poolKey = MakePoolKey(type, key); // 禁用 GameObject(对象池优化) audio.SetActive(false); // 1. 添加到 key 索引 if (!_poolByKey.TryGetValue(poolKey, out var list)) { list = new LinkedList(); _poolByKey[poolKey] = list; } list.AddFirst(audio); // 添加到链表头部(最近使用) // 2. 添加到 ID 索引 _poolById[audio.AudioId] = audio; _currentCount[type]++; // 记录进入停止池的时间戳(毫秒) _stoppedTimeById[audio.AudioId] = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); // 3. 检查是否超过上限,执行 LRU 淘汰 var toDestroy = new List(); if (_currentCount[type] > _maxLimit[type]) { int excessCount = _currentCount[type] - _maxLimit[type]; toDestroy = EvictOldest(type, excessCount); } return toDestroy; } /// /// 尝试从停止池获取缓存的 AudioPlayer(精确匹配) /// 只返回完全匹配的 audio(同类型 + 同 key),找不到返回 null /// 设计原则:停止池只用于精确复用,不跨 key 复用 /// /// 音频类型 /// 音频配置 key /// 找到返回缓存的 AudioPlayer,否则返回 null public AudioPlayer TryGetCached(EAudioType type, string key) { if (string.IsNullOrEmpty(key)) return null; var poolKey = MakePoolKey(type, key); // 只返回精确匹配的 audio(同类型 + 同 key) if (_poolByKey.TryGetValue(poolKey, out var list) && list.Count > 0) { var audio = list.First.Value; RemoveFromPool(audio); return audio; } // 找不到精确匹配,返回 null // 让 AudioManager 决定是创建新的(播放池未满)还是复用最旧的(播放池已满) return null; } /// /// 通过 ID 查询停止池中的 AudioPlayer /// /// AudioPlayer ID /// 找到返回 AudioPlayer,否则返回 null public AudioPlayer GetById(int id) { return _poolById.TryGetValue(id, out var audio) ? audio : null; } /// /// 通过 ID 从停止池移除 AudioPlayer /// /// AudioPlayer ID /// 找到并移除返回 AudioPlayer,否则返回 null public AudioPlayer RemoveById(int id) { if (!_poolById.TryGetValue(id, out var audio)) return null; RemoveFromPool(audio); return audio; } /// /// 获取当前各类型的统计信息 /// /// (Music数量, Sound数量, Speak数量) public (int music, int sound, int speak) GetStats() { return (_currentCount[EAudioType.Music], _currentCount[EAudioType.Sound], _currentCount[EAudioType.Speak]); } /// /// 更新停止池(检查并释放超时的音频) /// 降低检查频率,避免每帧都检查 /// /// 帧时间(秒) /// 被释放的 AudioPlayer 列表(需要调用方 Dispose),如果没有则返回 null public List Update(float dt) { // 降低检查频率(每秒检查一次) _checkTimer += dt; if (_checkTimer < AudioConstant.StoppedPoolCheckInterval) return null; // 返回 null,避免分配空列表 _checkTimer = 0f; var toDispose = new List(); var currentTime = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); // 复用临时列表 _tempKeysToRemove.Clear(); // 直接遍历字典,收集要删除的 key(避免 ToList 的内存分配) foreach (var kvp in _stoppedTimeById) { var audioId = kvp.Key; var stoppedTime = kvp.Value; // 获取音频(可能已被移除) if (!_poolById.TryGetValue(audioId, out var audio)) { // 收集无效的时间戳,稍后批量删除 _tempKeysToRemove.Add(audioId); continue; } var type = audio.AudioType; var timeoutMs = (long)(_timeoutByType[type] * 1000); // 检查是否超时(超时时间 > 0 才启用) if (timeoutMs > 0 && currentTime - stoppedTime >= timeoutMs) { _tempKeysToRemove.Add(audioId); toDispose.Add(audio); } } // 批量删除无效的时间戳 foreach (var key in _tempKeysToRemove) { _stoppedTimeById.Remove(key); } if (toDispose.Count > 0) { int a = 1; foreach (var toRemove in toDispose) { RemoveFromPool(toRemove); } } // 返回结果(空列表返回 null,避免分配) return toDispose.Count > 0 ? toDispose : null; } /// /// 清空停止池(不释放 AudioPlayer 资源) /// public void Clear() { _poolByKey.Clear(); _poolById.Clear(); _stoppedTimeById.Clear(); ResetCounts(); } /// /// 释放所有 AudioPlayer 资源并清空停止池 /// 适用于场景卸载、关卡退出等需要彻底清理的场景 /// /// 释放的 AudioPlayer 数量 public int DisposeAll() { int count = 0; // 释放所有 AudioPlayer 资源 foreach (var list in _poolByKey.Values) { foreach (var audio in list) { audio.Dispose(); count++; } } // 清空池子 _poolByKey.Clear(); _poolById.Clear(); _stoppedTimeById.Clear(); ResetCounts(); return count; } #endregion #region Private Methods /// /// 从停止池移除 AudioPlayer /// private void RemoveFromPool(AudioPlayer audio) { if (audio == null) return; // 从 ID 索引移除 _poolById.Remove(audio.AudioId); // 从 key 索引移除 var poolKey = MakePoolKey(audio.AudioType, audio.CurrentPlayingKey); if (_poolByKey.TryGetValue(poolKey, out var list)) { list.Remove(audio); if (list.Count == 0) { _poolByKey.Remove(poolKey); } } // 更新计数 _currentCount[audio.AudioType]--; // 清除时间戳 _stoppedTimeById.Remove(audio.AudioId); } /// /// LRU 淘汰:移除指定类型中最久未使用的 N 个 AudioPlayer /// 从各个 key 的链表尾部收集(尾部 = 最久未使用) /// /// 音频类型 /// 要淘汰的数量 /// 被淘汰的 AudioPlayer 列表 private List EvictOldest(EAudioType type, int count) { var evicted = new List(count); var typePrefix = $"{type}_"; // 从所有匹配类型的 key 中收集最久未使用的 AudioPlayer foreach (var kv in _poolByKey) { if (!kv.Key.StartsWith(typePrefix)) continue; // 从链表尾部开始收集(尾部 = 最久未使用) var node = kv.Value.Last; while (node != null && evicted.Count < count) { evicted.Add(node.Value); node = node.Previous; } // 已收集足够数量,提前退出 if (evicted.Count >= count) break; } // 从池中移除 foreach (var audio in evicted) { RemoveFromPool(audio); } return evicted; } /// /// 生成池 key(格式:类型_配置key) /// private string MakePoolKey(EAudioType type, string key) { return $"{type}_{key}"; } /// /// 重置所有类型的计数 /// private void ResetCounts() { _currentCount[EAudioType.Music] = 0; _currentCount[EAudioType.Sound] = 0; _currentCount[EAudioType.Speak] = 0; } #endregion }