项目地址https://github.com/ZHOURUIH/MyFrameworkResourceRefT解决的是资源引用问题。但资源引用清空后AssetBundle 不能马上卸载。因为 AssetBundle 不是单个资源。一个 AssetBundle 可能包含多个资源也可能被其他 AssetBundle 依赖。所以 MyFramework 在AssetBundleInfo中做了独立的卸载判断。一、问题资源释放和 AssetBundle 卸载不是一回事。资源释放只表示某个 UnityEngine.Object 不再被业务持有AssetBundle 能不能卸载还要看包内资源是否都释放了 是否有其他包依赖自己 当前包是否正在加载 是否允许卸载 是否已经过了延迟卸载时间所以 MyFramework 没有在资源释放时直接卸载 AssetBundle。它先释放资源再由AssetBundleInfo判断整个包是否可以卸载。二、依赖结构AssetBundleInfo中维护两组依赖protected Dictionarystring, AssetBundleInfo mChildren new(); // 依赖自己的AssetBundle列表,即引用了自己的AssetBundle protected Dictionarystring, 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 ListScopestring(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 doesnt 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 loadAssetT(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 依赖 CA 卸载后会通知 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 ListScopeAssetBundleCallback(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 延迟卸载与依赖保护的主要设计。