// Copyright (c) 2025 EFramework Innovation. All rights reserved. // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. using System; using System.IO; using System.Collections; using System.Collections.Generic; using UnityEngine; using EFramework.Unity.Utility; namespace EFramework.Unity.Asset { public partial class XAsset { /// /// XAsset.Bundle 提供了资源包的管理功能,支持自动处理依赖关系,并通过引用计数管理资源包的生命周期。 /// /// /// /// 功能特性 /// - 资源包管理:支持同步和异步加载资源包,支持缓存 /// - 引用计数管理:自动处理依赖关系,通过引用计数管理资源的生命周期 /// /// 使用手册 /// 1. 基本用法 /// /// 1.1 初始化资源清单 /// - 功能说明:初始化 Bundle 的清单文件 /// - 使用示例: /// /// /// Initialize 初始化 Bundle 的清单文件,如果存在旧的清单会先卸载它。 /// /// 仅适用于 Bundle 模式,这个清单文件对于资源的正确加载是必需的。 /// XAsset.Bundle.Initialize(); /// /// /// 1.2 同步加载资源包 /// - 功能说明:加载指定的资源包及其所有依赖资源包 /// - 使用示例: /// /// var bundle = XAsset.Bundle.Load("example.bundle"); /// /// /// 1.3 异步加载资源包 /// - 功能说明:异步加载指定的资源包及其所有依赖资源包 /// - 使用示例: /// /// var handler = new Handler(); /// yield return XAsset.Bundle.LoadAsync("example.bundle", handler); /// /// /// 1.4 查找已加载的资源包 /// - 功能说明:在已加载的资源包中查找指定名称的资源包 /// - 使用示例: /// /// var bundle = XAsset.Bundle.Find("example.bundle"); /// /// /// 1.5 卸载已加载的资源包 /// - 功能说明:卸载指定的资源包,减少其引用计数,当计数为 0 时释放资源 /// - 使用示例: /// /// XAsset.Bundle.Unload("example.bundle"); /// /// /// 2. 引用计数 /// /// 2.1 增加引用 /// - 功能说明:增加资源包的引用计数,同时增加所有依赖资源包的引用计数 /// - 使用示例: /// /// var count = bundle.Retain(); /// /// /// 2.2 减少引用 /// - 功能说明:减少资源包的引用计数,当计数为 0 时自动卸载资源包及其不再被引用的依赖资源 /// - 使用示例: /// /// var count = bundle.Release(); /// /// /// 更多信息请参考模块文档。 /// public partial class Bundle { /// /// Name 是资源包的名称,用于在资源系统中唯一标识一个资源包。 /// public string Name { get; internal set; } /// /// Source 是 Unity 的 AssetBundle 对象,包含实际的资源数据。 /// public AssetBundle Source { get; internal set; } /// /// Count 是资源包的引用计数,用于追踪资源包的使用情况。当计数为 0 时,资源包可以被安全卸载。 /// public int Count { get; internal set; } /// /// Retain 增加资源包的引用计数,同时增加所有依赖资源包的引用计数。 /// /// 引用来源的描述,用于调试时追踪资源的使用情况 /// 增加后的引用计数 public int Retain(string from = "") { var dependencies = Manifest.GetAllDependencies(Name); foreach (var dependency in dependencies) { if (Name == dependency) continue; if (Loaded.TryGetValue(dependency, out var dependBundle)) { dependBundle.Count++; if (Constants.DebugMode) XLog.Debug("XAsset.Bundle.Retain: depend bundle: {0}, reference count: {1}, cached bundle count: {2}, retain from: {3}.", dependBundle.Name, dependBundle.Count, Loaded.Count, from); } } Count++; if (Constants.DebugMode) XLog.Debug("XAsset.Bundle.Retain: main bundle: {0}, reference count: {1}, cached bundle count: {2}, retain from: {3}.", Name, Count, Loaded.Count, from); return Count; } /// /// Release 减少资源包的引用计数,当计数为 0 时自动卸载资源包及其不再被引用的依赖资源。 /// /// 引用来源的描述,用于调试时追踪资源的使用情况 /// 减少后的引用计数 public int Release(string from = "") { var dependencies = Manifest.GetAllDependencies(Name); if (dependencies != null && dependencies.Length > 0) { for (var i = 0; i < dependencies.Length; i++) { var dependency = dependencies[i]; if (dependency == Name) continue; if (Loaded.TryGetValue(dependency, out var dependBundle)) { dependBundle.Count--; if (Constants.DebugMode) XLog.Debug("XAsset.Bundle.Release: depend bundle: {0}, reference count: {1}, cached bundle count: {2}, release from: {3}.", dependBundle.Name, dependBundle.Count, Loaded.Count, from); if (dependBundle.Source == null) Loaded.Remove(dependency); else if (dependBundle.Count <= 0) { dependBundle.Source.Unload(true); Loaded.Remove(dependency); if (Constants.DebugMode) XLog.Debug("XAsset.Bundle.Release: unload depend bundle: {0}, cached bundle: {1}, release from: {2}.", dependBundle.Name, Loaded.Count, from); try { Event.Notify(EventType.OnPostUnloadBundle, dependBundle.Source); } catch (Exception e) { XLog.Panic(e); } } } } } Count--; if (Constants.DebugMode) XLog.Debug("XAsset.Bundle.Release: main bundle: {0}, reference count: {1}, cached bundle count: {2}, release from: {3}.", Name, Count, Loaded.Count, from); if (Source == null) { Loaded.Remove(Name); } else if (Count <= 0) { try { Event.Notify(EventType.OnPostUnloadBundle, Source); } catch (Exception e) { XLog.Panic(e); } Source.Unload(true); Loaded.Remove(Name); if (Constants.DebugMode) XLog.Debug("XAsset.Bundle.Release: unload main bundle: {0}, cached bundle count: {1}, release from: {2}.", Name, Loaded.Count, from); } return Count; } } public partial class Bundle { /// /// Task 是异步加载任务,用于跟踪资源包的加载状态和进度。 /// internal class Task { /// /// Name 是正在加载的资源包名称。 /// internal string Name; /// /// Request 是 AssetBundle 的异步加载操作对象。 /// internal AssetBundleCreateRequest Request; /// /// IsDone 表示是否完成加载。 /// internal bool IsDone; } /// /// Manifest 是 Bundle 依赖的清单。 /// internal static AssetBundleManifest Manifest; /// /// manifestBundle 保持了 AssetBundleManifest 所在 Bundle 的引用。 /// private static AssetBundle manifestBundle; /// /// Loading 记录当前正在加载的资源包,用于处理并发加载请求。 /// internal static Dictionary Loading = new(); /// /// Loaded 缓存已加载的资源包,避免重复加载相同的资源。 /// internal static Dictionary Loaded = new(); /// /// Initialize 初始化 Bundle 的清单文件,如果存在旧的清单会先卸载它。 /// 仅适用于 Bundle 模式,这个清单文件对于资源的正确加载是必需的。 /// public static void Initialize() { if (Constants.BundleMode) { try { if (manifestBundle) { manifestBundle.Unload(true); XLog.Notice("XAsset.Bundle.Initialize: previous manifest has been unloaded."); } } catch (Exception e) { XLog.Panic(e, "XAsset.Bundle.Initialize: unload manifest failed."); } var file = XFile.PathJoin(Constants.LocalPath, Constants.GetName(Constants.Manifest)); if (XFile.HasFile(file)) { try { manifestBundle = AssetBundle.LoadFromFile(file, 0, Constants.GetOffset(Path.GetFileName(file))); Manifest = manifestBundle.LoadAsset("AssetBundleManifest"); XLog.Notice("XAsset.Bundle.Initialize: load manifest from {1} succeeded.", Path.GetFullPath(file), Path.GetRelativePath(XEnv.LocalPath, file)); } catch (Exception e) { XLog.Panic(e, "XAsset.Bundle.Initialize: load manifest from {1} failed.".Format(Path.GetFullPath(file), Path.GetRelativePath(XEnv.LocalPath, file))); } } else XLog.Warn("XAsset.Bundle.Initialize: load failed because of non exist file: {0}.", Path.GetRelativePath(XEnv.LocalPath, file)); } else Manifest = null; } /// /// Load 同步加载资源包。如果资源包已加载,则直接返回缓存的实例,否则会加载资源包及其所有依赖。 /// /// 要加载的资源包名称 /// 加载的资源包,如果加载失败则返回 null public static Bundle Load(string name) { if (!Loaded.TryGetValue(name, out var bundleInfo)) { var dependencies = Manifest.GetAllDependencies(name); if (dependencies != null && dependencies.Length > 0) { var breakDependency = -1; for (var i = 0; i < dependencies.Length; i++) { var dependency = dependencies[i]; if (!Loaded.ContainsKey(dependency)) { if (Loading.TryGetValue(dependency, out var dependencyTask)) { // 如果正在异步加载,通过访问 assetBundle 可以获取到实例,然后 isDone 为 true // 这样就可以解决同步和异步并发加载相同资源导致的异常 // 内部机制:https://docs.unity.cn/cn/2021.1/ScriptReference/AssetBundleCreateRequest-assetBundle.html // 踩坑小记:访问 assetBundle 时,其内部会执行 UnityEngine.SetupCoroutine:InvokeMoveNext // 这样一来 LoadAsync 的 yield request 后续的流程就会执行,然后装载实例至 Loaded 字典中 // 所以维护当前分支无需维护 Loaded 字典,仅获取即可 if (dependencyTask.Request.assetBundle) XLog.Warn("XAsset.Bundle.Load: access depend bundle: {0} in async loading queue.", dependency); else XLog.Error("XAsset.Bundle.Load: access nil depend bundle: {0} in async loading queue.", dependency); } else { var file = XFile.PathJoin(Constants.LocalPath, dependency); var bundle = AssetBundle.LoadFromFile(file, 0, Constants.GetOffset(dependency)); if (bundle == null) { XLog.Error("XAsset.Bundle.Load: sync load depend bundle error: {0}.", dependency); breakDependency = i; break; } else { var dependBundle = new Bundle() { Name = dependency, Source = bundle }; Loaded.Add(dependency, dependBundle); } } } } // 如果有任何一个依赖加载失败,则解除对已加载依赖的引用 if (breakDependency >= 0) { for (var i = 0; i < breakDependency; i++) { var dependency = dependencies[i]; if (Loaded.TryGetValue(dependency, out var dependBundle)) { if (dependBundle.Count == 0) // 只处理未被引用的 Bundle { if (dependBundle.Source) dependBundle.Source.Unload(true); Loaded.Remove(dependency); XLog.Warn("XAsset.Bundle.Load: unload: {0} because of broken depend bundle: {1}.", dependency, dependencies[breakDependency]); } } } return null; // 不再执行后续流程 } } if (Loading.TryGetValue(name, out var mainTask)) { // 流程处理机制参考上述加载依赖的分支 if (mainTask.Request.assetBundle) XLog.Warn("XAsset.Bundle.Load: access main bundle: {0} in async loading queue.", name); else XLog.Error("XAsset.Bundle.Load: access nil main bundle: {0} in async loading queue.", name); bundleInfo = Find(name); } else { var file = XFile.PathJoin(Constants.LocalPath, name); var bundle = AssetBundle.LoadFromFile(file, 0, Constants.GetOffset(name)); if (bundle == null) { XLog.Error("XAsset.Bundle.Load: sync load main bundle error: {0}.", name); // 如果主包加载失败,则解除对所有依赖的引用 if (dependencies != null && dependencies.Length > 0) { for (var i = 0; i < dependencies.Length; i++) { var dependency = dependencies[i]; if (Loaded.TryGetValue(dependency, out var dependBundle)) { if (dependBundle.Count == 0) // 只处理未被引用的 Bundle { if (dependBundle.Source) dependBundle.Source.Unload(true); Loaded.Remove(dependency); XLog.Warn("XAsset.Bundle.Load: unload: {0} because of broken main bundle: {1}.", dependency, name); } } } } return null; // 不再执行后续流程 } bundleInfo = new Bundle() { Name = name, Source = bundle }; Loaded.Add(name, bundleInfo); } } return bundleInfo; } /// /// LoadAsync 异步加载资源包。支持同时加载多个资源包,并通过 handler 参数报告加载进度。 /// /// 要加载的资源包名称 /// 用于跟踪和报告加载进度的处理器 /// 异步加载的协程对象 public static IEnumerator LoadAsync(string name, Handler handler = null) { if (!Loaded.ContainsKey(name)) { var dependencies = Manifest.GetAllDependencies(name); if (handler != null) handler.totalCount += dependencies.Length + 1; // Self and Dependency if (dependencies != null && dependencies.Length > 0) { var breakDependency = -1; for (var i = 0; i < dependencies.Length; i++) { var dependency = dependencies[i]; if (!Loaded.ContainsKey(dependency)) { if (!Loading.TryGetValue(dependency, out var task)) { task = new Task() { Name = dependency }; Loading.Add(dependency, task); var file = XFile.PathJoin(Constants.LocalPath, dependency); task.Request = AssetBundle.LoadFromFileAsync(file, 0, Constants.GetOffset(dependency)); yield return task.Request; Loading.Remove(dependency); task.IsDone = true; } else yield return new WaitUntil(() => task.IsDone); if (!Loaded.ContainsKey(dependency)) { if (task.Request.assetBundle == null) { XLog.Error("XAsset.Bundle.LoadAsync: async load depend bundle error: {0}.", dependency); breakDependency = i; break; } else { var bundle = new Bundle() { Name = dependency, Source = task.Request.assetBundle }; Loaded.Add(dependency, bundle); } } } if (handler != null) handler.doneCount++; } // 如果有任何一个依赖加载失败,则解除对已加载依赖的引用 if (breakDependency >= 0) { for (var i = 0; i < breakDependency; i++) { var dependency = dependencies[i]; if (Loaded.TryGetValue(dependency, out var dependBundle)) { if (dependBundle.Count == 0 && dependBundle.Source) // 只处理未被引用的 Bundle { dependBundle.Source.Unload(true); Loaded.Remove(dependency); XLog.Warn("XAsset.Bundle.LoadAsync: unload: {0} because of broken depend bundle: {1}.", dependency, dependencies[breakDependency]); } } } yield break; // 不再执行后续流程 } } if (!Loaded.ContainsKey(name)) { if (!Loading.TryGetValue(name, out var task)) { task = new Task() { Name = name }; Loading.Add(name, task); var file = XFile.PathJoin(Constants.LocalPath, name); task.Request = AssetBundle.LoadFromFileAsync(file, 0, Constants.GetOffset(name)); yield return task.Request; Loading.Remove(name); task.IsDone = true; } else yield return new WaitUntil(() => task.IsDone); if (task.Request.assetBundle == null) { XLog.Error("XAsset.Bundle.LoadAsync: async load main bundle error: {0}.", name); // 如果主包加载失败,则解除对所有依赖的引用 if (dependencies != null && dependencies.Length > 0) { for (var i = 0; i < dependencies.Length; i++) { var dependency = dependencies[i]; if (Loaded.TryGetValue(dependency, out var dependBundle)) { if (dependBundle.Count == 0 && dependBundle.Source) // 只处理未被引用的 Bundle { dependBundle.Source.Unload(true); Loaded.Remove(dependency); XLog.Warn("XAsset.Bundle.LoadAsync: unload: {0} because of broken main bundle: {1}.", dependency, name); } } } } yield break; // 不再执行后续流程 } else { if (!Loaded.ContainsKey(name)) { var bundleInfo = new Bundle() { Name = name, Source = task.Request.assetBundle }; Loaded.Add(name, bundleInfo); } } } if (handler != null) handler.doneCount++; } } /// /// Find 在已加载的资源包中查找指定名称的资源包。 /// /// 要查找的资源包名称 /// 找到的资源包,如果未找到则返回 null public static Bundle Find(string name) { Loaded.TryGetValue(name, out var info); return info; } } } }