Unity MyFramework 用法说明(十五):使用 ResourceRef 管理资源引用与异步加载

Unity 项目中的资源加载并不只是"把资源取出来"。还需要明确资源由谁持有、什么时候释放,以及异步加载完成时原来的对象是否仍然有效。

MyFramework 使用 ResourceManagerResourceRef<T> 管理资源引用,避免资源被提前卸载或长期无人释放。

项目地址:

github.com/ZHOURUIH/My...

一、为什么不直接返回 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 的核心价值不是多包装一层,而是把"谁正在使用资源"变成框架能够追踪的信息。

只有资源引用的创建、转移和释放都清晰,资源管理器才能在避免提前卸载的同时,及时释放已经无人使用的资源。

相关推荐
SmalBox8 小时前
【节点】[ThreadMapDetail节点]原理解析与实际应用
unity3d·游戏开发·图形学
SmalBox21 小时前
【节点】[Emission节点]原理解析与实际应用
unity3d·游戏开发·图形学
_zhourui_h_1 天前
Unity MyFramework 用法说明(十四):使用 ComponentOwner 和 GameComponent 拆分对象能力
unity3d
_zhourui_h_2 天前
Unity MyFramework 用法说明(十三):使用 StateMachine 管理互斥与共存的角色状态
unity3d
SmalBox2 天前
【节点】[UnpackWaterData节点]原理解析与实际应用
unity3d·游戏开发·图形学
_zhourui_h_3 天前
Unity MyFramework 用法说明(十二):使用 DoubleBuffer 在多线程之间传递数据
unity3d
SmalBox3 天前
【节点】[PackWaterVertexData节点]原理解析与实际应用
unity3d·游戏开发·图形学
_zhourui_h_3 天前
Unity MyFramework 用法说明(十一):使用 SafeList 在遍历过程中安全增删
unity3d