一、引言

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 打包流程

  1. 资源分组:为资源指定 AssetBundleName(或用 AssetBundleBuild 结构 / Addressables 分组)。
  2. 依赖分析:递归分析资源引用,确定每个 AB 的完整依赖闭包。
  3. 序列化:将对象数据序列化为二进制,生成 TypeTree。
  4. 压缩:按所选压缩选项处理数据块。
  5. 组装:写出 UnityFS 文件(Header + BlocksInfo + Data)。
  6. 生成 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):反序列化生成的 Texture2DMeshAudioClip 等。
  • 实例对象(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 而不重复 LoadFromFileUnload 时计数减到 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);
    }
}
  • 纹理压缩格式按平台选择
平台推荐格式
AndroidASTC(首选)、ETC2
iOSASTC(首选)、PVRTC(老设备)
PC / 主机BC7 / BC3(DXT5) / DXT
WebGLASTC / 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 之上封装了自动依赖加载、引用计数、地址映射与热更新流程。