Unity AssetBundle 打包极限治理:依赖环、重复资源、包体膨胀,怎么在上线前全部揪出来?

上一篇讲的是 AssetBundle 在运行时怎么加载、引用和卸载,但很多资源问题其实在运行之前就已经埋下了。

一个资源到底应该进哪个 Bundle?公共资源为什么莫名其妙被打进多个包?一个 UI 为什么递归依赖几十个 AB?依赖关系甚至形成环以后,又该怎么定位?

MyFramework 在打包阶段把这些问题拆成了三件事:统一生成 BundleName、构建运行时依赖数据、在编辑器里分析依赖和重复资源。

项目地址:

GitHub - ZHOURUIH/MyFramework: Unity 商用级别开发框架,经过了多年经验沉淀.一个在unity上使用的网络游戏客户端开发框架,为unity所有使用方式提供完善的封装和管理,只需要专注于游戏逻辑的编写 · GitHub

一、先把"资源属于哪个 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 才真正算是可长期维护的资源体系。

相关推荐
_zhourui_h_1 天前
Unity AssetBundle 极限管理:依赖、异步加载、引用计数、自动卸载到底怎么串起来?
unity3d
SmalBox1 天前
01-09-认知篇-对比-方案选型矩阵
unity3d·游戏开发
鑫鑫哥adam2 天前
用一个 struct 给游戏关键数值上锁:ProtectedInt 反作弊实践
unity3d
fujisheng6612 天前
FUI 导航实践:拆开 Layer、History、Coverage 与 Cache 的组合语义
c#·unity3d
SmalBox2 天前
01-08-认知篇-对比-其他资源管理方案
unity3d·游戏开发
SmalBox3 天前
01-07-认知篇-对比-原生AssetBundle工作流
unity3d·游戏开发
SmalBox4 天前
01-06-认知篇-对比-Addressable Assets深度解析
unity3d·游戏开发
fujisheng6614 天前
FUI 绑定生命周期实战:可靠解绑、失败回滚与列表项换绑
c#·unity3d
_zhourui_h_4 天前
Unity 资源热更新极限拆解:几万个文件,怎么做到只下载真正变动的那几个?
unity3d