Unity 项目中的资源加载并不只是“把资源取出来”。还需要明确资源由谁持有、什么时候释放以及异步加载完成时原来的对象是否仍然有效。MyFramework 使用ResourceManager和ResourceRefT管理资源引用避免资源被提前卸载或长期无人释放。项目地址https://github.com/ZHOURUIH/MyFramework一、为什么不直接返回 Unity 资源普通资源加载通常直接返回Texture texture loadTexture(path);但仅凭一个Texture资源管理器无法判断哪些对象仍在使用它 什么时候可以卸载 多个界面是否共享它 异步回调完成时使用者是否还存在MyFramework 加载资源后返回ResourceRefTextureResourceRefT不只是包装资源它还代表一次独立的资源所有权。ResourceRef 创建 ↓ ResourceManager 增加引用凭证 ↓ 业务持有并使用资源 ↓ ResourceRef 被卸载 ↓ 移除引用凭证 ↓ 没有任何引用后释放资源二、同步加载资源同步加载接口是ResourceRefT loadGameResourceT(string name);例如读取一个文本资源using UnityEngine; using static FrameBaseHotFix; public class GuideConfig : ClassObject { protected ResourceRefTextAsset mConfigRef; public void load() { mConfigRef mResourceManager.loadGameResourceTextAsset( Config/Guide.json); if (mConfigRef null) { return; } string content mConfigRef.getResource().text; parseConfig(content); } public override void destroy() { mResourceManager?.unload(ref mConfigRef); base.destroy(); } protected void parseConfig(string content) { } }真正的 Unity 资源通过下面的接口获取TextAsset asset mConfigRef.getResource();判断引用是否有效if (mConfigRef ! null mConfigRef.isValid()) { }三、资源路径的写法传入的路径是相对于Assets/GameResources的路径并且必须带文件后缀。假设资源位于Assets/GameResources/UI/Login/Background.png正确写法UI/Login/Background.png下面几种写法都不符合框架规则Assets/GameResources/UI/Login/Background.png Assets/UI/Login/Background.png UI/Login/Background UI\\Login\\Background.png可以概括为不带 Assets/GameResources 必须带文件后缀 统一使用正斜杠编辑器中可以从 AssetDatabase 加载打包后则使用 AssetBundle但业务层调用方式不变。四、必须保存 ResourceRef而不是只保存资源下面的写法存在问题Texture texture mResourceManager .loadGameResourceTexture(UI/Login/Background.png) .getResource();这里取出了Texture却没有保存返回的ResourceRefTexture。后续无法正确归还这次引用也就破坏了框架的资源生命周期管理。正确方式是保存引用对象protected ResourceRefTexture mTextureRef; public void loadTexture() { mTextureRef mResourceManager.loadGameResourceTexture( UI/Login/Background.png); Texture texture mTextureRef?.getResource(); }不再使用时统一释放mResourceManager.unload(ref mTextureRef);unload完成后也会把变量设置为null。五、UI 节点可以接管资源所有权myUGUIRawImage.setTexture()接收的是ResourceRefTexture设置后myUGUIRawImage会持有这份资源引用mBackground.setTexture( mResourceManager.loadGameResourceTexture( UI/Login/Background.png));当再次设置其他贴图或者 UI 节点被销毁时它会释放原来的引用。因此调用setTexture()后不需要在LayoutScript中再保存同一份引用。需要清空贴图时mBackground.setTexture(null);旧资源引用也会被释放。六、一个资源被多个对象使用同一份ResourceRef不能同时交给多个生命周期不同的对象。错误示例ResourceRefTexture textureRef mResourceManager.loadGameResourceTexture( UI/Common/Background.png); mLeftBackground.setTexture(textureRef); mRightBackground.setTexture(textureRef);两个节点持有的是同一个引用对象。任意一方释放后另一方的所有权就不再可靠。需要多个对象独立持有时应创建多个引用ResourceRefTexture textureRef mResourceManager.loadGameResourceTexture( UI/Common/Background.png); mLeftBackground.setTexture(textureRef.copyRef()); mRightBackground.setTexture(textureRef.copyRef()); mResourceManager.unload(ref textureRef);底层 Unity 资源仍然是同一个但每个使用者都有独立的引用凭证同一个 Texture ├── 左侧界面的 ResourceRef └── 右侧界面的 ResourceRef左侧界面关闭时只会释放自己的引用不会影响右侧界面。七、异步加载资源普通异步加载接口mResourceManager.loadGameResourceAsyncTexture( UI/Login/Background.png, (ResourceRefTexture textureRef) { if (textureRef null) { return; } mBackground.setTexture(textureRef); });接口会返回一个CustomAsyncOperationCustomAsyncOperation operation mResourceManager.loadGameResourceAsyncTexture( UI/Login/Background.png, onTextureLoaded);它可以交给其他等待逻辑使用protected void onTextureLoaded( ResourceRefTexture textureRef) { mBackground.setTexture(textureRef); }回调收到的ResourceRef同样需要明确所有者。可以由当前对象保存也可以像上面的例子一样交给myUGUIRawImage管理。八、异步回调中的生命周期问题假设打开界面时开始异步加载mResourceManager.loadGameResourceAsyncTexture( UI/Login/Background.png, (textureRef) { mBackground.setTexture(textureRef); });资源还没有加载完成时玩家关闭了界面。随后加载完成旧回调仍然执行就可能出现访问已经销毁的界面 把资源设置给已经回收的对象 旧界面的结果覆盖新界面 加载到的资源无人释放尤其是框架对象会进入对象池同一个实例以后可能被重新分配。仅仅判断对象是否为null并不能完全解决问题。九、使用安全异步加载对于拥有明确生命周期的对象应使用loadGameResourceAsyncSafe例如在LayoutScript中mResourceManager.loadGameResourceAsyncSafeTexture( this, UI/Login/Background.png, (textureRef) { mBackground.setTexture(textureRef); });this是本次异步加载关联的对象。开始加载时框架会记录它当前的AssignID。加载完成后再次检查对象仍然是原来的分配周期 调用回调并交付 ResourceRef 对象已经销毁或被对象池重新分配 不调用回调并自动卸载资源这比单纯判断if (this null) { return; }更加可靠因为普通 C# 对象被回收后不一定为null。角色、界面、特效等可能在加载完成前被销毁的对象都应该优先使用安全接口。十、异步加载完成后不能忽略引用下面的写法会让资源引用无人管理mResourceManager.loadGameResourceAsyncTexture( UI/Login/Background.png, (textureRef) { // 什么都不做 });只要回调收到了有效的ResourceRef就必须选择一种处理方式保存到成员变量 交给其他对象接管 立即调用 unload 释放例如加载完成后发现已经不需要mResourceManager.loadGameResourceAsyncTexture( UI/Login/Background.png, (textureRef) { if (!mNeedBackground) { mResourceManager.unload(ref textureRef); return; } mBackground.setTexture(textureRef); });不要只保存getResource()返回的 Unity 对象而丢弃引用包装。十一、不要绕过 ResourceManager 卸载通过框架加载的资源不应该再直接调用Object.Destroy(resource); Resources.UnloadAsset(resource); AssetBundle.Unload(true);这些操作绕过了引用凭证可能导致仍然有人使用的资源被提前销毁 ResourceManager 中残留无效引用 共享资源的其他使用者出现丢失 资源包和实例化对象状态不一致正确方式是释放当前对象拥有的ResourceRefmResourceManager.unload(ref mTextureRef);当所有引用都被释放后由ResourceManager统一判断资源是否可以卸载。十二、总结MyFramework 的资源生命周期可以概括为加载资源 ↓ 获得 ResourceRefT ↓ 明确由谁持有 ↓ 使用 getResource() 获取 Unity 资源 ↓ 生命周期结束时 unload实际使用时主要注意五点1. 路径相对于 GameResources并且带后缀 2. 保存 ResourceRef而不是只保存 Unity 资源 3. 每一份 ResourceRef 都必须有明确所有者 4. 多个对象共享资源时使用 copyRef 5. 异步加载优先使用 loadGameResourceAsyncSafeResourceRef的核心价值不是多包装一层而是把“谁正在使用资源”变成框架能够追踪的信息。只有资源引用的创建、转移和释放都清晰资源管理器才能在避免提前卸载的同时及时释放已经无人使用的资源。