上一篇讲的是 AssetBundle 在运行时怎么加载、引用和卸载,但很多资源问题其实在运行之前就已经埋下了。
一个资源到底应该进哪个 Bundle?公共资源为什么莫名其妙被打进多个包?一个 UI 为什么递归依赖几十个 AB?依赖关系甚至形成环以后,又该怎么定位?
MyFramework 在打包阶段把这些问题拆成了三件事:统一生成 BundleName、构建运行时依赖数据、在编辑器里分析依赖和重复资源。
项目地址:
一、先把"资源属于哪个 AB"变成一条固定规则
MyFramework 不依赖策划或程序员手动给每个资源填写 AssetBundleName,而是根据资源路径统一生成。
核心规则集中在:
bash
protected static string generateFileAssetBundleName(
string file,
bool forceSingle = false)
{
...
}
普通资源默认按照所属目录归包。
例如:
bash
GameResources/UI/Login/Login.prefab
GameResources/UI/Login/LoginBG.png
GameResources/UI/Login/LoginButton.png
最终都会进入类似:
bash
ui/login.ab
而场景文件或者配置为强制独立打包的资源,则按照文件生成独立 Bundle:
bash
Map/Main.unity
↓
map/main.ab
最后还统一:
bash
return bundleName.ToLower();
避免不同平台文件系统大小写规则不同带来的问题。
二、不是所有资源都应该进 AssetBundle
生成 BundleName 前会直接过滤一批文件:
bash
if (file.endWith(".meta") ||
file.endWith(".DS_Store") ||
file.endWith(".cginc") ||
file.endWith(".hlsl") ||
file.endWith(".glslinc") ||
file.endWith(".tpsheet") ||
file.endWith("LightingData.asset"))
{
return EMPTY;
}
框架设置中还提供两种目录规则:
bash
UnpackFolder
完全不参与AssetBundle打包
ForceSingleFolder
目录中的资源强制单文件单Bundle
所以打包策略并不是散落在整个项目里,而是集中控制。
当某个目录从:
bash
整目录一个AB
改成:
bash
每个文件一个AB
也不需要人工逐个修改 .meta。
三、SpriteAtlas 是一个特别容易让包体膨胀的坑
正式打包前,框架会先扫描所有 SpriteAtlas,并生成 AtlasManager 使用的图集路径配置。
同时做了一件很关键的事情:
bash
atlas.SetIncludeInBuild(false);
代码里的注释已经说明了原因:如果保持 IncludeInBuild,AssetBundle 中可能出现冗余图片,导致 Bundle 异常变大。
因此流程是:
bash
开始打AB
↓
扫描所有SpriteAtlas
↓
生成Atlas路径配置
↓
SetIncludeInBuild(false)
↓
BuildAssetBundles
↓
打包结束
↓
SetIncludeInBuild(true)
这是典型的编辑器状态只为打包临时修改,结束后再恢复。
四、真正打包之前,所有 BundleName 都会重新刷新
完整打包入口不是直接:
bash
BuildPipeline.BuildAssetBundles(...);
而是先执行:
bash
Dictionary<string, BuildAssetBundleInfo>
assetBundleMap = doRefreshAllAssetBundleName();
每个 Bundle 会记录:
bash
public class BuildAssetBundleInfo
{
public List<string> mAssetNames = new();
public List<string> mDependencies = new();
public string mBundleName;
}
因此打包前框架已经知道:
bash
这个AB叫什么
里面显式有哪些Asset
然后才调用 Unity:
bash
BuildPipeline.BuildAssetBundles(
outputPath,
BuildAssetBundleOptions.StrictMode,
target);
这样 AssetBundleName 的生成规则和实际打包始终来自同一套流程。
五、Unity Manifest 用完以后,被转成自己的运行时依赖表
打包结束后,框架会加载 Unity 生成的 AssetBundleManifest。
针对每个 Bundle 获取:
bash
manifest.GetDirectDependencies(bundleName);
再把结果写回:
bash
BuildAssetBundleInfo.mDependencies
最终生成上一篇提到的 STREAMING_ASSET_FILE:
bash
serializer.write(assetBundleMap.Count);
foreach (var item in assetBundleMap)
{
BuildAssetBundleInfo info = item.Value;
serializer.writeString(info.mBundleName);
serializer.writeList(info.mAssetNames);
serializer.writeList(info.mDependencies);
}
也就是说运行时的:
bash
Asset → Bundle
Bundle → Dependencies
并不是启动游戏以后再去读取 Unity Manifest。
而是在构建阶段提前整理好,再序列化成框架自己的数据格式。
Unity 生成的 .manifest 文件随后就可以删除。
六、真正可怕的不是依赖多,而是你根本不知道为什么多
MyFramework 还提供了一个 AB依赖分析窗口。
点击"分析所有AB依赖"以后,它会直接读取当前工程:
bash
string[] allBundleNames =
AssetDatabase.GetAllAssetBundleNames();
然后分别获取:
bash
AssetDatabase.GetAssetPathsFromAssetBundle(bundleName);
AssetDatabase.GetAssetBundleDependencies(
bundleName,
false);
AssetDatabase.GetAssetBundleDependencies(
bundleName,
true);
于是一个 Bundle 同时拥有:
bash
显式包含的资源
直接依赖
A → B
递归依赖
A → B → C → D
直接被谁依赖
递归被谁依赖
这些数据最终被画成依赖节点图。
当一个 UI 包突然依赖了几十个 AssetBundle 时,不再需要对着 Manifest 一行行翻。
七、连依赖环都直接做 DFS 检测
依赖分析窗口还会把整个 Bundle 关系构造成图,然后使用 DFS 搜索依赖环。
例如:
bash
A
↓
B
↓
C
↓
A
正常资源关系中出现这种结构,基本就是一个强烈的危险信号。
工具会直接列出:
bash
A → B → C → A
并且在依赖图中标记环上的边。
这比单纯告诉你:
bash
A依赖了17个AB
更有价值,因为真正应该处理的是依赖链是怎么形成的。
八、最阴险的问题:同一个资源真的被打进了多个 AB
仅仅检查 AssetBundleName 还不够。
假设:
bash
A.ab
引用 Common.png
B.ab
也引用 Common.png
而 Common.png 自己没有被合理划到公共 Bundle 中。
最终它可能实际存在于:
bash
A.ab
B.ab
这意味着:
磁盘重复、下载重复、内存也可能重复。
MyFramework 的"检测已构建AB重复资源"不是分析工程配置,而是直接扫描已经构建完成的 AB 文件:
bash
bundle =
AssetBundle.LoadFromFile(filePath);
bundleInfo.mAssetNames.addRange(
bundle.GetAllAssetNames());
bundleInfo.mScenePaths.addRange(
bundle.GetAllScenePaths());
然后建立:
bash
Asset路径
↓
出现在哪些Bundle
只要同一路径出现于两个以上 Bundle,就进入重复资源列表。
这一点非常重要,因为它检查的是最终产物。
不管 Unity 最终为什么这么打,只要真的重复了,它就能被发现。
九、为什么依赖分析和重复检测要分开?
这两套工具解决的是完全不同的问题:
bash
AB依赖分析
看"Bundle之间是什么关系"
重复资源检测
看"最终Bundle里面到底装了什么"
前者适合找:
bash
依赖过深
公共包被大量引用
错误依赖
循环依赖
后者则专门抓:
bash
同一个资源被重复打包
包体无意义膨胀
公共资源没有正确拆包
两个一起用,才能从"配置关系"和"最终产物"两边同时检查。
十、一条完整的 AssetBundle 构建链路
最终整个过程可以压缩成:
bash
GameResources
↓
过滤不打包资源
↓
按目录 / 单资源规则生成BundleName
↓
处理SpriteAtlas
↓
BuildAssetBundles
↓
读取AssetBundleManifest
↓
生成Bundle直接依赖
↓
写入STREAMING_ASSET_FILE
↓
删除Manifest文件
↓
AB依赖图分析
↓
依赖环检测
↓
扫描最终AB
↓
重复资源检测
↓
正式发布
AssetBundle 管理真正成熟以后,关注点就不能只停留在:
"资源能不能加载出来?"
更重要的是:
"它为什么在这个包里?为什么依赖那个包?有没有重复?这个依赖关系以后会不会失控?"
运行时负责把资源安全地加载和卸载,而构建阶段负责从源头避免制造一个越来越难维护的 AssetBundle 世界。
这两边都管住,AssetBundle 才真正算是可长期维护的资源体系。