MyFramework:AssetBundle 延迟卸载与依赖保护
2026/6/22 14:18:50 网站建设 项目流程

项目地址:

https://github.com/ZHOURUIH/MyFramework

ResourceRef<T>解决的是资源引用问题。

但资源引用清空后,AssetBundle 不能马上卸载。

因为 AssetBundle 不是单个资源。

一个 AssetBundle 可能包含多个资源,也可能被其他 AssetBundle 依赖。

所以 MyFramework 在AssetBundleInfo中做了独立的卸载判断。


一、问题

资源释放和 AssetBundle 卸载不是一回事。

资源释放只表示:

某个 UnityEngine.Object 不再被业务持有

AssetBundle 能不能卸载,还要看:

包内资源是否都释放了 是否有其他包依赖自己 当前包是否正在加载 是否允许卸载 是否已经过了延迟卸载时间

所以 MyFramework 没有在资源释放时直接卸载 AssetBundle。

它先释放资源,再由AssetBundleInfo判断整个包是否可以卸载。


二、依赖结构

AssetBundleInfo中维护两组依赖:

protected Dictionary<string, AssetBundleInfo> mChildren = new(); // 依赖自己的AssetBundle列表,即引用了自己的AssetBundle protected Dictionary<string, AssetBundleInfo> mParents = new(); // 依赖的AssetBundle列表,即自己引用的AssetBundle,包含所有的直接和间接的依赖项

含义很明确:

mParents 当前包依赖的包 mChildren 依赖当前包的包

例如:

ui_panel.ab 依赖 common_texture.ab

那么关系是:

ui_panel.ab.mParents common_texture.ab common_texture.ab.mChildren ui_panel.ab

加载时看mParents

卸载时看mChildren

两个方向都要维护。


三、依赖建立

依赖关系先从配置中读出来。

初始阶段只知道依赖包名字:

public void addParent(string dep) { mParents.TryAdd(dep, null); }

资源清单解析完成后,再把名字转换成AssetBundleInfo引用:

public void findAllDependence() { using var a = new ListScope<string>(out var tempList); foreach (string depName in tempList.setRangeKeys(mParents)) { AssetBundleInfo info = mResourceManager.getAssetBundleInfo(depName); // 找到自己的父节点 mParents.set(depName, info); // 并且通知父节点添加自己为子节点 info.addChild(this); } }

addChild()会把当前包加入父包的子依赖列表:

public void addChild(AssetBundleInfo other) { mChildren.TryAdd(other.mBundleName, other); }

这一步很关键。

如果只记录“我依赖谁”,加载没有问题。

但卸载时还需要知道“谁依赖我”。

所以mChildren是为了卸载保护存在的。


四、加载顺序

同步加载 AssetBundle 时,会先加载所有父依赖:

public void loadAssetBundle() { if (isWebGL()) { logError("webgl无法使用loadAssetBundle"); return; } if (mAssetBundle != null) { return; } if (mLoadState != LOAD_STATE.NONE) { logError("资源包正在异步加载,无法开始同步加载." + mBundleFileName); return; } // 先确保所有依赖项已经加载 foreach (var item in mParents) { item.Value.loadAssetBundle(); } mAssetBundle = AssetBundle.LoadFromFile(availableReadPath(mBundleFileName)); if (mAssetBundle == null) { logError("can not load asset bundle : " + mBundleFileName); } mLoadState = LOAD_STATE.LOADED; mWillUnloadTime = -1.0f; }

异步加载也是一样,先请求依赖包加载:

public void loadParentAsync() { foreach (var item in mParents) { item.Value.loadAssetBundleAsync(null); } }

当前包加载时必须保证依赖存在。

当前包卸载时,也必须保证没有子包还在使用自己。


五、资源卸载

单个资源卸载在unloadAsset()中处理:

public bool unloadAsset(UObject obj) { if (!mObjectToAsset.Remove(obj, out AssetInfo info)) { logError("object doesn't exist! name:" + obj.name + ", can not unload!"); return false; } // 预设类型不真正进行卸载,否则在AssetBundle内存镜像重新加载之前,无法再次从AssetBundle加载此资源 if (obj is GameObject || obj is Component) { // UObject.DestroyImmediate(obj, true); } // 其他独立资源可以使用此方式卸载,使用Resources.UnloadAsset及时卸载资源 // 可以减少Resources.UnloadUnusedAssets的耗时 else { Resources.UnloadAsset(obj); } info.clear(); if (canUnload()) { mWillUnloadTime = UNLOAD_DELAY_TIME; } return true; }

这里没有马上卸载 AssetBundle。

它只是:

移除资源到 AssetInfo 的映射 卸载独立资源 清理 AssetInfo 状态 检查当前包是否可以卸载 如果可以卸载,设置延迟卸载时间

真正的 AssetBundle 卸载放到后面。


六、canUnload

卸载判断集中在canUnload()

// 尝试卸载AssetBundle,卸载需要满足两个条件 // 当前AssetBundle内的所有资源已经没有正在使用 // 已经没有其他的正在使用的AssetBundle引用了自己 protected bool canUnload() { if (mLoadState != LOAD_STATE.LOADED) { return false; } // 如果资源包的资源已经没有在使用中,则卸载当前资源包 foreach (var item in mAssetList) { if (item.Value.getLoadState() != LOAD_STATE.NONE) { return false; } } // 如果已经没有资源被引用了,则卸载AssetBundle // 当前已经没有正在使用的AssetBundle引用了自己时才可以卸载 foreach (var item in mChildren) { if (item.Value.getLoadState() != LOAD_STATE.NONE) { return false; } } return true; }

这个函数有三个判断。


七、加载状态

第一层判断:

if (mLoadState != LOAD_STATE.LOADED) { return false; }

只有已经加载完成的包才允许进入卸载流程。

如果包正在等待加载、异步加载中,或者已经是未加载状态,就不能按普通卸载逻辑处理。

AssetBundle 卸载必须基于明确状态。


八、包内资源

第二层判断:

foreach (var item in mAssetList) { if (item.Value.getLoadState() != LOAD_STATE.NONE) { return false; } }

mAssetList记录当前包内所有资源。

如果其中任意一个AssetInfo仍然不是NONE,说明包内还有资源正在使用或处于加载状态。

这时不能卸载整个包。

这个判断保护的是:

当前包自己的资源

资源引用没有清空,包不能卸载。


九、子包依赖

第三层判断:

foreach (var item in mChildren) { if (item.Value.getLoadState() != LOAD_STATE.NONE) { return false; } }

这一步检查的是依赖当前包的其他 AssetBundle。

如果某个子包还在使用中,当前包不能卸载。

例如:

common_texture.ab 被 ui_panel.ab 依赖

即使common_texture.ab自己的资源都没有被业务直接引用,只要ui_panel.ab还在加载状态,common_texture.ab就不能卸载。

否则ui_panel.ab中的资源可能会失去依赖。

这个判断保护的是:

其他包对当前包的依赖

十、延迟卸载

AssetBundleInfo中有一个延迟时间:

protected const float UNLOAD_DELAY_TIME = 5.0f; // 没有引用时延迟5秒卸载

还有一个倒计时变量:

protected float mWillUnloadTime = -1.0f; // 引用计数变为0时的计时,小于0表示还有引用,不会被卸载,大于等于0表示计数为0,即将在一定时间后卸载

canUnload()返回 true 时,不是马上调用unload(),而是设置倒计时:

mWillUnloadTime = UNLOAD_DELAY_TIME;

update()中再倒计时:

public void update(float elapsedTime) { // 需要再次确认是否有引用 if (tickTimerOnce(ref mWillUnloadTime, elapsedTime) && canUnload()) { unload(); } }

倒计时结束后,还会再次调用canUnload()

这点很重要。

5 秒内资源可能又被重新加载。

依赖关系也可能发生变化。

所以最终卸载前必须重新检查。


十一、重新使用

资源包重新被加载或使用时,会取消卸载倒计时。

同步加载中:

mWillUnloadTime = -1.0f;

异步加载中:

public void loadAssetBundleAsync(AssetBundleCallback callback) { mWillUnloadTime = -1.0f; ... }

资源加载中:

public T loadAsset<T>(string fileNameWithSuffix) where T : UObject { mWillUnloadTime = -1.0f; ... }

异步资源加载中:

public CustomAsyncOperation loadAssetAsync(string fileNameWithSuffix, AssetLoadCallback callback, string loadPath) { mWillUnloadTime = -1.0f; ... }

含义是:

只要资源包再次被使用 就取消即将卸载状态

这可以避免刚准备卸载,下一帧又重新加载。


十二、子包卸载通知

当前包卸载后,会通知自己的父依赖:

// 通知依赖项,自己被卸载了 mParents.forValue(item => item.notifyChildUnload());

父包收到通知后,会重新检查自己能否卸载:

public void notifyChildUnload() { if (canUnload()) { mWillUnloadTime = UNLOAD_DELAY_TIME; } }

这个流程解决的是依赖链卸载。

例如:

A 依赖 B B 依赖 C

A 卸载后,会通知 B。

B 如果也没人用了,会进入延迟卸载。

B 卸载后,再通知 C。

这样依赖包不是立即被强制卸掉,而是沿依赖关系逐层检查。


十三、真正卸载

真正卸载在unload()中:

public void unload() { if (mResourceManager.isDontUnloadAssetBundle(mBundleFileName)) { return; } if (mAssetBundle != null) { // 为true表示会卸载掉LoadAsset加载的资源,并不影响该资源实例化的物体 // 只支持参数为true,如果是false,则是只卸载AssetBundle镜像,但是加载资源包中时会需要使用内存镜像 // 其他资源包中的资源引用到此资源时,也会自动从此AssetBundle内存镜像中加载需要的资源 // 所以卸载镜像,将会造成这些自动加载失败,仅在当前资源包内已经没有任何资源在使用了,并且 // 其他资源包中的资源实例没有对当前资源包进行引用时才会卸载 #if BYTE_DANCE mAssetBundle.TTUnload(true); #else mAssetBundle.Unload(true); #endif mAssetBundle = null; } mObjectToAsset.Clear(); mAssetList.forValue(item => item.clear()); mLoadState = LOAD_STATE.NONE; // 通知依赖项,自己被卸载了 mParents.forValue(item => item.notifyChildUnload()); }

这里使用的是:

mAssetBundle.Unload(true);

true表示卸载通过LoadAsset加载出来的资源。

所以前面的canUnload()必须严格。

如果仍然有资源或依赖包在使用当前包,直接Unload(true)会破坏资源关系。


十四、禁止卸载

卸载前还有一层判断:

if (mResourceManager.isDontUnloadAssetBundle(mBundleFileName)) { return; }

有些 AssetBundle 可以被标记为不卸载。

这适合常驻资源包。

例如:

基础字体 公共材质 常用 Shader 通用 UI 资源 启动阶段常驻资源

这些资源频繁使用,卸载反而会造成重复加载。


十五、异步加载保护

异步加载完成时,也有状态保护:

public void notifyAssetBundleAsyncLoaded(AssetBundle assetBundle) { mAssetBundle = assetBundle; if (mLoadState != LOAD_STATE.NONE) { mLoadState = LOAD_STATE.LOADED; // 异步加载请求的资源 foreach (AssetInfo item in mLoadAsyncList) { mAssetList.get(item.getAssetName()).loadAssetAsync(); } } // 加载状态为已卸载,表示在异步加载过程中,资源包被卸载掉了 else { logWarning("资源包异步加载完成,但是异步加载过程中被卸载"); unload(); } mLoadAsyncList.Clear(); using var a = new ListScope<AssetBundleCallback>(out var callbacks); foreach (AssetBundleCallback callback in mLoadCallbackList.moveTo(callbacks)) { callback(this); } }

如果异步加载过程中包已经被卸载,完成后不会继续当作正常包使用。

这里直接走unload()

这避免了异步加载和卸载状态交叉时,包重新进入错误状态。


十六、设计重点

这个机制的重点不是“引用计数”。

而是三层保护:

资源保护 包内 AssetInfo 都为空,才允许卸载 依赖保护 没有正在使用的子包依赖自己,才允许卸载 时间保护 满足条件后延迟 5 秒,再次确认后才卸载

这三层缺一不可。

只看资源引用,依赖包可能出错。

只看依赖关系,包内资源可能还在用。

满足条件后马上卸载,又可能造成短时间内频繁加载和卸载。


十七、设计取舍

这套设计的优点:

不会因为单个资源释放就误卸载整个包 不会卸载仍被其他包依赖的公共包 减少频繁 Load / Unload 支持依赖链逐层释放 卸载前再次确认状态

代价也很明确:

AssetBundle 不会在资源释放后立刻消失 内存释放有 5 秒延迟 依赖关系需要在初始化阶段完整建立 卸载逻辑比简单引用计数更复杂

这个取舍适合长期项目。

资源系统更稳定,内存回收不追求立即发生。


总结

MyFramework 中 AssetBundle 卸载流程大致是:

ResourceRef 释放 ↓ 资源引用清空 ↓ ResourceManager 卸载单个资源 ↓ AssetInfo 清理状态 ↓ AssetBundleInfo.canUnload() ↓ 满足条件后设置 5 秒延迟 ↓ update 中倒计时 ↓ 再次 canUnload() ↓ AssetBundle.Unload(true) ↓ 通知父依赖重新检查

ResourceRef判断的是资源是否还被业务持有。

AssetBundleInfo判断的是整个资源包是否可以安全卸载。

canUnload()同时检查包内资源和子包依赖。

UNLOAD_DELAY_TIME避免资源刚释放就马上卸载。

这就是 MyFramework 中 AssetBundle 延迟卸载与依赖保护的主要设计。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询