AssetBundle 系统
Unity AssetBundle 系统 - 资源打包与运行时加载
一、什么是 AssetBundle
AssetBundle(简称 AB)是 Unity 提供的一种资源打包与加载机制,用于将资源(模型、贴图、材质、预制体、音频、Shader、场景等)从主包中分离出来,打包成独立的、可在运行时按需加载的文件。
核心用途
- 减小首包体积:把非必需资源从安装包中剥离,做成 AB 放到服务器,运行时下载。
- 热更新(资源热更):不发新版本 App,直接更新服务器上的 AB 即可替换游戏资源。
- 按需加载 / 内存管理:需要时才加载资源到内存,不用时卸载,控制内存峰值。
- DLC / 分包:把不同关卡、不同角色、活动内容做成独立 AB,动态下发。
与其他资源方式的关系
| 方式 | 说明 |
|---|---|
| Resources 文件夹 | 简单但全部打进包体、无法热更、加载慢,官方不推荐大量使用 |
| StreamingAssets | 原样存放的文件,可读但不参与 Unity 资源管理 |
| AssetBundle | 可热更、可按需加载,是中大型项目的主流方案 |
| Addressables | 建立在 AB 之上的高级封装,官方现在主推 |
二、AssetBundle 的基本结构
一个 AssetBundle 文件在物理上包含两部分:
┌─────────────────────────────────┐
│ AssetBundle 文件 │
│ ┌───────────────────────────┐ │
│ │ Header(头信息) │ │ ← 标识、压缩方式、Manifest 等
│ ├───────────────────────────┤ │
│ │ Data Segment(数据段) │ │ ← 序列化后的资源数据
│ │ - 序列化对象 │ │
│ │ - 资源数据(贴图/网格等)│ │
│ └───────────────────────────┘ │
└─────────────────────────────────┘两类内容
- Serialized File(序列化文件):把资源对象序列化后合并存储,是 AB 的主体。
- Resource files(.resS / .resource):贴图、音频等大块二进制数据单独存放,便于流式读取。
三、AssetBundle 的依赖关系
AB 系统最重要也最容易出错的部分。
依赖是如何产生的
当一个资源引用了另一个资源,而两者被打进不同的 AB 时,就产生了跨 AB 依赖。
例如:
prefab.ab ──依赖──► material.ab ──依赖──► texture.ab
(预制体) (材质) (贴图)如果预制体用了某材质,材质用了某贴图,而它们分属三个 AB,那么加载 prefab.ab 前,必须先加载它依赖的 material.ab 和 texture.ab,否则预制体会丢失材质(表现为洋红色 / 粉色)。
冗余打包问题
如果一个共享资源(如公用贴图)没有被显式指定 AB,那么每个引用它的 AB 都会各自拷贝一份,导致:
- 包体膨胀(同一张贴图打包多次)。
- 内存冗余(加载多个 AB 时内存里有多份相同资源)。
解决方案:把公共资源单独打成一个 AB(如 shared_textures.ab),让其他 AB 依赖它。
Manifest 文件
打包后,Unity 会生成 .manifest 文件(文本)和一个总的 AssetBundleManifest,记录:
- 每个 AB 的 CRC / Hash(用于版本比对、增量更新)。
- 每个 AB 的依赖列表(运行时据此加载依赖)。
ManifestFileVersion: 0
CRC: 2839472
Assets:
- Assets/Prefabs/Hero.prefab
Dependencies:
- shared_materials.ab
- shared_textures.ab四、如何打包 AssetBundle
第一步:指定资源的 AB 名称
两种方式:
- 编辑器手动指定:在资源 Inspector 底部的 AssetBundle 下拉框设置名称(支持
folder/name形式的分组)。 - 代码 / 脚本指定:通过
AssetImporter.assetBundleName批量设置。
// 通过代码给资源指定 AB 名
AssetImporter importer = AssetImporter.GetAtPath("Assets/Prefabs/Hero.prefab");
importer.assetBundleName = "hero";
importer.assetBundleVariant = "hd"; // 可选变体,最终名为 hero.hd第二步:执行打包
using UnityEditor;
using System.IO;
public class BuildAssetBundles
{
[MenuItem("Tools/Build AssetBundles")]
static void Build()
{
string outputPath = "Assets/StreamingAssets/AB";
if (!Directory.Exists(outputPath))
Directory.CreateDirectory(outputPath);
BuildPipeline.BuildAssetBundles(
outputPath,
BuildAssetBundleOptions.ChunkBasedCompression, // LZ4 压缩
BuildTarget.StandaloneWindows64 // 目标平台
);
AssetDatabase.Refresh();
}
}重要:AB 是平台相关的。为 Windows 打的包不能用于 Android / iOS,必须为每个目标平台分别打包。
五、压缩方式对比
打包时通过 BuildAssetBundleOptions 选择压缩方式:
| 压缩方式 | Option | 体积 | 加载速度 | 说明 |
|---|---|---|---|---|
| 不压缩 | UncompressedAssetBundle | 最大 | 最快 | 空间换时间,随包内置时可用 |
| LZMA | 默认(旧) | 最小 | 慢 | 流式压缩,加载时需整包解压,首次加载慢 |
| LZ4 | ChunkBasedCompression | 中等 | 快 | 基于块压缩,可按需解压单个资源,推荐 |
为什么推荐 LZ4(ChunkBasedCompression)
- LZMA 是整包压缩,加载任意一个资源都要把整个 AB 解压到内存,开销大。
- LZ4 是分块(chunk)压缩,可以只解压需要的部分,加载后常驻内存也是压缩状态,内存友好。
- 通常做法:下载/存储用 LZMA(体积小),本地缓存后重压缩为 LZ4(加载快)。Unity 的缓存系统会自动做这件事。
六、运行时加载 AssetBundle
加载 AB 文件的四种 API
| API | 特点 | 适用场景 |
|---|---|---|
AssetBundle.LoadFromMemory(Async) | 从字节数组加载 | 加密 AB 解密后加载(内存开销大) |
AssetBundle.LoadFromFile(Async) | 从文件路径加载 | 最常用、最高效,LZ4 下几乎不占额外内存 |
UnityWebRequestAssetBundle | 从远程 URL 下载 + 缓存 | 热更新下载 AB |
AssetBundle.LoadFromStream | 从流加载 | 自定义流式解密等 |
完整加载流程
using UnityEngine;
using System.Collections;
public class ABLoader : MonoBehaviour
{
IEnumerator LoadHero()
{
string abPath = Application.streamingAssetsPath + "/AB/";
// 1. 先加载总 Manifest(获取依赖信息)
AssetBundle manifestAB = AssetBundle.LoadFromFile(abPath + "AB");
AssetBundleManifest manifest =
manifestAB.LoadAsset<AssetBundleManifest>("AssetBundleManifest");
// 2. 加载目标 AB 的所有依赖
string[] deps = manifest.GetAllDependencies("hero");
foreach (string dep in deps)
AssetBundle.LoadFromFile(abPath + dep);
// 3. 加载目标 AB
AssetBundle heroAB = AssetBundle.LoadFromFile(abPath + "hero");
// 4. 从 AB 中加载具体资源(异步)
AssetBundleRequest req = heroAB.LoadAssetAsync<GameObject>("Hero");
yield return req;
// 5. 实例化
GameObject hero = Instantiate(req.asset as GameObject);
}
}关键点
- 必须先加载依赖:加载目标 AB 前,先把
GetAllDependencies返回的所有依赖 AB 加载进来。 - 加载 AB ≠ 加载资源:
LoadFromFile只是把 AB 载入(建立映射),真正的资源要用LoadAsset/LoadAssetAsync。 - 一个 AB 只能被加载一次:重复
LoadFromFile同一个已加载的 AB 会报错,需自己做缓存管理。
七、卸载与内存管理
内存管理是 AB 系统的核心难点,Unload 的参数选择尤其关键。
AssetBundle.Unload(bool)
myAB.Unload(true); // 卸载 AB + 同时销毁所有从它加载出来的资源
myAB.Unload(false); // 只卸载 AB 本身,已加载的资源保留在内存| 参数 | 行为 | 风险 |
|---|---|---|
Unload(true) | 连同已加载资源一起销毁 | 若资源还在场景中使用,会变成丢失引用(粉色 / 报错) |
Unload(false) | 只释放 AB 结构,资源留存 | 资源与 AB 断开联系,可能产生内存泄漏和重复加载 |
推荐策略
- 常规做法:加载完资源、实例化后,用
Unload(false)释放 AB 本体(AB 本身也占内存),资源由后续逻辑管理。 - 场景切换 / 彻底清理:确认资源不再使用时,用
Unload(true)彻底回收。 - 配合引用计数:为每个 AB 维护引用计数,计数归零才真正卸载 —— 这是所有成熟 AB 框架的核心。
Resources.UnloadUnusedAssets():清理没有任何引用的资源(配合Unload(false)使用)。
八、Addressables:AB 的现代封装
从 Unity 2018 起,官方推出 Addressable Asset System(可寻址资源系统),建立在 AssetBundle 之上,解决了原生 AB 的诸多痛点。
Addressables 解决了什么
| 原生 AB 痛点 | Addressables 的改进 |
|---|---|
| 手动管理依赖加载 | 自动处理依赖,加载一个地址即可 |
| 手动管理引用计数 / 卸载 | 自动引用计数,Release 即可 |
| 打包分组、命名混乱 | 可视化 Group 管理,地址即资源标识 |
| 本地/远程切换麻烦 | 配置化的 本地 / 远程 Catalog,无缝切换 |
| 依赖硬编码路径 | 用逻辑地址(字符串 Key)解耦资源位置 |
简单示例
using UnityEngine.AddressableAssets;
using UnityEngine.ResourceManagement.AsyncOperations;
// 通过地址异步加载并实例化,依赖自动处理
AsyncOperationHandle<GameObject> handle =
Addressables.InstantiateAsync("Hero");
handle.Completed += (op) =>
{
if (op.Status == AsyncOperationStatus.Succeeded)
Debug.Log("加载成功");
};
// 释放(自动引用计数)
Addressables.ReleaseInstance(handle);选型建议
- 新项目:优先使用 Addressables,省去大量手写 AB 管理代码。
- 老项目 / 需要极致定制:可继续使用原生 AB + 自研框架(如 XAsset、BundleMaster 等)。
九、常见问题与最佳实践
常见坑
- 粉色 / 洋红色资源:Shader 或材质丢失 —— 通常是依赖 AB 没加载,或 Shader 没打进 AB / 没在
Always Included Shaders。 - 资源重复 / 内存翻倍:共享资源没独立打包,被多个 AB 各拷一份。
- 平台不匹配:用错平台的 AB,加载失败。
- 重复加载同名 AB 报错:没做 AB 缓存,重复
LoadFromFile。 Unload(true)误杀正在用的资源:时机不对,导致引用丢失。
最佳实践
- 合理的粒度:AB 不要太碎(数量爆炸、依赖复杂),也不要太大(加载慢、更新粒度粗)。按逻辑模块 + 共享资源分层。
- 共享资源单独打包:贴图、Shader、字体等公共资源独立成 AB。
- Shader 单独打包:所有 Shader 打一个 AB,或放入 ShaderVariantCollection 预热,避免运行时卡顿。
- 用 LZ4 压缩:加载快、内存友好。
- 引用计数管理生命周期:这是稳定 AB 系统的基石。
- 自动化打包:用脚本 + 依赖分析自动生成 AB 配置,避免手动指定出错。
- 版本管理:利用 Manifest 的 Hash 做增量更新,只下载变化的 AB。
十、完整工作流总览
【编辑器阶段】
资源指定 AB 名 → BuildAssetBundles 打包 → 生成 AB 文件 + Manifest
│
▼
上传到服务器(热更场景)
【运行时阶段】
下载 Manifest → 比对 Hash → 下载有变化的 AB → 本地缓存
│
▼
加载 Manifest → 查询依赖 → 加载依赖 AB → 加载目标 AB
│
▼
LoadAsset 加载资源 → 实例化使用 → Unload 卸载 → 引用计数归零彻底释放附:核心 API 速查
| 功能 | API |
|---|---|
| 打包 | BuildPipeline.BuildAssetBundles() |
| 从文件加载 AB | AssetBundle.LoadFromFile(Async)() |
| 从内存加载 AB | AssetBundle.LoadFromMemory(Async)() |
| 下载 AB | UnityWebRequestAssetBundle.GetAssetBundle() |
| 加载资源 | ab.LoadAsset<T>() / LoadAssetAsync<T>() |
| 加载全部资源 | ab.LoadAllAssets<T>() |
| 查询依赖 | manifest.GetAllDependencies() |
| 卸载 AB | ab.Unload(bool) |
| 卸载无引用资源 | Resources.UnloadUnusedAssets() |
总结
AssetBundle 是 Unity 中成熟的资源热更新与按需加载方案,其核心围绕打包策略、依赖管理、压缩方式、运行时加载与内存卸载五个方面。打包时需为资源指定 AB 名称并通过 BuildPipeline.BuildAssetBundles 构建,注意平台相关性;依赖管理是系统中最易出错的部分,共享资源必须独立打包以避免冗余,运行时需通过 Manifest 查询并提前加载所有依赖。推荐 LZ4(ChunkBasedCompression)作为默认压缩方式以平衡体积与加载性能。内存管理的核心是 Unload(bool) 的参数选择与引用计数机制,计数归零才真正卸载资产。Addressables 作为原生 AB 的高级封装,自动处理依赖、引用计数与分组管理,是新项目的优先选择。
