MyFramework 中的ResourceManager不是简单封装LoadAsset。它主要负责统一几件事编辑器加载 AssetBundle 加载 资源路径规则 资源引用管理 异步加载回调 AssetBundle 依赖 资源下载 资源卸载项目地址https://github.com/ZHOURUIH/MyFramework一、定位ResourceManager是框架中的资源入口。外部加载资源时不直接关心当前资源来自哪里。编辑器下可以走AssetDatabase。打包后强制走AssetBundle。调用层只需要使用统一接口loadGameResourceT() loadGameResourceAsyncT() loadSubGameResourceT() getGameResourceT() unloadT() unloadPath()这样业务层不会到处写AssetDatabase.LoadAssetAtPath、Resources.Load、AssetBundle.LoadAsset。加载源变化时业务代码不用改。二、加载源ResourceManager内部有两个加载器protected AssetDataBaseLoader mAssetDataBaseLoader new(); protected AssetBundleLoader mAssetBundleLoader new();编辑器下根据配置选择加载源mLoadSource isEditor() ? GameEntryBase.getInstance().mFrameworkParam.mLoadSource : LOAD_SOURCE.ASSET_BUNDLE;打包后固定使用AssetBundle。这点很重要。编辑器阶段可以为了开发效率走AssetDatabase但最终运行环境必须按真实资源包流程验证。三、路径规则MyFramework 中资源路径统一使用GameResources下的相对路径。例如UI/UIPrefab/UILogin.prefab Texture/Icon/item_1001.png Audio/BGM/main.mp3路径要求必须带后缀 不能传绝对路径 不能以 Assets 开头 不能以 Assets/GameResources 开头 不能使用反斜杠对应检查在checkRelativePath中完成。这样做的目的很明确业务代码只使用工程内统一的资源逻辑路径不直接依赖 Unity 工程路径。资源最终来自AssetDatabase还是AssetBundle由ResourceManager决定。四、初始化如果加载源是AssetBundle框架启动时会先初始化资源清单。入口是preInitAsync()逻辑是ResourceManager.preInitAsync ↓ AssetBundleLoader.initAssets ↓ 加载 StreamingAssets 配置文件 ↓ 解析 AssetBundle、Asset、依赖关系 ↓ 建立资源名到 AssetBundle 的索引如果不是AssetBundle加载直接回调完成。所以资源系统初始化阶段最关键的事情是先把资源清单读出来。否则后面通过资源名加载时无法知道这个资源在哪个 AssetBundle 里。五、资源清单AssetBundleLoader中有几个核心索引protected Dictionarystring, AssetBundleInfo mAssetBundleInfoList new(); protected Dictionarystring, AssetInfo mAssetToBundleInfo new();含义是mAssetBundleInfoList AssetBundle 名 - AssetBundleInfo mAssetToBundleInfo 资源路径 - AssetInfo初始化时会解析配置文件。配置文件中记录AssetBundle 名 AssetBundle 包含的资源列表 AssetBundle 依赖项解析完成后还会调用findAllDependence()把依赖关系从名字转换成AssetBundleInfo引用。这样后续加载资源时可以直接从资源路径找到所属 AssetBundle再处理依赖加载。六、同步加载同步加载入口是public ResourceRefT loadGameResourceT(string name, bool errorIfNull true) where T : UObject内部流程检查路径 ↓ 根据加载源选择加载器 ↓ AssetDatabase 加载或 AssetBundle 加载 ↓ 加载成功后创建 ResourceRefT ↓ 返回资源引用对象AssetBundle模式下资源加载过程是资源路径 ↓ mAssetToBundleInfo 找到 AssetInfo ↓ AssetInfo 找到 AssetBundleInfo ↓ 先加载依赖包 ↓ 加载当前 AssetBundle ↓ LoadAssetWithSubAssets ↓ 返回主资源同步加载不只是加载单个资源。它会确保资源所属的 AssetBundle 已经加载并且依赖包也已经加载。七、异步加载异步加载入口有多种重载loadGameResourceAsyncT(string name, ActionResourceRefT callback) loadGameResourceAsyncT(string name, ActionResourceRefT, string callback) loadGameResourceAsyncT(string name, AssetRefLoadCallbackT callback)底层统一走loadGameResourceAsyncInternalT()AssetBundle模式下异步流程资源路径 ↓ 找到 AssetInfo ↓ 找到 AssetBundleInfo ↓ 如果 AssetBundle 未加载先异步加载依赖 ↓ 等待依赖加载完成 ↓ 加载当前 AssetBundle ↓ 加载 Asset ↓ 回调 ResourceRefTAssetBundleInfo中用mLoadAsyncList保存资源包未加载完成时发起的资源请求。资源包加载完成后再统一触发这些资源的异步加载。八、安全加载异步加载最常见的问题是资源还没加载完对象已经销毁MyFramework 提供了安全加载接口loadGameResourceAsyncSafeT(IRecyclable relatedObj, string name, ActionResourceRefT callback)它会记录对象当前的AssignID。回调时再次比较加载发起时的 AssignID 是否等于 回调时对象当前 AssignID如果不一致说明对象已经被销毁或复用。这时不会执行回调并且会卸载已经加载出的资源。这个设计适合 UI、角色、特效等对象的异步资源加载。对象销毁后不需要在每个回调里手动判断一堆状态。九、资源引用MyFramework 没有直接把UnityEngine.Object裸返回给业务层而是返回ResourceRefTResourceRefT中保存protected T mResource; protected long mToken;资源加载成功后会调用mResourceManager.addReference(mResource)生成一个引用凭证token。ResourceManager内部记录protected Dictionaryint, HashSetlong mReferenceTokenList new(); protected Dictionaryint, UObject mInstanceIDToUObject new();这里使用的是GetInstanceID()不是直接使用UObject做 Key。原因是 Unity 的Object重载了。外部如果把资源卸载掉可能出现对象引用状态异常但GetHashCode不变。所以这里使用InstanceID来追踪资源引用。十、引用释放资源释放方式是mResourceManager.unload(ref resRef);它内部会回收ResourceRefT。ResourceRefT.destroy()中会调用mResourceManager.removeReference(mResource, ref mToken)资源引用凭证移除后ResourceManager会定时检查protected const float CHECK_REF_INTERVAL 3.0f;当某个资源的引用凭证列表为空就会调用内部卸载逻辑。这套设计的重点是资源卸载不依赖业务层直接传 Unity 对象而是依赖 ResourceRef 的生命周期。资源谁持有谁释放。引用都释放后资源才进入卸载流程。十一、AssetBundle 依赖AssetBundleInfo中保存两类依赖protected Dictionarystring, AssetBundleInfo mParents new(); protected Dictionarystring, AssetBundleInfo mChildren new();含义是mParents 当前包依赖的 AssetBundle mChildren 依赖当前包的 AssetBundle加载当前包之前会先加载所有依赖包。同步加载时foreach (var item in mParents) { item.Value.loadAssetBundle(); }异步加载时loadParentAsync();卸载时也要看依赖关系。一个 AssetBundle 只有在满足两个条件时才能卸载当前包内资源没有正在使用 没有其他正在使用的 AssetBundle 依赖自己对应逻辑在canUnload()中。这可以避免依赖包被提前卸载导致其他包中的资源引用异常。十二、延迟卸载AssetBundleInfo中有一个延迟卸载时间protected const float UNLOAD_DELAY_TIME 5.0f;当包内资源没有引用时不是立刻卸载而是延迟 5 秒。原因是资源可能很快又被重新加载。立即卸载会造成频繁 Load / Unload。延迟卸载可以减少抖动。逻辑大致是资源引用清空 ↓ 设置 mWillUnloadTime 5 秒 ↓ update 中倒计时 ↓ 倒计时结束后再次检查 canUnload ↓ 满足条件才真正卸载这比引用一清空就立刻卸载更稳。十三、资源下载AssetBundleLoader支持资源包动态下载。当本地找不到某个 AssetBundle 文件时会从下载地址请求mDownloadURL bundleFileName下载完成后如果不是 WebGL会写入本地持久化目录PersistentAssets并更新本地文件列表mAssetVersionSystem.addPersistentFile(fileInfo); writeFileList(...)这样下载后的资源包可以进入本地资源版本管理。下载流程主要服务热更新资源。十四、编辑器加载编辑器加载由AssetDataBaseLoader负责。编辑器下使用loadAssetAtPathT() loadAllAssetsAtPath()非编辑器下备用Resources.Load但打包后ResourceManager默认强制使用AssetBundle。AssetDataBaseLoader也会缓存已加载资源protected Dictionarystring, Dictionarystring, AssetDataBaseLoadInfo mLoadedPath new(); protected DictionaryUObject, AssetDataBaseLoadInfo mLoadedObjects new();这样编辑器模式和 AssetBundle 模式都能走ResourceManager的统一接口。开发阶段不需要每次都打 AssetBundle。打包环境又可以按真实 AssetBundle 流程运行。十五、子资源有些资源不只是一个主资源。例如SpriteAtlas FBX 包含多个子资源的文件MyFramework 提供loadSubGameResourceT()它会返回子资源数组同时返回主资源引用UObject[] loadSubGameResourceT(string name, out ResourceRefUObject mainAsset)这里主资源使用ResourceRef管理生命周期。子资源跟随主资源不单独生成引用对象。这适合图集、FBX 这类资源。十六、回调处理异步加载可能出现多个地方同时请求同一个资源。AssetInfo中保存回调列表protected ListAssetLoadCallback mCallback new(); protected Liststring mLoadPath new();资源加载完成后统一回调callbackAll()回调前会先把列表移动到临时列表中。这样可以避免回调过程中再次修改回调列表造成遍历错误。这是 MyFramework 很常见的处理方式回调列表先转移 再遍历执行 避免执行期间修改原列表十七、外部接口对业务层来说常用接口主要是这些ResourceRefT loadGameResourceT(string name) CustomAsyncOperation loadGameResourceAsyncT( string name, ActionResourceRefT callback ) CustomAsyncOperation loadGameResourceAsyncSafeT( IRecyclable relatedObj, string name, ActionResourceRefT callback ) T getGameResourceT(string name) bool isGameResourceLoadedT(string name) void unloadT(ref ResourceRefT res) void unloadPath(string path)调用层只关心资源名和类型。加载源、依赖、缓存、引用、卸载都由ResourceManager处理。十八、设计取舍这套资源系统没有直接使用 Unity 的Addressables。MyFramework 的做法更偏向自己控制完整流程资源路径自己定义 资源清单自己生成 AssetBundle 依赖自己管理 资源引用自己维护 下载和版本系统自己接入 卸载时机自己控制这样做比直接接入现成方案更麻烦。但好处是每个环节都能按项目规则调整。对于长期项目尤其是需要热更新、资源版本、服务器下载、资源检查和自定义打包规则的项目这种方式更容易和整套框架配合。十九、适用场景这套ResourceManager更适合下面这些项目需要 AssetBundle 热更新 需要编辑器和运行时加载方式统一 资源路径需要统一规则 需要资源引用管理 需要控制 AssetBundle 卸载时机 需要动态下载资源包 需要资源版本系统配合 需要加载 SpriteAtlas、FBX 等子资源如果只是很小的 Demo直接Resources.Load或简单封装就够了。MyFramework 的资源系统更适合中大型项目和长期维护项目。总结ResourceManager的核心不是封装一层LoadAsset。它真正做的是把资源加载相关流程统一起来统一资源路径 统一编辑器加载和 AssetBundle 加载 统一同步和异步接口 统一资源引用 统一 AssetBundle 依赖 统一下载逻辑 统一卸载流程 统一子资源加载业务层只使用GameResources下的相对路径。框架内部根据加载源决定走AssetDatabase还是AssetBundle。资源加载成功后通过ResourceRef管理引用。引用释放后再进入卸载流程。AssetBundle 卸载时还会检查资源引用和依赖关系。这就是 MyFramework 中ResourceManager的主要设计。