MyFramework:ResourceManager 资源加载体系解析
MyFramework 中的ResourceManager不是简单封装LoadAsset。
它主要负责统一几件事:
编辑器加载 AssetBundle 加载 资源路径规则 资源引用管理 异步加载回调 AssetBundle 依赖 资源下载 资源卸载项目地址:
https://github.com/ZHOURUIH/MyFramework
一、定位
ResourceManager是框架中的资源入口。
外部加载资源时,不直接关心当前资源来自哪里。
编辑器下可以走AssetDatabase。
打包后强制走AssetBundle。
调用层只需要使用统一接口:
loadGameResource<T>() loadGameResourceAsync<T>() loadSubGameResource<T>() getGameResource<T>() unload<T>() 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 Dictionary<string, AssetBundleInfo> mAssetBundleInfoList = new(); protected Dictionary<string, AssetInfo> mAssetToBundleInfo = new();含义是:
mAssetBundleInfoList AssetBundle 名 -> AssetBundleInfo mAssetToBundleInfo 资源路径 -> AssetInfo初始化时会解析配置文件。
配置文件中记录:
AssetBundle 名 AssetBundle 包含的资源列表 AssetBundle 依赖项解析完成后,还会调用:
findAllDependence()把依赖关系从名字转换成AssetBundleInfo引用。
这样后续加载资源时,可以直接从资源路径找到所属 AssetBundle,再处理依赖加载。
六、同步加载
同步加载入口是:
public ResourceRef<T> loadGameResource<T>(string name, bool errorIfNull = true) where T : UObject内部流程:
检查路径 ↓ 根据加载源选择加载器 ↓ AssetDatabase 加载或 AssetBundle 加载 ↓ 加载成功后创建 ResourceRef<T> ↓ 返回资源引用对象AssetBundle模式下,资源加载过程是:
资源路径 ↓ mAssetToBundleInfo 找到 AssetInfo ↓ AssetInfo 找到 AssetBundleInfo ↓ 先加载依赖包 ↓ 加载当前 AssetBundle ↓ LoadAssetWithSubAssets ↓ 返回主资源同步加载不只是加载单个资源。
它会确保资源所属的 AssetBundle 已经加载,并且依赖包也已经加载。
七、异步加载
异步加载入口有多种重载:
loadGameResourceAsync<T>(string name, Action<ResourceRef<T>> callback) loadGameResourceAsync<T>(string name, Action<ResourceRef<T>, string> callback) loadGameResourceAsync<T>(string name, AssetRefLoadCallback<T> callback)底层统一走:
loadGameResourceAsyncInternal<T>()AssetBundle模式下异步流程:
资源路径 ↓ 找到 AssetInfo ↓ 找到 AssetBundleInfo ↓ 如果 AssetBundle 未加载,先异步加载依赖 ↓ 等待依赖加载完成 ↓ 加载当前 AssetBundle ↓ 加载 Asset ↓ 回调 ResourceRef<T>AssetBundleInfo中用mLoadAsyncList保存资源包未加载完成时发起的资源请求。
资源包加载完成后,再统一触发这些资源的异步加载。
八、安全加载
异步加载最常见的问题是:
资源还没加载完,对象已经销毁MyFramework 提供了安全加载接口:
loadGameResourceAsyncSafe<T>(IRecyclable relatedObj, string name, Action<ResourceRef<T>> callback)它会记录对象当前的AssignID。
回调时再次比较:
加载发起时的 AssignID 是否等于 回调时对象当前 AssignID如果不一致,说明对象已经被销毁或复用。
这时不会执行回调,并且会卸载已经加载出的资源。
这个设计适合 UI、角色、特效等对象的异步资源加载。
对象销毁后,不需要在每个回调里手动判断一堆状态。
九、资源引用
MyFramework 没有直接把UnityEngine.Object裸返回给业务层,而是返回:
ResourceRef<T>ResourceRef<T>中保存:
protected T mResource; protected long mToken;资源加载成功后,会调用:
mResourceManager.addReference(mResource)生成一个引用凭证token。
ResourceManager内部记录:
protected Dictionary<int, HashSet<long>> mReferenceTokenList = new(); protected Dictionary<int, UObject> mInstanceIDToUObject = new();这里使用的是GetInstanceID(),不是直接使用UObject做 Key。
原因是 Unity 的Object重载了==。
外部如果把资源卸载掉,可能出现对象引用状态异常,但GetHashCode不变。
所以这里使用InstanceID来追踪资源引用。
十、引用释放
资源释放方式是:
mResourceManager.unload(ref resRef);它内部会回收ResourceRef<T>。
ResourceRef<T>.destroy()中会调用:
mResourceManager.removeReference(mResource, ref mToken)资源引用凭证移除后,ResourceManager会定时检查:
protected const float CHECK_REF_INTERVAL = 3.0f;当某个资源的引用凭证列表为空,就会调用内部卸载逻辑。
这套设计的重点是:
资源卸载不依赖业务层直接传 Unity 对象,而是依赖 ResourceRef 的生命周期。
资源谁持有,谁释放。
引用都释放后,资源才进入卸载流程。
十一、AssetBundle 依赖
AssetBundleInfo中保存两类依赖:
protected Dictionary<string, AssetBundleInfo> mParents = new(); protected Dictionary<string, 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负责。
编辑器下使用:
loadAssetAtPath<T>() loadAllAssetsAtPath()非编辑器下备用Resources.Load,但打包后ResourceManager默认强制使用AssetBundle。
AssetDataBaseLoader也会缓存已加载资源:
protected Dictionary<string, Dictionary<string, AssetDataBaseLoadInfo>> mLoadedPath = new(); protected Dictionary<UObject, AssetDataBaseLoadInfo> mLoadedObjects = new();这样编辑器模式和 AssetBundle 模式都能走ResourceManager的统一接口。
开发阶段不需要每次都打 AssetBundle。
打包环境又可以按真实 AssetBundle 流程运行。
十五、子资源
有些资源不只是一个主资源。
例如:
SpriteAtlas FBX 包含多个子资源的文件MyFramework 提供:
loadSubGameResource<T>()它会返回子资源数组,同时返回主资源引用:
UObject[] loadSubGameResource<T>(string name, out ResourceRef<UObject> mainAsset)这里主资源使用ResourceRef管理生命周期。
子资源跟随主资源,不单独生成引用对象。
这适合图集、FBX 这类资源。
十六、回调处理
异步加载可能出现多个地方同时请求同一个资源。
AssetInfo中保存回调列表:
protected List<AssetLoadCallback> mCallback = new(); protected List<string> mLoadPath = new();资源加载完成后统一回调:
callbackAll()回调前会先把列表移动到临时列表中。
这样可以避免回调过程中再次修改回调列表,造成遍历错误。
这是 MyFramework 很常见的处理方式:
回调列表先转移 再遍历执行 避免执行期间修改原列表十七、外部接口
对业务层来说,常用接口主要是这些:
ResourceRef<T> loadGameResource<T>(string name) CustomAsyncOperation loadGameResourceAsync<T>( string name, Action<ResourceRef<T>> callback ) CustomAsyncOperation loadGameResourceAsyncSafe<T>( IRecyclable relatedObj, string name, Action<ResourceRef<T>> callback ) T getGameResource<T>(string name) bool isGameResourceLoaded<T>(string name) void unload<T>(ref ResourceRef<T> 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的主要设计。
