1. 引言
在 Unity 游戏开发中,资源管理是决定项目包体大小、加载速度和内存占用的关键环节。AssetBundle(简称 AB)作为 Unity 官方的资源打包与加载方案,是热更新、模块化开发和资源动态加载的基础设施。本文将基于实际项目经验,系统性地讲解 AssetBundle 的含义、功能、编辑器操作流程、常用 API、分析方法以及常见问题的排查方案,帮助开发者构建一套健壮的 AB 资源管理体系。
2. AssetBundle 概述
2.1 什么是 AssetBundle
AssetBundle 是 Unity 提供的一种资源打包格式,它可以将多个资源(模型、贴图、材质、Prefab、音频、场景等)序列化压缩到一个文件中,供运行时按需加载。每个 AB 包本质上是一个二进制容器,内部存储了资源的序列化数据、类型树(TypeTree)以及依赖关系信息。
2.2 AssetBundle 的核心功能
- 减小初始包体:将非必需资源从安装包中剥离,按需下载加载。
- 支持热更新:通过服务器分发新版本 AB 包,实现无需重新安装的更新。
- 模块化资源管理:按功能模块划分资源边界,便于团队协作和版本管理。
- 内存优化:按需加载和卸载资源,避免一次性加载全部资源导致内存峰值。
- 跨平台适配:针对不同平台生成对应的纹理压缩格式和 Shader 变体。
2.3 AssetBundle 的组成结构
一个完整的 AB 构建产物包含以下文件:
- xxx.unity3d (或无后缀):实际的资源 AB 文件,序列化存储模型、贴图、Prefab 等业务资源,是运行时加载的核心资源文件。后缀名
.unity3d是自定义命名时的常见选择,本质与无后缀名的标准 AB 包完全等价。 - xxx.unity3d.manifest:单 AB 包的个体清单文件,记录该包内包含的所有资源列表、每个资源的 Asset GUID 以及引用的外部 AB 资源信息。主要用于开发期排查依赖异常。
- AssetBundle.unity3d:主 AB 包索引文件,记录本次全量构建生成的所有 AB 包名称、Hash 校验值、CRC 校验值以及每个包的完整依赖关系链。运行时必须先加载主 Manifest 才能正确加载目标 AB 包。
- AssetBundle.manifest:全局清单文件,记录全局资源信息,不需要打进正式包,但它是排查依赖异常的最佳工具。
3. 编辑器操作流程
3.1 资源标记与命名约定
在打包之前,需要为资源设置 AB 包名。通常将目录路径转为包名方便后续排查。
不会被打入 AB 包的资源:
Editor/目录下的文件.cs脚本文件StreamingAssets/目录.svn目录- 含非 ASCII 字符(中文)的文件(AB 解压会报错)
- 文件夹路径(不含
.的条目)
3.2 构建前的环境准备
- 切换目标平台:需要先将编辑器切换到目标平台(Android/iOS/WebGL/Windows),确保后续编译与资源处理都按目标平台规则执行。
- 刷新资源数据库 :切换 BuildTarget 之后,需要重新调用一次
AssetDatabase.Refresh,让 Shader 变体和资源导入结果按目标平台重新编译。 - 创建输出目录 :
BuildAssetBundles不会自动创建输出文件夹,必须输出到已有文件夹或提前用Directory.CreateDirectory手动创建。 - 整理资源 :构建前先调用
AssetDatabase.SaveAssets()和Resources.UnloadUnusedAssets()整理资源。
3.3 分发策略
生成的 AB 包有两种分发方式:
- 首包必需的基础 AB 包:放入 StreamingAssets 目录打进初始安装包
- 非必需的业务 AB 包:上传到热更新服务器,运行时按需下载
加载优先级逻辑 :优先检查 Application.persistentDataPath 是否有新版本,如果有则加载;如果没有(首次安装或未更新),则回退加载 StreamingAssetsPath 中的初始版本。
下面是完整的编辑器操作流程时序图,从切换目标平台到最终分发:
热更服务器 StreamingAssets 构建管线 资源数据库 Unity 编辑器 开发者 热更服务器 StreamingAssets 构建管线 资源数据库 Unity 编辑器 开发者 #mermaid-svg-cYO2z3Or6fbO0fCH{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-cYO2z3Or6fbO0fCH .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-cYO2z3Or6fbO0fCH .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-cYO2z3Or6fbO0fCH .error-icon{fill:#552222;}#mermaid-svg-cYO2z3Or6fbO0fCH .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-cYO2z3Or6fbO0fCH .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-cYO2z3Or6fbO0fCH .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-cYO2z3Or6fbO0fCH .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-cYO2z3Or6fbO0fCH .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-cYO2z3Or6fbO0fCH .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-cYO2z3Or6fbO0fCH .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-cYO2z3Or6fbO0fCH .marker{fill:#333333;stroke:#333333;}#mermaid-svg-cYO2z3Or6fbO0fCH .marker.cross{stroke:#333333;}#mermaid-svg-cYO2z3Or6fbO0fCH svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-cYO2z3Or6fbO0fCH p{margin:0;}#mermaid-svg-cYO2z3Or6fbO0fCH .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-cYO2z3Or6fbO0fCH text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-cYO2z3Or6fbO0fCH .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-cYO2z3Or6fbO0fCH .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-cYO2z3Or6fbO0fCH .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-cYO2z3Or6fbO0fCH .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-cYO2z3Or6fbO0fCH #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-cYO2z3Or6fbO0fCH .sequenceNumber{fill:white;}#mermaid-svg-cYO2z3Or6fbO0fCH #sequencenumber{fill:#333;}#mermaid-svg-cYO2z3Or6fbO0fCH #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-cYO2z3Or6fbO0fCH .messageText{fill:#333;stroke:none;}#mermaid-svg-cYO2z3Or6fbO0fCH .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-cYO2z3Or6fbO0fCH .labelText,#mermaid-svg-cYO2z3Or6fbO0fCH .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-cYO2z3Or6fbO0fCH .loopText,#mermaid-svg-cYO2z3Or6fbO0fCH .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-cYO2z3Or6fbO0fCH .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-cYO2z3Or6fbO0fCH .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-cYO2z3Or6fbO0fCH .noteText,#mermaid-svg-cYO2z3Or6fbO0fCH .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-cYO2z3Or6fbO0fCH .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-cYO2z3Or6fbO0fCH .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-cYO2z3Or6fbO0fCH .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-cYO2z3Or6fbO0fCH .actorPopupMenu{position:absolute;}#mermaid-svg-cYO2z3Or6fbO0fCH .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-cYO2z3Or6fbO0fCH .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-cYO2z3Or6fbO0fCH .actor-man circle,#mermaid-svg-cYO2z3Or6fbO0fCH line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-cYO2z3Or6fbO0fCH :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} alt首包必需的基础 AB 包非必需的业务 AB 包 1. 切换目标平台设置 BuildTarget(Android/iOS/WebGL/Windows)2. 刷新资源数据库AssetDatabase.Refresh()Shader 变体与导入结果按目标平台重新编译3. 创建输出目录Directory.CreateDirectory(BuildAssetBundles 不会自动创建)4. 整理资源SaveAssets() +UnloadUnusedAssets()5. 调用 BuildAssetBundles资源收集 + 依赖计算+ 序列化压缩返回 AssetBundleManifest(Hash/CRC/依赖关系)放入 StreamingAssets打进初始安装包上传到热更新服务器运行时按需下载
关键步骤说明:
- 切换目标平台:必须在构建前完成,不同平台的 AB 包二进制格式不兼容,纹理压缩格式和 Shader 变体都会按目标平台重新适配。
- 刷新资源数据库 :切换 BuildTarget 后调用
AssetDatabase.Refresh,让资源导入结果和 Shader 变体按新平台重新编译,避免沿用旧平台的序列化数据。 - 创建输出目录 :
BuildAssetBundles不会自动创建输出文件夹,必须提前用Directory.CreateDirectory手动创建,否则函数会直接失败。 - 整理资源 :构建前调用
AssetDatabase.SaveAssets()和Resources.UnloadUnusedAssets(),释放未使用的资源引用,降低构建进程的内存峰值。 - 构建 AssetBundle :
BuildPipeline.BuildAssetBundles负责资源收集、依赖计算和序列化压缩,返回的AssetBundleManifest记录了所有包的 Hash、CRC 和依赖关系。 - 分发 :首包必需的基础 AB 包放入 StreamingAssets 打进安装包;非必需的业务 AB 包上传到热更服务器,运行时按需下载。加载时优先检查
persistentDataPath的新版本,没有则回退加载 StreamingAssets 中的初始版本。
4. 常用 API 详解
4.1 构建 API(仅编辑器使用)
BuildPipeline.BuildAssetBundles 是 Unity 中构建 AssetBundle 的核心编辑器 API,负责资源收集、依赖计算和序列化压缩三件事。
csharp
AssetBundleManifest manifest = BuildPipeline.BuildAssetBundles(outputPath, options, target);
接口参数:
- outputPath(输出路径) :打包产物的输出目录,如
"Assets/AssetBundles"。注意该文件夹不会自动创建,如果不存在函数会直接失败 - options(构建选项) :
BuildAssetBundleOptions枚举,决定压缩方式和构建行为,可以按位|组合 - target(目标平台) :
BuildTarget枚举,如BuildTarget.StandaloneWindows、BuildTarget.Android。不同平台的 AB 包不兼容
返回值 :调用成功后返回 AssetBundleManifest 对象,记录所有 AB 包的哈希、CRC 和依赖关系;构建失败则返回 null。
4.2 运行时加载 API
| API 名称 | 加载方式 | 适用场景 | 核心用法示例 |
|---|---|---|---|
AssetBundle.LoadFromFile |
同步加载 | 本地未压缩/LZ4 压缩的 AB 包,加载速度最快 | AssetBundle ab = AssetBundle.LoadFromFile(Application.streamingAssetsPath + "/common"); |
AssetBundle.LoadFromFileAsync |
异步加载 | 本地大体积 AB 包,不阻塞主线程 | 用协程等待 AssetBundleCreateRequest 完成,得到 AB 对象 |
UnityWebRequestAssetBundle.GetAssetBundle |
异步网络加载 | 从远程热更服务器下载 AB 包 | 支持断点续传和缓存,是热更新场景的标准用法 |
AssetBundle.LoadFromMemory |
同步内存加载 | 从加密后的字节流加载 AB 包 | 适合做资源加密防破解,性能低于直接从文件加载 |
4.3 资源读取 API
得到 AssetBundle 对象后,就可以从包内读取具体的资源对象:
csharp
// 同步加载指定名称的预制体
GameObject heroPrefab = ab.LoadAsset<GameObject>("Hero.prefab");
Instantiate(heroPrefab);
// 异步加载资源,避免大资源加载阻塞主线程
AssetBundleRequest request = ab.LoadAssetAsync<Texture2D>("HeroTex.png");
yield return request;
Texture2D heroTex = request.asset as Texture2D;
注意:加载资源时必须指定正确的资源类型,否则可能加载失败;如果不指定类型,会加载 AB 包内所有同名的不同类型资源,造成内存浪费。
4.4 依赖加载管理
csharp
// 读取主 Manifest 获取依赖列表
AssetBundle manifestBundle = AssetBundle.LoadFromFile(manifestPath);
AssetBundleManifest manifest = manifestBundle.LoadAsset<AssetBundleManifest>("AssetBundleManifest");
string[] dependencies = manifest.GetAllDependencies(abName);
foreach (string dep in dependencies)
{
string depPath = Path.Combine(bundleDir, dep);
if (!loadedBundles.ContainsKey(dep))
{
AssetBundle.LoadFromFile(depPath);
loadedBundles.Add(dep, bundle);
}
}
这段逻辑要证明两点:一是所有 AB 必须先加载依赖包,再加载自己;二是重复加载必须用字典去重,否则内存里会同时存活多个相同 AB,浪费内存还容易造成引用混乱。可以在加载管理器里把每个 AB 包缓存起来,卸载时只减引用计数,计数到零才真正 Unload(true)。
5. 构建选项详解
BuildAssetBundleOptions 枚举是控制 AB 构建行为的关键,以下是各选项的详细说明:
| 枚举值 | 值 | 说明 |
|---|---|---|
None |
0 | 默认值,使用 LZMA 压缩,压缩率最高但加载需整体解压 |
UncompressedAssetBundle |
1 | 不压缩,包体最大但加载最快,仅用于调试 |
DisableWriteTypeTree |
8 | 不写入 TypeTree,减小包体但降低跨版本兼容性 |
ForceRebuildAssetBundle |
0x20 | 忽略缓存强制全量重打,仅限切换平台或排查异常时使用 |
IgnoreTypeTreeChanges |
0x40 | 增量构建时忽略 TypeTree 变化,避免微小变动导致全量重打 |
AppendHashToAssetBundleName |
0x80 | 将哈希值附加到文件名,便于热更时通过文件名判断资源变更 |
ChunkBasedCompression |
0x100 | 使用 LZ4 块压缩,支持按需解压,生产环境推荐选项 |
StrictMode |
0x200 | 严格模式,构建出现任何错误或警告即判定失败 |
DryRunBuild |
0x400 | 预演构建,执行流程但不生成文件,用于检查配置 |
DisableLoadAssetByFileName |
0x1000 | 禁用通过文件名加载资源,仅允许路径或 Hash 加载 |
DisableLoadAssetByFileNameWithExtension |
0x2000 | 禁用通过"文件名+扩展名"加载资源,进一步优化索引 |
AssetBundleStripUnityVersion |
0x8000 | 移除文件头中的 Unity 版本号,减小包体并提升小版本兼容性 |
UseContentHash |
0x10000 | 基于内容计算哈希,提升增量构建准确性,建议开启 |
RecurseDependencies |
0x20000 | 递归计算依赖,适用于 ScriptableObject 等复杂依赖链场景 |
StripUnatlasedSpriteCopies |
0x40000 | 去除未打图集 Sprite 的重复副本,避免纹理数据冗余 |
5.1 压缩方式选择
ChunkBasedCompression(LZ4):这是生产环境推荐使用的压缩方式。它实际上是一个由 Unity 改良过的 LZ4 算法,支持按需解压,兼顾压缩率和加载速度。
- 随包发布的本地资源 :用
ChunkBasedCompression打包,配合AssetBundle.LoadFromFileAsync加载 - 需要加密的 Bundle :先
ChunkBasedCompression压缩,再用LoadFromMemoryAsync加载
5.2 DisableWriteTypeTree 的妙用
这个参数经常被开发者忽略,但它非常有用:可以减小 AssetBundle 包体大小、减小内存占用,同时减少加载 AssetBundle 时的 CPU 时间。
当开启 TypeTree 写入时,Unity 在打 AssetBundle 时会先把数据内容的树状结构先写入一遍(如 mipMapMode、enableMipMap、sRGBTexture 这些字段),然后才写入它们的值,这导致 AssetBundle 大小增加。使用时 Unity 会先解析 TypeTree,再反向解析数据内容。
跨版本兼容性说明:当用 Unity 2020 解析 2018 的 AssetBundle 时,如果发现某个字段(如 vTOnly)是 TypeTree 里没有的,就会使用默认值填充;如果某个字段是 2018 有的而 2020 没有的,则会丢弃该字段对应的值,防止反向解析出错。
5.3 禁用文件名加载优化
当我们加载好一个 AssetBundle 然后使用 LoadAsset 加载 Asset 时,需要传递 Asset 的路径名称。这个名称有三种写法:
csharp
AssetBundle ab = AssetBundle.LoadFromFile(Path.Combine(Application.streamingAssetsPath, "sphere"));
Instantiate(ab.LoadAsset("Sphere")); // 文件名
Instantiate(ab.LoadAsset("Sphere.prefab")); // 文件名+扩展名
Instantiate(ab.LoadAsset("Assets/Sphere.prefab")); // 全路径
如果不设置 DisableLoadAssetByFileName 和 DisableLoadAssetByFileNameWithExtension 参数,使用这三种名称都可以正确加载 AB 里面的 Asset。但其中只有全路径是被序列化到 AssetBundle 当中的,查看对应的 .manifest 可以发现里面存储的是全路径。
文件名和文件名+扩展名是在 AssetBundle 被加载成功后产生的,因此会产生一定的代价。当没有禁用时,Unity 实际上算了一个 Hash 进去,当通过文件名去找 Asset 时,它会生成这个文件名的原路径然后对比,在 CPU 时间和内存上会有一些消耗。如果确定加载 Asset 的方式是用全路径加载,就可以把它关闭掉。
6. 常用分析方法
6.1 AB 包依赖图分析
采集工程内的资源依赖图:核心思路是遍历指定的资源目录,对每一个资源文件获取其所有的依赖项。这里的关键是区分"直接依赖"和"递归依赖"。
csharp
// 获取直接依赖(性能更优)
string[] directDeps = AssetDatabase.GetDependencies(assetPath, recursive: false);
// 获取递归依赖(一次性获取,但性能堪忧)
string[] allDeps = AssetDatabase.GetDependencies(assetPath, recursive: true);
实操心得 :直接使用 recursive: true 在处理大量资源时性能堪忧。更优的做法是使用 recursive: false 获取直接依赖,然后自己构建依赖图,这样既能获得更结构化的数据,也便于后续分析。同时要特别注意对 .cs 脚本文件的处理,通常不将脚本视为 AssetBundle 的打包资源,但脚本对资源的引用关系需要记录用于分析。
解析已生成的 AssetBundle 及其 Manifest :通过 AssetBundleManifest.GetAllAssetBundles() 获取所有 Bundle 名,通过 GetAllDependencies、GetDirectDependencies 获取依赖关系。这一层输出的数据结构化模型包括:
- AssetNode:表示一个具体的资源,包含 GUID、路径、类型、文件大小等信息
- BundleNode:表示一个 AssetBundle,包含名称、哈希值、文件大小、包含的资源列表
- DependencyLink:表示一条依赖边,记录源节点和目标节点,以及依赖类型(如"直接引用"、"打包包含")
6.2 依赖报告与公共资源抽取
更实用的做法是做一个 Editor 脚本扫描所有 AB 包的依赖,在构建前后分别输出依赖关系报告。如果发现某个 AB 包依赖了 10 个以上的其他包,就要审视一下是不是有公共资源没有抽成独立共享包。
公共资源的打包策略 :把所有可能被多个模块引用的资源单独抽出来放进一个 Common 包或按类型分包,比如 common_shared_assets.ab,让其他包都依赖它。这样热更新时只要公共包不变,各个业务包可以随意更新。
6.3 绑包规则与依赖陷阱
凡是 AB 包之间的引用关系,尽量控制在同一层级的依赖链上,不要出现 A 包依赖 B 包、B 包依赖 C 包、C 包又依赖 A 包的闭环。Unity 本身没有对循环依赖做强校验,但运行时加载 AB 如果不按顺序,就可能出现资源加载一半找不到依赖的情况。
7. 常见问题与排查方案
7.1 构建成功但 AB 为空文件或体积异常小
遇到 AB 生成了但文件只有几 KB 甚至 B 级大小,十有八九是打的 AB 包含的资源都是纯引用类型,没有实际资产内容。例如把一个 Prefab 设为 AB 包,但它引用的模型贴图全部被其他 AB 包先占用了,这个 AB 里就只存了依赖关系。BuildAssetBundles 看起来能构建成功,但运行时资源加载会依赖其他包。
排查方法 :打开 AB 旁边的 .manifest,查看 Assets 列表是不是只有这个 Prefab 本身。如果预期它应该包含贴图和模型,那就说明打包边界有问题,而不是构建错了。
7.2 增量构建失效:每次全量构建的原因
最常见的原因是资源目录里有一个持续生成的文件(如日志文件、临时缓存图)被打进了某个 AB 包,每次内容都变,Unity 只能判定这个 AB 包全部重打。另一个常见原因是 Shader 或 SpriteAtlas 引用动态变化,导致依赖 hash 每次不同。
解决办法 :用 AssetDatabase.GetDependencies 做一次全量依赖 dump,看看每次构建前后的 hash 差异在哪,定位到具体资源后排除或固定其序列化数据。
7.3 加载时机错误导致依赖缺失
运行时加载 AB 次序问题表现很隐蔽,比如某个 UI 点击后弹窗空白,报错 The AssetBundle 'xxx' can't be loaded because another AssetBundle with the same file is already loaded。
原因:AB 包名大小写不一致或加载管理器没处理好加载去重。
解决方案:统一在加载管理器进行加载,并将 AB 包名做一个相对路径标准化,在加载前统一转成全小写或全绝对路径。
7.4 构建时 Shader 丢变体
很多项目在打 AB 后,资源运行起来没有阴影或出现紫皮。Shader 变体丢失典型原因是 Unity 默认只收集在场景中实际使用的 Pass 和关键字。
解决方案 :要让所有变体都打进 AB,需要手动配置 ShaderVariantCollection,且该 Collection 要勾选对应 keyword。这个坑建议在构建文档里专门标注:任何 Shader 代码升级或新增 keyword,都需要重新生成 ShaderVariantCollection,否则 AB 构建不会包含新变体。
7.5 主 Manifest 找不到依赖包
如果你用 manifest.GetAllDependencies 返回的是一个空数组,而实际 AB 之间存在依赖,那大概率是构建时用错了 BuildTarget。不同平台的 AB 底层二进制格式不同,Unity 会为不同平台生成不同 hash,但你给的是同一份主 Manifest,所以依赖列表判断也会出错。
深层原因:Unity 的 AB 构建缓存默认是全局共享的,不会自动按平台做隔离。增量构建的判断逻辑是基于资源修改后的哈希值,决定是否复用历史缓存中的序列化结果。当你切换平台后,之前其他平台的历史构建缓存不会被自动清除,新的平台构建过程中很可能错误复用了旧平台生成的序列化数据。
根据 AB 构建系统的底层特性,不同平台的 AB 包本身完全不兼容:Unity 在序列化资源时,会根据目标平台的平台宏特性自动适配纹理压缩格式、Shader 变体、目标架构的类型树,生成完全不同的二进制内容。如果增量构建直接复用到了旧平台的缓存数据,生成出的 AB 包本质上是跨平台的畸形产物。
解决方案:每次平台切换后,务必清理构建缓存并重新生成主 Manifest。
7.6 编辑器内存溢出或构建崩溃
大型项目构建 AB 经常遇到 OutOfMemory。这通常是由于 AssetBundle 构建进程缓存了大量导入资源的计算结果。
解决方案:
- 先关掉 Unity 编辑器,删除
Library/BuildCache目录,再重新启动 - 把资源按目录分批构建,或采用多进程分别构建不同 AB 包集合,能显著降低单进程内存峰值
- 构建前先调用
AssetDatabase.SaveAssets()和Resources.UnloadUnusedAssets()整理资源 - 一次不要构建太多 AB,可以做分区构建:先公共资源,再业务资源
- 分步构建时注意,不要重复设置
AssetBundleName,否则后续构建会打回原样 - 如果资源导入器本身吃内存,尝试在构建的 Job 间增加 GC 时间片
- CI 环境下,建议每个常见的构建任务都放在干净的 BatchMode 命令中,不带编辑器界面,用
-quit -batchmode -executeMethod来执行。这样能避免 Editor 停留在后台累积内存碎片
7.7 更新包体过大,明明改动很小
排除资源本身变异,最大的嫌疑是间接依赖范围被扩大。比如你改了一个 Prefab,而这个 Prefab 引用了某个公共 ArtBundle,公共 ArtBundle 里的资源又被其他 10 个业务包依赖,如果构建时公共 ArtBundle 的 Hash 变了,那这 10 个业务包在 Manifest 里的依赖 Hash 也都会发生改变。客户端的更新逻辑仿照"只比对各 AB 自己的 Hash"是发现不了这种连锁的,但只要它的更新策略是"任一依赖 Hash 变了就下载依赖包",那就会下载所有引用了公共包的业务包。
解决思路有两类:
- 第一类:把更新时间拉长,只在版本发布时全量对比,平时小更新只记录增量文件
- 第二类:从依赖源头控制,让公共包尽量回归稳定。比如动画资源、UI 图集这种大资源,尽量不要和业务 Prefab 共享一个 AB,或者把公共包拆得更细
实用判断标准:如果公共包超过 100MB 且被超过 20 个业务包引用,它一定会成为更新风暴的中心,趁早拆分。
7.8 脚本字段变更导致 AB 失效
这是热更项目最痛的问题。一个服务端组件里如果包含 MonoBehaviour,它的序列化数据里保存了该 MonoBehaviour 的字段值。当你修改脚本的字段名称、删除字段、改变字段类型,Unity 反序列化时可能对不上,要么字段丢失,要么整个资源加载失败。这和 BuildPipeline 本身无关,但增量构建和 TypeTree 会放大这个影响。
如果 AB 必须包含 MonoBehaviour,建议:
- 不要删除字段,只新增字段,并且给新增字段设置合理的默认值
- 不要修改字段名,除非你有完整的版本升级函数
- 尽量把可变配置放在 ScriptableObject 或 Json 中,运行时序列化,避免频繁改脚本结构
- 如果确实改了脚本,记得在构建时强制重建所有包含该脚本的 AB,不要只做增量
关于 IgnoreTypeTreeChanges :构建时如果用 IgnoreTypeTreeChanges,Unity 在对比增量时会忽略 TypeTree 变化,但运行时加载时如果 AB 里的 TypeTree 和当前运行的程序集不一致,仍然可能出问题。所以这个选项不是万能药,它省的是构建时间,省不掉兼容性风险。
参考:
https://zhuanlan.zhihu.com/p/411946807;
https://blog.csdn.net/weixin_32147929/article/details/165779372;
https://blog.csdn.net/weixin_32631179/article/details/166606932;
https://blog.csdn.net/weixin_30363509/article/details/97990396;
https://blog.csdn.net/weixin_32147929/article/details/165779372;