AssetBundle 真正难的从来不是这一句:
bash
AssetBundle.LoadFromFile(path);
而是项目规模变大以后随之而来的问题:
资源在哪个 Bundle?依赖包谁先加载?多个地方同时请求怎么办?远端资源没下载怎么办?什么时候才能安全卸载?一个公共依赖又怎么避免被提前卸掉?
MyFramework 的 ResourceManager + AssetBundleLoader + AssetBundleInfo + AssetInfo + ResourceRef,就是围绕这些问题组成了一套完整的 AssetBundle 生命周期管理。
项目地址:
一、业务层根本不关心资源在哪个 AssetBundle
业务加载资源时只写:
bash
ResourceRef<GameObject> prefab =
mResourceManager.loadGameResource<GameObject>(
"UI/Login.prefab");
传进去的是 GameResources 下的资源路径,而不是 Bundle 名。
ResourceManager 在编辑器和正式包中还可以使用不同加载源:
bash
mLoadSource = isEditor() ?
GameEntryBase.getInstance().mFrameworkParam.mLoadSource :
LOAD_SOURCE.ASSET_BUNDLE;
所以编辑器可以直接走 AssetDatabase,打包后则强制进入 AssetBundle。
上层代码完全不用分两套。
二、启动时先建立"资源 → Bundle"索引
AssetBundle 系统初始化时首先读取:
bash
STREAMING_ASSET_FILE
这个文件保存了每个 Bundle 的:
bash
Bundle名字
包含的所有Asset
依赖的Bundle
解析以后建立:
bash
Dictionary<string, AssetBundleInfo> mAssetBundleInfoList;
Dictionary<string, AssetInfo> mAssetToBundleInfo;
因此以后给出:
bash
UI/Login.prefab
就可以直接定位到:
bash
AssetInfo
↓
AssetBundleInfo
↓
真正的AssetBundle
不需要运行时再扫描 Manifest 或遍历所有 Bundle。
三、依赖关系不是加载时临时查的
每个 AssetBundleInfo 同时保存:
bash
Dictionary<string, AssetBundleInfo> mParents;
Dictionary<string, AssetBundleInfo> mChildren;
其中:
bash
Parents
当前Bundle依赖了谁
Children
谁依赖了当前Bundle
初始化结束以后统一执行:
bash
bundleInfo.findAllDependence();
把依赖名字真正解析成 AssetBundleInfo,同时反向建立 Children。
这样一张依赖图在游戏开始前就已经建立完成。
四、加载自己之前,先把所有依赖拉起来
同步加载时非常直接:
bash
foreach (var item in mParents)
{
item.Value.loadAssetBundle();
}
mAssetBundle =
AssetBundle.LoadFromFile(
availableReadPath(mBundleFileName));
异步也是同样原则。
先触发:
bash
loadParentAsync();
真正轮到当前 Bundle 加载时,再等待:
bash
while (!bundleInfo.isAllParentLoaded())
{
yield return null;
}
所以:
bash
角色Bundle
├─ 材质Bundle
├─ ShaderBundle
└─ 公共贴图Bundle
不会出现角色已经开始加载,依赖包却还没准备好的情况。
五、本地没有 Bundle?加载流程直接接上下载
异步加载时首先查询:
bash
string fullPath =
availableReadPath(bundleFileName);
这个函数会结合前面讲过的资源版本系统,判断 PersistentData 或 StreamingAssets 中有没有当前可用版本。
如果返回 null:
bash
本地没有最新版Bundle
↓
从服务器下载
↓
写入PersistentData
↓
更新本地FileList
↓
LoadFromMemoryAsync
有本地文件则直接:
bash
AssetBundle.LoadFromFileAsync(fullPath);
所以 AssetBundle 管理和上一篇的资源热更新并不是两套独立系统。
热更新决定文件在哪里,AssetBundleLoader 决定怎么把它加载进内存。
六、十个地方同时请求,不会加载十遍
一个 Bundle 正在异步加载时,再次请求不会重新开始加载,而是加入:
bash
mLoadCallbackList
下载也是一样。
如果当前状态已经是:
bash
LOAD_STATE.DOWNLOADING
新请求只加入:
bash
mDownloadCallbackList
单个 Asset 也维护自己的:
bash
List<AssetLoadCallback> mCallback;
所以最终结构其实是:
bash
请求A ┐
请求B ├─→ 同一个Bundle加载任务
请求C ┘
↓
加载完成
↓
一次性通知全部回调
这对 UI、角色、特效同时依赖公共资源的场景非常重要。
七、Bundle 加载完成,不代表资源也全部加载
MyFramework 没有在加载 Bundle 后马上把内部所有资源都实例化出来。
真正访问:
bash
UI/Login.prefab
时才调用:
bash
LoadAssetWithSubAssets(...)
异步则:
bash
LoadAssetWithSubAssetsAsync(...)
如果资源请求到来时 Bundle 还没加载完成,它会先进入:
bash
mLoadAsyncList
等 Bundle 完成以后再自动触发这些 Asset 的异步加载。
因此是两层生命周期:
bash
AssetBundle
↓
按需加载Asset
而不是"加载一个 Bundle 就把里面所有东西全塞进内存"。
八、资源生命周期不是靠业务猜,而是 ResourceRef
加载成功返回的不是裸 UnityEngine.Object,而是:
bash
ResourceRef<T>
设置资源时:
bash
mToken =
mResourceManager.addReference(mResource);
每一个引用对象都会拿到唯一 Token。
复制一份所有权:
bash
ResourceRef<T> newRef =
oldRef.copyRef();
又会产生新的 Token。
释放:
bash
mResourceManager.unload(ref prefab);
最终 ResourceRef.destroy() 会:
bash
mResourceManager.removeReference(
mResource,
ref mToken);
所以真正管理的不是一句简单的:
bash
引用计数++
引用计数--
而是每个持有者都有独立引用凭证,还能检测重复释放等异常。
九、资源没引用了,也不会立刻把 Bundle 干掉
ResourceManager 每隔 3 秒检查一次资源引用。
某个资源已经没有任何 Token 时,才真正卸载 Asset。
然后 AssetBundleInfo 再判断整个 Bundle 是否满足:
bash
自己的所有Asset都已经不用了
+
没有仍在使用的其他Bundle依赖自己
核心判断就是:
bash
foreach (var item in mAssetList)
{
if (item.Value.getLoadState() != LOAD_STATE.NONE)
{
return false;
}
}
foreach (var item in mChildren)
{
if (item.Value.getLoadState() != LOAD_STATE.NONE)
{
return false;
}
}
这解决了公共依赖最容易出现的问题:
bash
A依赖Common
B也依赖Common
A释放
↓
Common不能卸
因为B还在使用
十、满足卸载条件后,还要再等5秒
即使 canUnload() 已经返回 true,也不会立即:
bash
AssetBundle.Unload(true);
而是设置:
bash
mWillUnloadTime = 5.0f;
五秒以后再次确认仍然可以卸载,才真正释放。
这是为了避免:
bash
打开UI
↓
关闭UI
↓
Bundle立即卸载
↓
1秒后又打开UI
↓
Bundle重新加载
这种高频抖动。
某些常驻资源包还可以直接加入:
bash
addDontUnloadAssetBundle(...)
彻底禁止自动卸载。
十一、一套 AssetBundle 管理最终在管理什么?
整个生命周期可以浓缩成:
bash
资源路径
↓
Asset索引
↓
找到AssetBundle
↓
递归准备依赖
↓
本地存在?
├─ Yes → LoadFromFileAsync
└─ No → 下载 → LoadFromMemoryAsync
↓
Bundle加载完成
↓
按需LoadAsset
↓
ResourceRef持有引用
↓
引用全部释放
↓
检查其他Bundle是否仍然依赖
↓
等待5秒
↓
再次确认
↓
AssetBundle.Unload(true)
所以一个真正完整的 AssetBundle 管理器,核心并不是"把 AB 加载出来"。
它真正管理的是:
资源定位、依赖图、并发请求合并、远端下载、Asset 按需加载、引用生命周期以及安全卸载。
只有把这整条链路都管住,AssetBundle 才真正从一个 Unity API,变成项目里可以长期稳定使用的资源系统。