Unity 项目里,资源管理是一个绕不开的问题。
小项目里可以直接Resources.Load,或者直接拖引用。
但项目一大,就会遇到很多问题:
- 编辑器和真机加载方式不一致
- AssetBundle 依赖关系需要管理
- 异步加载需要合并回调
- 资源卸载时机不好控制
- UI、特效、音效、表格都需要统一资源入口
- 热更新资源和本地资源要用同一套调用方式
MyFramework 里有自己的资源管理模块。
它和 YooAsset 这种成熟资源管理框架的定位并不一样。
项目地址:
https://github.com/ZHOURUIH/MyFramework
先说结论:
YooAsset 更像一个通用资源管理框架。
MyFramework 的资源管理更像框架内部的一层资源抽象。
YooAsset 重点解决的是资源包构建、资源定位、资源下载、资源缓存、资源版本、资源卸载这些通用问题。
MyFramework 的资源管理重点解决的是:
资源怎么融入自己的 UI、对象池、热更新、表格、特效、生命周期和工程结构。
所以这两个东西不是简单的谁替代谁。
它们的目标不同。
一、YooAsset 是通用资源系统,MyFramework 是框架内部模块
YooAsset 的目标很明确。
它是一套 Unity3D 资源管理系统,用于帮助团队快速部署和交付游戏。官方介绍里也提到,它支持编辑器模拟模式、单机运行模式、联机运行模式、Web 运行模式,支持引用计数、边玩边下、多功能下载器、版本管理等能力。
也就是说,YooAsset 重点解决的是:
- 资源包怎么构建
- 资源怎么定位
- 资源怎么下载
- 资源怎么缓存
- 资源怎么版本管理
- 资源怎么卸载
- 多运行模式怎么切换
MyFramework 的资源管理不是独立产品。
它是 MyFramework 运行时的一部分。
它要解决的是:
- 编辑器下用 AssetDatabase 加载
- 打包后用 AssetBundle 加载
- 业务层统一调用资源
- 异步加载结果用
ResourceRef<T>管理 - 资源引用释放接入框架对象池
- 资源卸载接入框架生命周期
- 资源路径规则强制统一
- AssetBundle 依赖关系由框架自己管理
这就是最核心的区别。
YooAsset 关注的是资源系统本身。
MyFramework 关注的是资源怎么服务整个框架。
二、YooAsset 的入口是 ResourcePackage
YooAsset 3.0.x 中,资源系统初始化后需要创建或获取ResourcePackage,资源加载、下载、清理等操作都应该通过ResourcePackage实例调用。官方文档也说明 3.0 版本已经移除了默认包裹静态快捷加载接口。
YooAsset 官方文档里的初始化方式大致是:
YooAssets.Initialize(); var package = YooAssets.CreatePackage("DefaultPackage"); var package = YooAssets.GetPackage("DefaultPackage");资源加载也是通过 package 调用。
比如官方文档里的异步加载示例:
AssetHandle handle = package.LoadAssetAsync<AudioClip>("Assets/GameRes/Audio/bgMusic.mp3"); yield return handle; AudioClip audioClip = handle.AssetObject as AudioClip;预制体加载示例:
AssetHandle handle = package.LoadAssetAsync<GameObject>("Assets/GameRes/Panel/login.prefab"); yield return handle; GameObject go = handle.InstantiateSync(); Debug.Log($"Prefab name is {go.name}");这套方式很标准。
业务层拿到的是AssetHandle。
资源对象在handle.AssetObject里。
如果要实例化预制体,可以用handle.InstantiateSync()。
如果资源不用了,需要释放句柄。
官方文档里的卸载示例也明确写了:
AssetHandle handle = package.LoadAssetAsync<AudioClip>("Assets/GameRes/Audio/bgMusic.mp3"); yield return handle; handle.Release();这就是 YooAsset 的资源生命周期模型:
加载返回 Handle,不用时 Release。
三、MyFramework 的入口是 ResourceManager
MyFramework 里资源管理的核心入口是ResourceManager。
它是一个FrameSystem。
资源系统初始化时,会根据当前环境决定加载源。
ResourceManager.init()是这样写的:
public override void init() { base.init(); mLoadSource = isEditor() ? GameEntryBase.getInstance().mFrameworkParam.mLoadSource : LOAD_SOURCE.ASSET_BUNDLE; if (isEditor()) { mObject.AddComponent<ResourcesManagerDebug>(); } }这里能看出几个关键点。
第一,MyFramework 会判断是否在编辑器下。
第二,编辑器下的加载源可以通过mFrameworkParam.mLoadSource配置。
第三,非编辑器环境强制使用LOAD_SOURCE.ASSET_BUNDLE。
也就是说,MyFramework 的资源模块不是让业务层自己选择加载模式。
加载模式由框架启动参数决定。
业务层只需要调用资源接口。
四、MyFramework 同步加载返回 ResourceRef
MyFramework 的同步加载接口不是直接返回 Unity 对象,而是返回ResourceRef<T>。
loadGameResource是这样写的:
public ResourceRef<T> loadGameResource<T>(string name, bool errorIfNull = true) where T : UObject { using var a = new ProfilerScope(0); checkRelativePath(name); T res = null; if (mLoadSource == LOAD_SOURCE.ASSET_DATABASE) { res = mAssetDataBaseLoader.loadResource<T>(name); } else if (mLoadSource == LOAD_SOURCE.ASSET_BUNDLE) { res = mAssetBundleLoader.loadAsset<T>(name); } if (res == null && errorIfNull) { logError("can not find resource : " + name + ",请确认文件存在,且带后缀名,且不能使用反斜杠\\," + (name.Contains(' ') || name.Contains(' ') ? "注意此文件名中带有空格" : "")); } if (res == null) { return null; } CLASS(out ResourceRef<T> resRef).setResource(res); return resRef; }这段代码很能说明 MyFramework 的特点。
它不是直接把UObject返回给业务层,而是创建一个ResourceRef<T>。
也就是说,加载资源后,框架希望外部持有的是资源引用对象,而不是裸资源对象。
这个设计和 YooAsset 的AssetHandle有一点类似,都是希望资源生命周期有一个明确的持有者。
但实现方式不一样。
YooAsset 的持有者是AssetHandle。
MyFramework 的持有者是ResourceRef<T>。
五、ResourceRef 是 MyFramework 自己的引用凭证
它的作用不是简单包装一下资源对象,而是会向ResourceManager注册引用凭证。
代码如下:
public class ResourceRef<T> : ClassObject where T : UObject { protected T mResource; // 引用的资源 protected long mToken; // 引用凭证,一般不允许外部直接访问 public override void resetProperty() { base.resetProperty(); mResource = null; mToken = 0; } public void setResource(T res) { mResource = res; if (mResource == null) { logError("resource is null"); return; } mToken = mResourceManager.addReference(mResource); } public bool isValid() { return mResource != null; } public T getResource() { return mResource; } public long getToken() { return mToken; } // 在UN_CLASS时自动被调用 public override void destroy() { base.destroy(); if (mResource == null) { logError("resource is null"); return; } mResourceManager.removeReference(mResource, ref mToken); } // 对当前资源新创建一个引用对象出来,用于使多个地方对同一个资源拥有生命周期所有权 public ResourceRef<T> copyRef() { CLASS(out ResourceRef<T> newObjRef).setResource(mResource); return newObjRef; } }这里最关键的是setResource和destroy。
加载资源时:
mToken = mResourceManager.addReference(mResource);销毁引用时:
mResourceManager.removeReference(mResource, ref mToken);所以 MyFramework 的资源引用不是简单依赖 Unity 对象本身。
它会给每一次引用生成一个 token。
多个地方使用同一个资源,可以通过copyRef()生成新的引用对象。
这和 YooAsset 的handle.Release()不一样。
YooAsset 是显式释放资源句柄。
MyFramework 是通过ResourceRef<T>接入自己的ClassObject / UN_CLASS生命周期。
这也是 MyFramework 更项目化的地方。
六、MyFramework 会定时检查引用是否为空
ResourceManager里有一段引用检查逻辑。
它会定时检查某个资源是否已经没有引用凭证。
代码如下:
if (tickTimerLoop(ref mCheckRefTimer, elapsedTime, CHECK_REF_INTERVAL)) { List<int> willRemoveList = null; foreach (var item in mReferenceTokenList) { if (item.Value.isEmpty()) { if (willRemoveList == null) { LIST(out willRemoveList); } willRemoveList.add(item.Key); } } if (willRemoveList != null) { foreach (int id in willRemoveList) { mInstanceIDToUObject.Remove(id, out UObject item); mReferenceTokenList.Remove(id); unloadInternal(item); } UN_LIST(ref willRemoveList); } }这段逻辑说明 MyFramework 的卸载不是简单“谁调用 unload 谁卸载”。
它会记录资源的引用 token。
当某个资源没有任何引用 token 时,框架会自动进入卸载流程。
这个机制是和 MyFramework 自己的对象池、ClassObject、UN_CLASS配套的。
所以这里的区别是:
YooAsset 更强调资源句柄的显式释放。
MyFramework 更强调资源引用对象接入框架生命周期。
七、MyFramework 的异步安全加载和对象生命周期绑定
MyFramework 里有一个很有项目特点的接口:
loadGameResourceAsyncSafe它的作用是:
在某个 IRecyclable 对象生命周期内加载资源,如果加载完成时对象已经被回收,就自动卸载资源并且不回调。
真实代码如下:
public CustomAsyncOperation loadGameResourceAsyncSafe<T>(IRecyclable relatedObj, string name, Action<ResourceRef<T>, string> callback, bool errorIfNull = true) where T : UObject { long assignID = relatedObj?.getAssignID() ?? 0; return loadGameResourceAsyncInternal<T>(name, (UObject asset, UObject[] _, byte[] _, string loadPath) => { if (callback == null || assignID != (relatedObj?.getAssignID() ?? 0)) { unloadInternal(asset); return; } ResourceRef<T> resRef = null; if (asset != null) { CLASS(out resRef).setResource(asset as T); } callback(resRef, loadPath); }, errorIfNull); }这段代码是 MyFramework 和通用资源系统差异比较明显的地方。
它不是只关心资源有没有加载成功。
它还关心:
加载完成时,发起加载的对象是不是还活着。
如果relatedObj已经被回收,或者assignID已经变化,说明这个对象已经不是原来的对象了。
那资源加载完成也不能继续回调。
否则就可能出现:
- 窗口已经关闭,异步图片又回调回来
- 对象已经回收到对象池,异步加载又设置到旧对象上
- 角色已经销毁,异步资源又被挂到无效对象上
- UI 节点已经复用,旧请求覆盖新显示
所以 MyFramework 这里直接做了防护:
unloadInternal(asset); return;这就是项目内资源模块的特点。
它不只是加载资源。
它要和框架对象生命周期配合。
八、MyFramework 强制资源路径规则
MyFramework 对资源路径有比较明确的要求。
checkRelativePath是这样写的:
protected static void checkRelativePath(string path) { // 需要带后缀 if (!path.Contains('.')) { logError("资源文件名需要带后缀:" + path); return; } // 不能是绝对路径 if (path.startWith(FrameBaseDefine.F_ASSETS_PATH)) { logError("不能传入绝对路径:" + path); return; } // 不能是以Assets或者Assets/GameResources开头的相对路径 if (path.startWith(FrameDefine.P_GAME_RESOURCES_PATH) || path.startWith(FrameBaseDefine.ASSETS)) { logError("不能是以Assets或者Assets/GameResources开头的相对路径:" + path); return; } }这里可以看出,MyFramework 强制要求:
- 资源路径必须带后缀
- 不能传绝对路径
- 不能以
Assets开头 - 不能以
Assets/GameResources开头 - 必须是相对于
GameResources的路径
也就是说,MyFramework 不想让业务层随便传路径。
路径规则必须统一。
这和 YooAsset 的设计不一样。
YooAsset 的官方文档里,资源定位地址location可以是完整路径,也可以在开启可寻址模式后使用可寻址地址。官方文档里还说明,不带扩展名可以模糊匹配,带扩展名是精准匹配。
MyFramework 的选择更收敛。
它不追求所有定位方式都支持。
它更希望项目里所有资源路径都保持统一规则。
这也是项目内工具和通用工具的区别。
九、编辑器下走 AssetDatabase,打包后走 AssetBundle
MyFramework 里有两个加载器:
AssetDataBaseLoaderAssetBundleLoader
编辑器下可以用AssetDataBaseLoader加载。
非编辑器环境强制走AssetBundleLoader。
AssetDataBaseLoader.loadResource真实代码如下:
public T loadResource<T>(string name) where T : UObject { string path = getFilePath(name); // 如果文件夹还未加载,则添加文件夹 var resList = mLoadedPath.getOrAddNew(path); // 资源未加载,则使用Resources.Load加载资源 if (!resList.TryGetValue(name, out AssetDataBaseLoadInfo info)) { if (!load<T>(path, name)) { return null; } // 加载后需要重新获取一次 info = resList.get(name); return info.getObject() as T; } if (info.getState() == LOAD_STATE.LOADED) { return info.getObject() as T; } else if (info.getState() == LOAD_STATE.DOWNLOADING) { logWarning("资源正在后台下载,不能同步加载!" + name); } else if (info.getState() == LOAD_STATE.LOADING) { logWarning("资源正在后台加载,不能同步加载!" + name); } else if (info.getState() == LOAD_STATE.NONE) { logWarning("资源已加入列表,但是未加载" + name); } return null; }内部真正加载时,编辑器下走loadAssetAtPath:
if (isEditor()) { string filePath = P_GAME_RESOURCES_PATH + name; if (isFileExist(filePath)) { info.setObject(loadAssetAtPath<T>(filePath)); info.setSubObjects(loadAllAssetsAtPath(filePath)); } } else { string filePath = removeSuffix(name); info.setObject(Resources.Load(filePath, typeof(T))); info.setSubObjects(Resources.LoadAll(filePath)); }这里的重点是:
MyFramework 不希望业务层关心编辑器和运行时加载差异。
业务层只传GameResources下的相对路径。
底层是AssetDatabase,还是AssetBundle,由框架决定。
十、AssetBundle 依赖关系由框架自己管理
MyFramework 里AssetBundleInfo记录了当前资源包的依赖和被依赖关系。
真实字段如下:
protected Dictionary<string, AssetBundleInfo> mChildren = new(); // 依赖自己的AssetBundle列表,即引用了自己的AssetBundle protected Dictionary<string, AssetBundleInfo> mParents = new(); // 依赖的AssetBundle列表,即自己引用的AssetBundle,包含所有的直接和间接的依赖项 protected Dictionary<UObject, AssetInfo> mObjectToAsset = new(); // 通过Object查找AssetInfo的列表 protected Dictionary<string, AssetInfo> mAssetList = new(); // 资源包中的所有资源,初始化时就会填充此列表加载 AssetBundle 时,会先加载依赖项。
同步加载资源包代码里有这一段:
foreach (var item in mParents) { item.Value.loadAssetBundle(); } mAssetBundle = AssetBundle.LoadFromFile(availableReadPath(mBundleFileName));异步加载时,也会先请求依赖项:
public void loadParentAsync() { foreach (var item in mParents) { item.Value.loadAssetBundleAsync(null); } }卸载时也会判断:
- 当前包里的资源是否还在使用
- 是否还有其他正在使用的 AssetBundle 依赖自己
canUnload是这样写的:
protected bool canUnload() { if (mLoadState != LOAD_STATE.LOADED) { return false; } // 如果资源包的资源已经没有在使用中,则卸载当前资源包 foreach (var item in mAssetList) { if (item.Value.getLoadState() != LOAD_STATE.NONE) { return false; } } // 如果已经没有资源被引用了,则卸载AssetBundle // 当前已经没有正在使用的AssetBundle引用了自己时才可以卸载 foreach (var item in mChildren) { if (item.Value.getLoadState() != LOAD_STATE.NONE) { return false; } } return true; }这说明 MyFramework 自己维护了一套 AssetBundle 依赖关系和卸载策略。
YooAsset 也有完整的依赖管理,而且官方介绍里强调了它基于资源对象的资源包依赖管理方案,可以避免资源包之间循环依赖问题。
但两者的差异在于:
YooAsset 是通用资源包依赖管理。
MyFramework 是根据自己 AssetBundle 构建规则和运行时生命周期写的项目内依赖管理。
十一、MyFramework 支持边加载边下载,但不是完整通用更新框架
MyFramework 的AssetBundleLoader也支持远端下载。
比如资源包本地不存在时,会先下载,再加载:
if (fullPath == null) { byte[] assetBundleBytes = null; yield return downloadAssetBundleCoroutine(bundleInfo, (byte[] bytes) => { assetBundleBytes = bytes; }); bundleInfo.setLoadState(LOAD_STATE.LOADING); AssetBundleCreateRequest request = AssetBundle.LoadFromMemoryAsync(assetBundleBytes); if (request != null) { yield return request; assetBundle = request.assetBundle; } }下载资源包的逻辑里,也会写入本地文件,并更新本地文件列表:
if (bytes != null && !isWebGL()) { // 写入到本地,并且更新资源列表 writeFile(F_PERSISTENT_ASSETS_PATH + bundleFileName, bytes); GameFileInfo fileInfo = new() { mFileName = bundleFileName, mFileSize = bytes.Length, mMD5 = generateFileMD5(bytes) }; mAssetVersionSystem.addPersistentFile(fileInfo); // 更新本地的文件列表 writeFileList(F_PERSISTENT_ASSETS_PATH, mAssetVersionSystem.generatePersistentAssetFileList()); }所以 MyFramework 不是完全没有远端资源能力。
它也能在资源包缺失时下载资源包,并写入本地持久化目录。
但和 YooAsset 相比,MyFramework 的这部分更偏项目内实现。
YooAsset 的资源更新能力更完整。
官方文档里提到它支持版本管理、边玩边下载、多线程下载、断点续传、自动验证下载文件、自动修复损坏文件、多功能下载器等能力。
所以这里不能说 MyFramework 替代 YooAsset。
更准确地说:
MyFramework 有自己项目需要的下载和本地文件维护逻辑。
YooAsset 提供的是更完整、更通用的资源更新体系。
十二、适合场景不同
YooAsset 更适合这些情况:
- 项目需要成熟通用资源管理框架
- 需要完整资源版本管理
- 需要完善下载器
- 需要断点续传和文件校验
- 需要多模式自由切换
- 需要资源包构建管线
- 需要可寻址资源定位
- 需要通用工具和分析器支持
- 希望资源系统独立于业务框架
MyFramework 的资源管理更适合这些情况:
- 已经使用 MyFramework
- 资源路径规则希望强制统一
- 资源生命周期要接入框架对象池
- 异步加载要和
IRecyclable生命周期绑定 - 资源引用希望接入
ResourceRef<T>和UN_CLASS - 编辑器下使用 AssetDatabase
- 打包后使用 AssetBundle
- AssetBundle 规则由项目自己控制
- 不希望业务层直接接触资源系统细节
所以两者不是同一个目标。
YooAsset 更像一套完整资源基础设施。
MyFramework 更像框架内部的一层资源调用和生命周期管理模块。
总结
MyFramework 的资源管理和 YooAsset 最大的区别,不是某个接口怎么写。
而是定位不同。
YooAsset 是一个通用 Unity 资源管理系统。
它重点解决资源构建、定位、下载、版本、缓存、卸载、多运行模式等问题。
MyFramework 的资源管理是框架内部模块。
它重点解决资源怎么进入自己的框架生命周期。
从代码上可以看到:
ResourceManager根据编辑器和运行时选择加载源loadGameResource<T>返回ResourceRef<T>,不是直接返回裸资源ResourceRef<T>通过 token 接入引用管理loadGameResourceAsyncSafe把资源异步加载和IRecyclable生命周期绑定checkRelativePath强制项目资源路径规则AssetDataBaseLoader和AssetBundleLoader分别处理编辑器和运行时加载AssetBundleInfo自己维护父依赖、子依赖、资源列表和延迟卸载
所以 MyFramework 的资源管理不是为了替代 YooAsset。
它更像是:
把资源加载变成框架生命周期的一部分。
如果项目需要成熟通用资源系统,YooAsset 是更完整的选择。
如果项目已经深度使用 MyFramework,并且希望资源加载、引用、卸载、对象池、UI、热更新流程都由框架统一控制,那 MyFramework 自己的资源管理模块会更贴合。