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