MyFramework:AssignID 在异步资源安全回调中的用法

ResourceManager 里有一个很小的函数:

复制代码
loadGameResourceAsyncSafe

它解决的是 Unity 项目里非常常见的异步回调问题。

资源还没加载完,发起加载的对象已经销毁,或者已经被对象池复用。

回调再执行时,访问的就可能不是原来的对象。

这个问题在 UI、角色、特效、头像、动画控制器加载中很常见。


一、普通异步加载的问题

普通异步加载大概是这样:

复制代码
mResourceManager.loadGameResourceAsync<Texture>("Icon/item.png", (res) =>
{
	mIcon.setTexture(res);
});

如果加载完成前,界面已经关闭,mIcon 可能已经销毁。

如果对象来自对象池,问题更隐蔽。

对象 A 发起加载。

对象 A 被回收。

同一个对象实例又被分配给对象 B。

资源加载完成。

旧回调执行。

结果回调把对象 B 的内容改了。

这里的问题不是空引用,而是对象实例被复用了。


二、AssignID

MyFramework 中可回收对象会实现 IRecyclable

复制代码
public interface IRecyclable
{
	public void setAssignID(long assignID);
	public long getAssignID();
}

AssignID 表示对象当前这一次分配的生命周期。

同一个对象实例可以被对象池重复使用,但每次重新分配时,AssignID 都会变化。

所以它可以区分:

复制代码
同一个 C# 对象实例
不同的一次使用生命周期

这点比简单判断 obj != null 更准确。


三、安全加载函数

ResourceManager 中的安全加载接口如下:

复制代码
// 异步加载资源,name是GameResources下的相对路径,带后缀名,errorIfNull表示当找不到资源时是否报错提示
// 在relatedObj生命周期内加载资源,如果完成加载后relatedObj已经被销毁,则会自动卸载资源并且不会调用回调
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);
}

另一个重载少了 loadPath

复制代码
public CustomAsyncOperation loadGameResourceAsyncSafe<T>(IRecyclable relatedObj, string name, Action<ResourceRef<T>> callback, bool errorIfNull = true) where T : UObject
{
	long assignID = relatedObj?.getAssignID() ?? 0;
	return loadGameResourceAsyncInternal<T>(name, (UObject asset, UObject[] _, byte[] _, string _) =>
	{
		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);
	}, errorIfNull);
}

四、关键逻辑

这个函数的核心只有两步。

1. 加载开始时记录 AssignID

复制代码
long assignID = relatedObj?.getAssignID() ?? 0;

这个值代表发起加载时对象的生命周期。

2. 回调时重新比较 AssignID

复制代码
if (callback == null || assignID != (relatedObj?.getAssignID() ?? 0))
{
	unloadInternal(asset);
	return;
}

如果两次 AssignID 不一致,说明对象已经不是原来的生命周期。

可能是:

复制代码
对象已经销毁
对象已经回收到池里
对象已经被重新分配

这时回调不再执行。

已经加载出来的资源也会立刻卸载。


五、精巧点

这个设计精巧的地方不在代码复杂,而在判断条件选得准。

很多项目会用下面几种方式处理异步回调:

复制代码
if (this == null)
{
	return;
}

或者:

复制代码
if (!gameObject.activeSelf)
{
	return;
}

这些判断都不够完整。

对象池复用时,对象可能不为空,也可能是激活状态,但它已经不是发起加载时的那个业务对象。

AssignID 直接判断生命周期。

它不关心对象是否还存在,只关心它是否还是同一次分配。


六、自动卸载

回调失效时,函数没有直接 return,而是先执行:

复制代码
unloadInternal(asset);

这点也很重要。

资源已经加载出来了,如果回调不执行,业务层就没有机会释放它。

这里由 ResourceManager 自己卸载,避免出现一次无主引用。

流程是:

复制代码
异步资源加载完成
    ↓
发现 relatedObj 生命周期已变化
    ↓
不创建 ResourceRef
    ↓
不执行业务回调
    ↓
直接卸载 asset

这样调用者不需要额外处理失效资源。


七、ResourceRef 创建时机

ResourceRef<T> 不是一开始就创建。

它只在确认回调有效后创建:

复制代码
ResourceRef<T> resRef = null;
if (asset != null)
{
	CLASS(out resRef).setResource(asset as T);
}
callback(resRef, loadPath);

原因很直接。

如果回调已经失效,就不应该再给这个资源添加正常引用。

否则资源会被包装成 ResourceRef,还需要额外回收。

当前写法更干净:

复制代码
回调有效
    创建 ResourceRef
    交给业务层持有

回调无效
    不创建 ResourceRef
    直接卸载 asset

八、适用场景

这个函数适合所有"资源加载结果依赖某个对象生命周期"的场景。

比如:

复制代码
UI 打开后异步加载图标
角色创建后异步加载 AnimatorController
头像节点异步加载 Sprite
列表项异步加载图片
特效对象异步加载材质
PrefabPool 异步加载预制体

这些对象都可能在资源加载完成前销毁或复用。

使用 loadGameResourceAsyncSafe 可以把生命周期判断收敛到资源系统里。


九、设计边界

这个函数只解决一类问题:

复制代码
发起加载的对象生命周期已经变化

它不负责取消底层加载请求。

资源仍然会继续加载完成。

加载完成后再判断回调是否有效。

这种设计的好处是实现简单,和底层加载流程耦合较少。

代价是无效请求仍然会完成一次加载。

但完成后会自动卸载,不会把无效资源继续交给业务层。


十、总结

loadGameResourceAsyncSafe 的核心设计是:

复制代码
异步开始时记录 AssignID
异步完成时再次比较 AssignID
AssignID 不一致就丢弃回调并卸载资源
AssignID 一致才创建 ResourceRef 并执行回调

它用很少的代码解决了异步资源加载中的生命周期问题。

这个设计适合 MyFramework 这种大量使用对象池和异步资源加载的框架。

对象是否还存在不是关键。

对象是否还是原来的那一次生命周期,才是关键。

github:

GitHub - ZHOURUIH/MyFramework: Unity 商用级别开发框架,经过了多年经验沉淀.一个在unity上使用的网络游戏客户端开发框架,为unity所有使用方式提供完善的封装和管理,只需要专注于游戏逻辑的编写 · GitHub

期待你的star