using Newtonsoft.Json; using Spine.Unity; using System; using System.Collections; using System.Collections.Generic; using System.Threading.Tasks; using UnityEngine; using UnityEngine.AddressableAssets; using UnityEngine.InputSystem; using UnityEngine.ResourceManagement.AsyncOperations; using static UnityEngine.AddressableAssets.Addressables; [Serializable] public class AudioResourcesData { public int audioID; public string audioResourcesPath; public static Dictionary name_IDDic=new Dictionary(); public static Dictionary audioClipDic=new Dictionary(); static Dictionary audioClipDefaultDic=new Dictionary(); static Dictionary audioClipGameDic=new Dictionary(); const string Audio_AALabel="Audio"; // ⭐ 句柄显式管理(修复"包只增不减"的根源): // 之前 Load 的句柄直接丢弃(泄漏,包永不卸载),UnLoad 再重新 Load 一遍拿句柄释放 // (净引用数 0,等于什么都没卸载)。现在:加载时保存句柄,卸载时真正 Release。 // 注意:游戏音频物理上打在鱼包里(Audio+玩法 交集 = 鱼包),释放本句柄是鱼包能卸载的前提之一。 /// 默认音频句柄(Audio+Default,进程常驻,不卸载) static AsyncOperationHandle> _defaultAudioHandle; /// 当前玩法游戏音频句柄(Audio+玩法,进房加载/离房释放) static AsyncOperationHandle> _gameAudioHandle; static AudioResourcesData() { //默认音效(背景音乐、按钮点击 PW_Button 等)放在 Audio+Default 标签集合里,随启动常驻加载 LoadDefaultAudioGame(); } static async void LoadDefaultAudioGame() { string prefabFishPath = Resources.Load("tbaudio").text; var v = JsonConvert.DeserializeObject>(prefabFishPath); foreach (var item in v) { Debug.Log("加载文件音频ID: " + item.audioID); Debug.Log(item.audioID + "加载文件音频路径: " + item.audioResourcesPath); name_IDDic[item.audioResourcesPath]=item.audioID; } var keys = new List { Audio_AALabel, "Default" }; _defaultAudioHandle = Addressables.LoadAssetsAsync(keys, audio => { Debug.Log("加载音频: " + audio.name); if (name_IDDic.ContainsKey(audio.name)) { Debug.Log("加载音频ID: " + name_IDDic[audio.name]); audioClipDefaultDic.Add(name_IDDic[audio.name],audio); } }, MergeMode.Intersection); await _defaultAudioHandle.Task; foreach (var item in audioClipDefaultDic) { audioClipDic.Add(item.Key, item.Value); } Debug.Log("加载了:"+ audioClipDic.Count); } //public static async Task LoadAudioGame() //{ // GameType gameType=GameManager.Instance.gameData.currGameType; // var keys = new List { Audio_AALabel, gameType.ToString() }; // await Addressables.LoadAssetsAsync(keys, audio => // { // //Debug.Log("加载音频: " + audio.name); // //Debug.Log("加载音频ID: " + name_IDDic[audio.name]); // if (name_IDDic.ContainsKey(audio.name)) // { // audioClipGameDic[name_IDDic[audio.name]]= audio; // } // }, MergeMode.Intersection).Task; // audioClipDic=new Dictionary(audioClipDefaultDic); // foreach (var item in audioClipGameDic) // { // audioClipDic[item.Key]=item.Value; // } // Debug.Log("加载了:"+ audioClipDic.Count); //} /// /// 加载当前玩法游戏音频(Audio+玩法 交集)。 /// onProgress:进度回调 0~1(按 LoadAssetsAsync 的 PercentComplete),供进房进度条使用;不传则行为不变。 /// public static async Task LoadAudioGame(Action onProgress = null) { // ⭐ 资源加载超过 0.1 秒才显示"加载中"界面(引用计数,快速加载不显示) UIManager.BeginLoading(); try { await LoadAudioGameCore(onProgress); } finally { UIManager.EndLoading(); } } /// LoadAudioGame 的实际逻辑(外层负责"加载中"界面) private static async Task LoadAudioGameCore(Action onProgress) { GameType gameType = GameManager.Instance.gameData.currGameType; var keys = new List { Audio_AALabel, gameType.ToString() }; try { // ⭐ 防御:上一轮句柄未释放(如离房被跳过)时先释放,避免引用计数累加 if (_gameAudioHandle.IsValid()) Addressables.Release(_gameAudioHandle); // ⭐ 双标签交集(Audio + 当前玩法):只加载当前玩法标记的音频。 // Union 会按 "Audio" 标签把其它玩法的音频包一并打开, // 而这些音频物理上和鱼的 Prefab 打在同一个 LZMA 包里 → 整包解压进内存导致 iOS 被杀 // 句柄保存到 _gameAudioHandle,离房时 UnLoadAudioGame 真正释放 → 鱼包才能卸载 _gameAudioHandle = Addressables.LoadAssetsAsync(keys, audio => { if (name_IDDic != null && name_IDDic.ContainsKey(audio.name)) { audioClipGameDic[name_IDDic[audio.name]] = audio; } }, MergeMode.Intersection); // 50ms 轮询上报进度(无回调时直接等完成) if (onProgress != null) { while (!_gameAudioHandle.IsDone) { onProgress(_gameAudioHandle.PercentComplete); await Task.Delay(50); } onProgress(1f); } else { await _gameAudioHandle.Task; } audioClipDic = new Dictionary(); if (audioClipDefaultDic != null) { foreach (var item in audioClipDefaultDic) audioClipDic[item.Key] = item.Value; } foreach (var item in audioClipGameDic) { audioClipDic[item.Key] = item.Value; } Debug.Log("加载音频数量: " + audioClipDic.Count); } catch (Exception e) { onProgress?.Invoke(1f); Debug.LogError($"加载游戏音频失败: {e.Message}"); } } public static Task UnLoadAudioGame() { // ⭐ 真正的卸载:释放加载时保存的句柄(引用归零后 Audio+玩法 命中的 bundle 才会真正卸载)。 // 旧实现"重新 Load 一遍拿句柄再 Release"是净 0 操作,什么都卸不掉。 if (_gameAudioHandle.IsValid()) Addressables.Release(_gameAudioHandle); // 清空游戏音频字典(包卸载后 AudioClip 对象已失效,残留引用会导致播放 null) audioClipGameDic.Clear(); // 重建总字典:只保留默认音频 audioClipDic = new Dictionary(audioClipDefaultDic); Debug.Log("卸载完成,当前音频数量:" + audioClipDic.Count); return Task.CompletedTask; } }