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:
期待你的star