一、引言
AssetBundle(简称 AB 包)是 Unity 资源管理的基石。理解它的底层原理,是掌握后续所有资源管理方案(包括 Addressables 和 YooAsset)的前提。
AssetBundle 是 Unity 提供的一种将资源打包成自定义格式文件的机制。通过它,开发者可以把资源从游戏安装包中分离出来,实现按需加载、热更新、资源共享等功能。Unity 从 5.x 起提供了较完善的 AssetBundle 打包与加载 API。
它的核心价值在于将资源与代码分离,使资源可以在运行时动态加载和卸载。对于体量较大的项目尤为重要:把资源拆成多个小包,按游戏进度按需加载,从而控制安装包体积和运行时内存占用。
二、AssetBundle 内部结构
2.1 文件结构(UnityFS 格式)
现代 Unity 使用的 AB 文件格式标识为 UnityFS。一个 AB 文件在磁盘上大致分为如下几部分:
AssetBundle 文件(UnityFS)
├── Header(文件头)
│ ├── 签名(Signature):"UnityFS"
│ ├── 格式版本(Format Version)
│ ├── Unity 版本 / 生成版本字符串
│ ├── 文件总大小
│ ├── 压缩后 / 压缩前 BlocksInfo 大小
│ └── 压缩标志(LZMA / LZ4 / 无压缩)
├── BlocksInfo & DirectoryInfo(块与目录信息,通常单独压缩)
│ ├── 数据块(Block)列表:每块的压缩/解压大小、压缩方式
│ └── 目录(Node)列表:内部文件名、offset、size、flags
└── Data(数据块)
├── CAB-xxxxx(SerializedFile:序列化对象数据 + TypeTree)
└── CAB-xxxxx.resS / .resource(Texture、Mesh、Audio 等原始二进制)
各部分说明:
- Header:文件元数据。签名
UnityFS用于识别有效 AB;压缩标志决定BlocksInfo与数据块的压缩算法。 - BlocksInfo / DirectoryInfo:AB 的「索引」。
BlocksInfo描述每个数据块的压缩情况,DirectoryInfo描述包内各内部文件(Node)的名字与偏移,Unity 借此快速定位资源,无需扫描整个文件。 - SerializedFile(CAB-*):核心数据。存放对象的序列化数据、对象之间的内部引用(用
FileID/PathID表示),以及 TypeTree(类型树,描述每个对象各字段的名称/类型/偏移,用于跨版本正确反序列化)。 - .resS / .resource:Texture、Mesh、AudioClip 等大块原始二进制通常分离到独立数据流中,便于流式读取。
注意:跨包依赖信息不在单个 AB 内部。常见说法会把「依赖 Bundle 列表 + CRC」画进单个 AB 的结构里,这并不准确。
- AB 内部存的是自身对象及其内部引用(
PathID),以及指向其他 AB 中对象的外部引用(FileID→ 外部文件名)。 - 跨包依赖关系集中记录在打包时生成的 Manifest(
AssetBundleManifest) 中,运行时通过manifest.GetAllDependencies(bundleName)/GetDirectDependencies查询。 - CRC 校验值同样记录在
.manifest文本文件里,用于校验,而非塞在每个 AB 的头部。
2.2 三种压缩方式
| 压缩方式 | 压缩率 | 解压/加载速度 | 随机访问 | 典型场景 |
|---|---|---|---|---|
| LZMA | 最高 | 慢 | 不支持(整包流式,需整体解压) | 网络传输、首次下载 |
| LZ4 (Chunk-Based) | 中等 | 快 | 支持(按块解压) | 运行时加载、本地存储(推荐) |
| 无压缩(Uncompressed) | 无 | 最快 | 支持 | 性能极敏感、包体不敏感的场景 |
LZMA:基于字典的高压缩率算法(Lempel-Ziv-Markov chain)。压缩率最高,但采用整体流式压缩,加载时需要把整包解压到内存,且不支持按需读取单个资源。Unity 中 LZMA 包首次加载通常会被重新解压/重压缩缓存为 LZ4 形式(例如 LoadFromCacheOrDownload / WebRequest 缓存路径),以便后续快速访问。适合网络下发(下载小)。
LZ4(分块压缩):Unity 运行时的推荐格式。按固定大小的 chunk 分块压缩,支持只解压当前需要的块,因此可以做到「加载哪个资源就解压哪一块」,内存占用可控、加载快。压缩率适中。BuildAssetBundleOptions.ChunkBasedCompression 即选择该模式。
无压缩:直接读取,加载最快,但包体最大。
注意:原生 BuildAssetBundleOptions 只提供 LZMA / LZ4(ChunkBased) / Uncompressed 三档。「LZ4HC」是 LZ4 算法家族里的高压缩变体概念,并非 Unity AB 打包选项,不要在讲 AB 打包设置时把它当成第四种可选项。
2.3 序列化机制
Unity 使用自定义的二进制序列化格式存储对象数据,特点:
- 跨平台:序列化格式与平台无关,可在不同平台间交换(但压缩后的 AB 文件本身按平台构建,不能跨平台混用)。
- 类型安全:通过 TypeTree 保留字段结构,反序列化时正确重建对象。
- 引用管理:对象间引用用
FileID + PathID表达,自动维护指向关系。 - 紧凑高效:二进制相比 JSON/XML 体积更小、解析更快。
三、打包机制
3.1 打包流程
- 资源分组:为资源指定
AssetBundleName(或用AssetBundleBuild结构 / Addressables 分组)。 - 依赖分析:递归分析资源引用,确定每个 AB 的完整依赖闭包。
- 序列化:将对象数据序列化为二进制,生成 TypeTree。
- 压缩:按所选压缩选项处理数据块。
- 组装:写出 UnityFS 文件(Header + BlocksInfo + Data)。
- 生成 Manifest:为每个 AB 生成
.manifest,并生成描述整体依赖关系的总 Manifest。
对应的核心 API:
// 5.x ~ 现代版本的原生打包入口
BuildPipeline.BuildAssetBundles(
outputPath,
BuildAssetBundleOptions.ChunkBasedCompression, // LZ4,推荐
BuildTarget.StandaloneWindows64);
3.2 依赖分析原理
依赖分析是打包的核心。Unity 递归分析资源间的引用,确保被依赖的资源都被正确纳入。编辑器下可用 AssetDatabase.GetDependencies 查看依赖:
// 递归收集依赖(去重)
public static List<string> AnalyzeDependencies(string assetPath)
{
var result = new List<string>();
var visited = new HashSet<string>();
Visit(assetPath, result, visited);
return result;
}
static void Visit(string path, List<string> result, HashSet<string> visited)
{
if (!visited.Add(path)) return; // 已访问则跳过,天然处理环
// recursive:false 只拿直接依赖,自己控制递归
foreach (var dep in AssetDatabase.GetDependencies(path, recursive: false))
{
if (dep == path) continue;
Visit(dep, result, visited);
}
result.Add(path);
}
关键点:
- 递归 + 去重:用
HashSet避免重复分析,也避免因为资源相互引用而无限递归。 - 共享资源:若一个资源被多个 AB 引用又没有被单独分包,它会被冗余打进每个 AB,导致重复、内存浪费。应把公共资源单独分包。
依赖图示例:
Bundle A: [Prefab1]
Bundle B: [Prefab2]
Bundle C: [Prefab3]
Bundle Shared: [Texture1, Material1] // 公共资源单独分包
Prefab1 -> Texture1
Prefab2 -> Texture1, Material1
Prefab3 -> Material1
Material1 -> Texture1
结论:A、B、C 都依赖 Shared;加载它们前需先加载 Shared。
3.3 打包策略
| 策略 | 描述 | 优点 | 缺点 |
|---|---|---|---|
| 单资源单包 | 每个资源一个 AB | 更新粒度最细 | 包数量爆炸,加载请求多 |
| 按目录分包 | 同目录资源一个 AB | 粒度适中、易管理 | 更新灵活性一般 |
| 按类型分包 | 同类资源一个 AB | 便于类型管理 | 易造成跨类共享资源冗余 |
| 按场景分包 | 每场景一个 AB | 场景加载方便 | 场景公共依赖需额外抽取 |
选择建议:
- 按模块分包:适合大型项目,模块间耦合低。
- 按更新频率分包:频繁热更的资源单独分,减少更新下发体积。
- 公共资源单独分包:这是避免冗余最重要的一条。
四、加载流程
4.1 加载 API
优先用 LoadFromFile,而不是 LoadFromMemory。不少资料把三种「加载方式」都写成 LoadFromMemoryAsync,这是错的,也是性能陷阱。实际应区分:
// 推荐:从文件直接加载(内部走内存映射,托管内存占用最低)
AssetBundle bundle = AssetBundle.LoadFromFile(path);
// 异步版本
AssetBundleCreateRequest req = AssetBundle.LoadFromFileAsync(path);
yield return req;
AssetBundle bundle = req.assetBundle;
// 慎用:LoadFromMemory 会把整包读入托管内存,占用翻倍,仅在拿到的是内存字节流时使用
AssetBundle bundle = AssetBundle.LoadFromMemory(bytes);
// 网络下载 + 缓存(会把 LZMA 重压为 LZ4 缓存到本地)
using var uwr = UnityWebRequestAssetBundle.GetAssetBundle(url, version, crc);
yield return uwr.SendWebRequest();
AssetBundle bundle = DownloadHandlerAssetBundle.GetContent(uwr);
从 AB 中取资源、实例化:
// 同步取资源
GameObject prefab = bundle.LoadAsset<GameObject>("MyPrefab");
// 异步取资源
AssetBundleRequest ar = bundle.LoadAssetAsync<GameObject>("MyPrefab");
yield return ar;
GameObject prefab = ar.asset as GameObject;
// 实例化
GameObject instance = Object.Instantiate(prefab);
常用 API 一览:
AssetBundle.LoadFromFile / LoadFromFileAsync:本地首选。AssetBundle.LoadFromMemory / LoadFromMemoryAsync:从字节流加载,内存占用高,慎用。AssetBundle.LoadFromStream / LoadFromStreamAsync:从流加载,可配合自定义解密。LoadAsset<T>/LoadAssetAsync<T>/LoadAllAssets<T>/LoadAssetWithSubAssets:取资源。AssetBundle.Unload(bool):卸载 AB。
最佳实践:优先异步加载避免卡主线程;每次加载检查返回值防空引用;不用时及时卸载;用自建引用计数管理生命周期(见 5.2)。
4.2 依赖加载机制
原生 AB 不会「自动」加载依赖。常见误解是「加载一个 AB,Unity 会自动把依赖 AB 也加载好」。原生 AssetBundle 并不会自动加载依赖包——必须先手动加载所有依赖 AB,被依赖包中的资源才能被正确解析。Addressables / YooAsset 之所以「看起来自动」,是它们在上层封装帮你做了这件事。
正确做法是借助 Manifest 手动加载依赖:
// 1. 先加载总 Manifest 包
var manifestBundle = AssetBundle.LoadFromFile(manifestPath);
var manifest = manifestBundle.LoadAsset<AssetBundleManifest>("AssetBundleManifest");
// 2. 查询并先加载所有依赖
foreach (string dep in manifest.GetAllDependencies("player.ab"))
{
AssetBundle.LoadFromFile(Path.Combine(root, dep));
}
// 3. 再加载目标包,此时内部引用可被正确解析
var target = AssetBundle.LoadFromFile(Path.Combine(root, "player.ab"));
注意事项:
- 依赖 AB 必须先于被依赖 AB 加载完成。
- 依赖包缺失 → 资源引用变成丢失(表现为材质变紫、Mesh 丢失等)。
- 循环依赖:Unity 打包能处理资源级引用环,但应从设计上避免包与包之间的循环依赖,否则卸载时机难以管理。
五、内存管理
5.1 内存构成
加载 AB 后,内存里大致有几类占用:
- AB 头/索引:Header、BlocksInfo、TypeTree 等元数据。
- AB 内的资源数据:尚未反序列化 / 正在被引用的序列化数据(LZ4 时按块解压)。
- 资源对象(Asset):反序列化生成的
Texture2D、Mesh、AudioClip等。 - 实例对象(Instance):
Instantiate出来的GameObject及其组件。
5.2 卸载与引用计数
// 卸载 AB
bundle.Unload(false); // 只卸载 AB 结构,已加载出的资源对象保留
bundle.Unload(true); // 连同从该 AB 加载出的资源对象一起卸载(可能导致正在使用的对象失效)
Unload(false) vs Unload(true):
Unload(false):释放 AB 的压缩数据/索引,但保留已LoadAsset出来的资源对象。若之后还想再LoadAsset同名资源,会重新加载出一份新副本(可能导致重复)。Unload(true):把该 AB 派生的所有资源对象一并销毁。若这些对象正被场景使用,会立刻失效(贴图变紫等)。一般在切换场景、确定不再使用时调用。
注意:AB 句柄没有自动引用计数。原生 API 中,对同一个 AB 文件重复 LoadFromFile 不会「引用计数 +1」,而是直接报错:
The AssetBundle 'xxx' can't be loaded because another AssetBundle with the same files is already loaded.
因此「引用计数」必须由上层自行封装:维护 Dictionary<string, (AssetBundle, int)>,加载时若已存在则计数 +1 而不重复 LoadFromFile,Unload 时计数减到 0 才真正卸载。Unity 底层只对资源对象有引用追踪(决定 UnloadUnusedAssets 能否回收),对 AB 句柄本身没有内置引用计数。
清理未使用的资源对象:
// 卸载没有任何引用的资源对象(较重的操作,建议在 Loading 时机调用)
AsyncOperation op = Resources.UnloadUnusedAssets();
yield return op;
内存管理关键点:自建 AB 引用计数管理生命周期;选好卸载时机;用 Profiler / Memory Profiler 排查泄漏;控制内存峰值防 OOM。
5.3 优化技巧
- 公共资源单独分包:从源头避免重复加载(配合 5.2 的引用计数)。
- 对象池:对频繁创建/销毁的实例复用,减少 GC 与实例化开销。
public class ObjectPool
{
readonly Queue<GameObject> _pool = new();
readonly GameObject _prefab;
public ObjectPool(GameObject prefab) => _prefab = prefab;
public GameObject Get() =>
_pool.Count > 0 ? _pool.Dequeue() : Object.Instantiate(_prefab);
public void Release(GameObject go)
{
go.SetActive(false);
_pool.Enqueue(go);
}
}
- 纹理压缩格式按平台选择:
| 平台 | 推荐格式 |
|---|---|
| Android | ASTC(首选)、ETC2 |
| iOS | ASTC(首选)、PVRTC(老设备) |
| PC / 主机 | BC7 / BC3(DXT5) / DXT |
| WebGL | ASTC / ETC2(视目标而定) |
注意:UI 图集不建议用 PVRTC。PVRTC 要求纹理为 2 的幂次方且正方形,UI 图集通常不满足;且 PVRTC 在文字/UI 边缘容易出块状伪影。UI 更推荐 ASTC(现代设备普遍支持,块大小可调)。PVRTC 仅在需要兼容非常老的 iOS 设备时才考虑。
六、常见问题与调试
6.1 常见问题
- 材质变紫 / Mesh 丢失:多为依赖 AB 未先加载,或 Shader 未被正确纳入包(可用一个「Shader 变体/公共」包统一管理)。
- 内存泄漏:AB 或资源未正确卸载。用自建引用计数 +
Unload(false)+ 适时Resources.UnloadUnusedAssets()。 - 加载慢:包过大或用了 LZMA。改用 LZ4(ChunkBased)、拆分大包、走异步加载。
- 同名 AB 重复加载报错:见 5.2,上层用引用计数缓存句柄。
- 资源重复占内存:
Unload(false)后又重新LoadAsset,产生多份副本;用引用计数统一取用。
6.2 调试工具
- Unity Profiler / Memory Profiler:监控内存、加载耗时、GC 分配、资源重复。
.manifest文本文件:直接查看每个 AB 的依赖列表与 CRC。- AssetBundle Browser(官方工具,已较老):查看包内资源与依赖关系。
- 自定义日志:在加载/卸载处打点,定位加载顺序问题。
七、总结
| 主题 | 原生 AssetBundle 的关键结论 |
|---|---|
| 文件格式 | UnityFS:Header + BlocksInfo/Directory + Data(SerializedFile + resS) |
| 压缩 | 仅 LZMA / LZ4(ChunkBased) / 无压缩三档;运行时推荐 LZ4 |
| 依赖 | 跨包依赖记录在 Manifest;依赖需手动先加载,原生不自动 |
| 加载 API | 本地首选 LoadFromFile(Async),慎用 LoadFromMemory |
| 卸载 | Unload(false/true) 语义不同;AB 句柄无内置引用计数,需自行封装 |
| 优化 | 公共资源分包、对象池、按平台选纹理压缩(UI 用 ASTC 而非 PVRTC) |
理解原生 AssetBundle 的结构、打包、加载与内存模型,是后续学习 Addressables 和 YooAsset 的基础——这两者本质上都是在原生 AB 之上封装了自动依赖加载、引用计数、地址映射与热更新流程。
