Unity MyFramework:框架内资源管理和 YooAsset 有什么区别
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 加载业务层统一调用资源异步加载结果用ResourceRefT管理资源引用释放接入框架对象池资源卸载接入框架生命周期资源路径规则强制统一AssetBundle 依赖关系由框架自己管理这就是最核心的区别。YooAsset 关注的是资源系统本身。MyFramework 关注的是资源怎么服务整个框架。二、YooAsset 的入口是 ResourcePackageYooAsset 3.0.x 中资源系统初始化后需要创建或获取ResourcePackage资源加载、下载、清理等操作都应该通过ResourcePackage实例调用。官方文档也说明 3.0 版本已经移除了默认包裹静态快捷加载接口。YooAsset 官方文档里的初始化方式大致是YooAssets.Initialize(); var package YooAssets.CreatePackage(DefaultPackage); var package YooAssets.GetPackage(DefaultPackage);资源加载也是通过 package 调用。比如官方文档里的异步加载示例AssetHandle handle package.LoadAssetAsyncAudioClip(Assets/GameRes/Audio/bgMusic.mp3); yield return handle; AudioClip audioClip handle.AssetObject as AudioClip;预制体加载示例AssetHandle handle package.LoadAssetAsyncGameObject(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.LoadAssetAsyncAudioClip(Assets/GameRes/Audio/bgMusic.mp3); yield return handle; handle.Release();这就是 YooAsset 的资源生命周期模型加载返回 Handle不用时 Release。三、MyFramework 的入口是 ResourceManagerMyFramework 里资源管理的核心入口是ResourceManager。它是一个FrameSystem。资源系统初始化时会根据当前环境决定加载源。ResourceManager.init()是这样写的public override void init() { base.init(); mLoadSource isEditor() ? GameEntryBase.getInstance().mFrameworkParam.mLoadSource : LOAD_SOURCE.ASSET_BUNDLE; if (isEditor()) { mObject.AddComponentResourcesManagerDebug(); } }这里能看出几个关键点。第一MyFramework 会判断是否在编辑器下。第二编辑器下的加载源可以通过mFrameworkParam.mLoadSource配置。第三非编辑器环境强制使用LOAD_SOURCE.ASSET_BUNDLE。也就是说MyFramework 的资源模块不是让业务层自己选择加载模式。加载模式由框架启动参数决定。业务层只需要调用资源接口。四、MyFramework 同步加载返回 ResourceRefMyFramework 的同步加载接口不是直接返回 Unity 对象而是返回ResourceRefT。loadGameResource是这样写的public ResourceRefT loadGameResourceT(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.loadResourceT(name); } else if (mLoadSource LOAD_SOURCE.ASSET_BUNDLE) { res mAssetBundleLoader.loadAssetT(name); } if (res null errorIfNull) { logError(can not find resource : name ,请确认文件存在,且带后缀名,且不能使用反斜杠\\, (name.Contains( ) || name.Contains( ) ? 注意此文件名中带有空格 : )); } if (res null) { return null; } CLASS(out ResourceRefT resRef).setResource(res); return resRef; }这段代码很能说明 MyFramework 的特点。它不是直接把UObject返回给业务层而是创建一个ResourceRefT。也就是说加载资源后框架希望外部持有的是资源引用对象而不是裸资源对象。这个设计和 YooAsset 的AssetHandle有一点类似都是希望资源生命周期有一个明确的持有者。但实现方式不一样。YooAsset 的持有者是AssetHandle。MyFramework 的持有者是ResourceRefT。五、ResourceRef 是 MyFramework 自己的引用凭证它的作用不是简单包装一下资源对象而是会向ResourceManager注册引用凭证。代码如下public class ResourceRefT : 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 ResourceRefT copyRef() { CLASS(out ResourceRefT 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 是通过ResourceRefT接入自己的ClassObject / UN_CLASS生命周期。这也是 MyFramework 更项目化的地方。六、MyFramework 会定时检查引用是否为空ResourceManager里有一段引用检查逻辑。它会定时检查某个资源是否已经没有引用凭证。代码如下if (tickTimerLoop(ref mCheckRefTimer, elapsedTime, CHECK_REF_INTERVAL)) { Listint 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 loadGameResourceAsyncSafeT(IRecyclable relatedObj, string name, ActionResourceRefT, string callback, bool errorIfNull true) where T : UObject { long assignID relatedObj?.getAssignID() ?? 0; return loadGameResourceAsyncInternalT(name, (UObject asset, UObject[] _, byte[] _, string loadPath) { if (callback null || assignID ! (relatedObj?.getAssignID() ?? 0)) { unloadInternal(asset); return; } ResourceRefT 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打包后走 AssetBundleMyFramework 里有两个加载器AssetDataBaseLoaderAssetBundleLoader编辑器下可以用AssetDataBaseLoader加载。非编辑器环境强制走AssetBundleLoader。AssetDataBaseLoader.loadResource真实代码如下public T loadResourceT(string name) where T : UObject { string path getFilePath(name); // 如果文件夹还未加载,则添加文件夹 var resList mLoadedPath.getOrAddNew(path); // 资源未加载,则使用Resources.Load加载资源 if (!resList.TryGetValue(name, out AssetDataBaseLoadInfo info)) { if (!loadT(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; }内部真正加载时编辑器下走loadAssetAtPathif (isEditor()) { string filePath P_GAME_RESOURCES_PATH name; if (isFileExist(filePath)) { info.setObject(loadAssetAtPathT(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 Dictionarystring, AssetBundleInfo mChildren new(); // 依赖自己的AssetBundle列表,即引用了自己的AssetBundle protected Dictionarystring, AssetBundleInfo mParents new(); // 依赖的AssetBundle列表,即自己引用的AssetBundle,包含所有的直接和间接的依赖项 protected DictionaryUObject, AssetInfo mObjectToAsset new(); // 通过Object查找AssetInfo的列表 protected Dictionarystring, 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生命周期绑定资源引用希望接入ResourceRefT和UN_CLASS编辑器下使用 AssetDatabase打包后使用 AssetBundleAssetBundle 规则由项目自己控制不希望业务层直接接触资源系统细节所以两者不是同一个目标。YooAsset 更像一套完整资源基础设施。MyFramework 更像框架内部的一层资源调用和生命周期管理模块。总结MyFramework 的资源管理和 YooAsset 最大的区别不是某个接口怎么写。而是定位不同。YooAsset 是一个通用 Unity 资源管理系统。它重点解决资源构建、定位、下载、版本、缓存、卸载、多运行模式等问题。MyFramework 的资源管理是框架内部模块。它重点解决资源怎么进入自己的框架生命周期。从代码上可以看到ResourceManager根据编辑器和运行时选择加载源loadGameResourceT返回ResourceRefT不是直接返回裸资源ResourceRefT通过 token 接入引用管理loadGameResourceAsyncSafe把资源异步加载和IRecyclable生命周期绑定checkRelativePath强制项目资源路径规则AssetDataBaseLoader和AssetBundleLoader分别处理编辑器和运行时加载AssetBundleInfo自己维护父依赖、子依赖、资源列表和延迟卸载所以 MyFramework 的资源管理不是为了替代 YooAsset。它更像是把资源加载变成框架生命周期的一部分。如果项目需要成熟通用资源系统YooAsset 是更完整的选择。如果项目已经深度使用 MyFramework并且希望资源加载、引用、卸载、对象池、UI、热更新流程都由框架统一控制那 MyFramework 自己的资源管理模块会更贴合。