Unity AssetBundle 入门:别再把资源全塞进包里了,一分钟学会手动打包AB
很多Unity开发者,尤其是做单机或者小体量项目的朋友,最初接触资源管理时,多半是直接往Resources文件夹里一丢,或者干脆用Scene引用就完事了。项目小的时候还好说,一旦项目开始膨胀,打出来的安装包几百MB起步,每次更新版本都要重新下载整个包,策划改个UI图都要等半天完整构建,那时候你才会意识到AssetBundle(以下简称AB包)这个东西是绕不开的。
这篇博文就做一件最简单的事:把手上的一个资源(模型、贴图、Prefab或者UI图)打成AB包,再在运行时加载出来。基于我在实际项目里的操作,把整个流程拆到最细,不讲虚的。适合刚接触AB包、被各种教程绕晕的入门者,也适合已经会用但想回头把原理补扎实的开发者。
先说好,这只是一个入门流程。AB包体系里还有依赖管理、变体、增量构建、加密热更这些大型模块,我当年也是从这里一步步踩出来的。搞懂今天这套最小流程,后面那些复杂玩法你才有能力接住。
1. 为什么每个项目最终都会用到AB包
很多新手不理解,Unity自带Resources文件夹,也是动态加载资源,为什么非要搞一套AB包体系?还真不是Unity官方在制造复杂的东西。两者解决的核心问题完全不同。
Resources的核心特征是随包发布、只读、全量加载。你放进Resources里的所有东西,在构建后都会被完整地打进安装包里,没有任何例外。这意味着你没法做增量更新——想修一个模型?必须重新出包。另外Resources还牵涉启动性能和内存占用,资源太多时Unity会维护一个庞大的索引列表,拖慢启动。一个小技巧是,Resources目录建议只放启动所必需的最少量资源。
而AB包解决的核心痛点有两个:
- 包体瘦身与增量更新:AB包是独立于主程序的文件。你可以把资源打包后扔在StreamingAssets里随首包分发,也可以放服务器上,运行时下载。更新游戏时,玩家只需要下载变化的那几个AB文件,不用重新打包整个App。
- 资源复用与按需加载:同一个AB包可以被多个配置表、多个场景引用,内存层面可以做到只加载一份。比如UI图集、公共模型、角色动画,抽成AB包后,所有业务模块统一从AB包加载,避免散落各处导致资源冗余。
还有一个经常被忽略的点:AB包让“数据与逻辑分离”成为可能。策划改完数值表、美术换完贴图,不需要开发介入,直接走打包机产出AB包,程序逻辑不动,运营热更就能生效。在商业游戏团队,这个流程是刚需。
所以说,学习AB包不只是学一个API的问题,它背后是一整套资源管线的思维。今天这篇先解决最基础的手工打包流程,把管线跑通,后面你再去看那些“AB包管理器”“依赖分析工具”的源码,脉络会清晰得多。
2. 把第一个资源打成AB包:最小可用打包脚本
先讲一个常见认知误区:很多教程一上来就让你用第三方插件、或者搭建一整套带界面的打包管理器,对刚入门的人来说完全是劝退。其实Unity官网早就提供了最基础的打包API,也就是BuildPipeline.BuildAssetBundles。你只需要一个Editor脚本、一个目录规划,就能完成打包。
2.1 AssetBundle名称与后缀规划
在Unity工程里选中任意资源,在Inspector面板最下方能看到一个AssetBundle的选项。点开左边的下拉框,选择New,给它起个名字,比如我这里就把一个角色模型资源命名为model/character。
这里有两个容易忽略的细节,直接说结论:
- 名称实际就是包的相对路径。
model/character会被解析成两层目录:model/character,最后生成的文件名也是character。你可以利用这个特性给AB包做目录分组,也方便在加载时按路径去查。 - 后缀跟种类多没关系,纯粹是给加载时区分类型用的。同一个名称下可以有多个资源,但通常建议一个资源一个包,避免后期的依赖图变得太复杂。我这里写的
model/character,则是把同一个角色模型相关的Prefab、Mesh、Material都放一起。
这是第一个需要你手动做的操作:给每个想打包的资源设置AB包名称。
2.2 编写Editor打包脚本
下面是我在实际项目里用的最简版本,去掉了所有花哨逻辑,只保留核心。
using System.IO; using UnityEditor; using UnityEngine; public class BuildAB { [MenuItem("Tools/Build AssetBundles")] public static void BuildAllAssetBundles() { // 输出目录:工程根目录下的 AssetBundles 文件夹 string outputPath = "AssetBundles"; if (!Directory.Exists(outputPath)) { Directory.CreateDirectory(outputPath); } // 第二个参数传 BuildAssetBundleOptions.None,表示最基本压缩方式(LZMA) // 第三个参数传 BuildTarget.StandaloneWindows64,要按你的实际发布平台切换 BuildPipeline.BuildAssetBundles(outputPath, BuildAssetBundleOptions.None, BuildTarget.StandaloneWindows64); AssetDatabase.Refresh(); Debug.Log("AB包构建完成,输出目录:" + outputPath); } }脚本很简短,但是有几个地方我觉得值得展开说一下,这也是实际开发中最容易出问题的位置。
第一,BuildTarget的作用。AB包跟平台强相关,你在Windows上打出来的AB包,Android设备基本用不了。原因在于AB包内部序列化格式与纹理压缩格式都是按目标平台生成的。所以实际项目里,打包脚本一定要做成可配置平台的方式,或者做成菜单按钮对应不同平台。我见过不少新人打包后换平台就出问题,就是这个参数没变。
第二,BuildAssetBundleOptions参数。这里我传的是None,Unity会用LZMA压缩,优点是压缩率高,缺点是加载时要有一次完整解压,初次加载比较慢。实际线上项目大多用ChunkBasedCompression,也就是LZ4,介于压缩率和加载性能之间,这我在后面第4章细说。
第三,输出路径。这个路径不必非得在工程内,完全可以输出到工程外面。但作为入门,放在工程根目录下比较直观,方便你看输出结果。注意,如果你打算把AB包放在StreamingAssets下随首包发布,那StreamingAssets的路径在移动平台上是个只读目录,运行期不能往里写。真要热更下载的AB包,应该放在Application.persistentDataPath下。这个知识点请先记住,后面踩坑我能讲一天。
2.3 输出目录里的三个文件分别是什么
点下菜单后,去工程目录的AssetBundles文件夹下看,你会发现除了刚才设置的那些资源文件外,多出来三个特殊文件:AssetBundles(不带后缀)、AssetBundles.manifest、AssetBundles/AssetBundles.manifest。
这里直接说结论,这是每个初学者绕不开的困惑:
AssetBundles这个不带后缀的文件是整个AB包组的总入口,它记录了包与包之间、包与资源之间的依赖关系。AssetBundles.manifest是主清单的文本形式,人可读,但运行时加载靠的还是前面那个无后缀文件。- 每个具体的AB包还会生成一个同名
.manifest文件,比如model/character.manifest,里面列出了这个包内所有资源、依赖、CRC等。 - 那极容易被忽略的最终产物,其实是那个没有后缀的
AssetBundles文件。它和各个资源文件本身就是真正的AB包,其余manifest文件在运行时只有在你需要做依赖加载时才用得上。
顺便说一句,你最终上线时,可以不把.manifest文件上传到CDN。具体看你的加载方案。
3. 运行时加载AB包资源:LoadFromFile与异步加载
打完包,下一步就是运行时把它加载进来。这是入门阶段最重要的一块,也是最容易踩坑的地方。AB包的各种加载API,网上说法不一,我按“最常用、最符合实际需求”的程度给你梳理一遍。
3.1 同步加载:AssetBundle.LoadFromFile
核心API就是这一个:
AssetBundle ab = AssetBundle.LoadFromFile(Path.Combine(Application.streamingAssetsPath, "model/character")); GameObject prefab = ab.LoadAsset<GameObject>("Assets/Prefabs/Character.prefab"); Instantiate(prefab);这里有两个容易出错的地方,展开讲。
第一,LoadFromFile加载的是包文件,不是Asset名称。LoadFromFile的参数是AB包文件路径,而不是你在Unity编辑器里看到的那个资源文件名。有些新手会误以为可以直接LoadFromFile("character.prefab"),这是错的。你要加载的资源名,是包内资源,通过ab.LoadAsset再来查询。
第二,加载Prefab时,资源名必须与原始路径有关。上面例子中,我传给LoadAsset<GameObject>的是Assets/Prefabs/Character.prefab——注意,这里要用相对Assets根的路径,不是仅用文件名。这一点经常让新手崩溃:明明包里有这个资源,为什么加载出来是null?其实很可能是路径写错了。有一个更简便但不太推荐的办法是,在打包资源时给资源AssetBundle名称时,给资源起一个别名,然后在加载时直接按别名找。但最保险、最传统的做法还是用完整路径。
再说一个轻量的技巧,如果资源在包里唯一的,你还可以用ab.LoadAllAssets<GameObject>()一次性取出来。但如果你包里塞了十个资源,这么干容易搞混,建议包里资源越单一越好。
3.2 异步加载:AssetBundle.LoadFromFileAsync
在移动端,同步加载大文件会引起卡顿掉帧,业界常规操作是使用异步API。
using System.Collections; using UnityEngine; using UnityEngine.Networking; public class ABLoadExample : MonoBehaviour { IEnumerator Start() { string path = Path.Combine(Application.streamingAssetsPath, "model/character"); AssetBundleCreateRequest request = AssetBundle.LoadFromFileAsync(path); yield return request; AssetBundle ab = request.assetBundle; AssetBundleRequest assetRequest = ab.LoadAssetAsync<GameObject>("Assets/Prefabs/Character.prefab"); yield return assetRequest; GameObject prefab = assetRequest.asset as GameObject; Instantiate(prefab); } }注意这里有个关键点:LoadFromFileAsync只是“加载AB包文件”这个动作变成了异步,但真正加载具体资源时,我同样用的是异步的LoadAssetAsync。两者配合才能避免主线程卡顿。不过在实际项目中,包文件加载一般不是瓶颈,资源反序列化和实例化才是大头,所以LoadAssetAsync的收益更明显。
3.3 释放问题:Unload(false) 与 Unload(true) 的天壤之别
当资源用完,你需要调用ab.Unload(false)或ab.Unload(true),但很多人选错了参数,导致内存莫名其妙地爆掉,或者资源明明释放了但纹理还在。
区别是这样的:
Unload(false):卸载AB包内存中的元数据,但不会销毁已经实例化出来的对象及其纹理网格。也就是说,场景里已经生成的角色还能继续显示,但后续无法再从该AB包加载新资源。Unload(true):直接销毁AB包内的所有资产,包括已经实例化出来的对象引用的资源,场景里如果还有人在用,会出现模型变粉或物体消失。
入门阶段我建议用一个简单策略:如果你只把AB包当一次性资源加载用,加载完生成物体后,直接用Unload(false)。因为场景里的Prefab实例已经在内存里了,AB包本身可以安全卸载。等你需要完全清理,再配合引用计数去管理。
但如果是“大世界地图”那种持续从AB包加载资源、而且频繁切换的场景,Unload(false)会让内存碎片堆积,这时候必须上引用计数策略。具体方案以后写一篇单独展开,今天就先记住这个坑。
3.4 依赖加载:一个包引用了另一个包里的资源怎么办
这是新手最容易懵的地方。我打包时把角色Prefab放在一个包,但把它的贴图放在了另一个包。运行时只加载了角色包,一运行,模型是粉的。
原因:AssetBundle不会自动帮你把“被依赖的其他AB包”也加载进来。你需要手动加载依赖包。最常用的做法是借助主manifest文件。
AssetBundle mainAB = AssetBundle.LoadFromFile(Path.Combine(path, "AssetBundles")); AssetBundleManifest manifest = mainAB.LoadAsset<AssetBundleManifest>("AssetBundleManifest"); string[] dependencies = manifest.GetAllDependencies("model/character"); foreach (string dep in dependencies) { AssetBundle.LoadFromFile(Path.Combine(path, dep)); }重点在最后一行:GetAllDependencies返回的是被依赖的AB包名称数组,比如依赖的贴图包名。你得逐个加载这些依赖的AB包,然后被依赖的包里面的资源资源也就能被查找和引用了。需要特别注意的是,被依赖包的加载必须在主包资源加载之前完成,否则仍然会解析失败。这也是为什么很多项目会做一个“依赖预加载管理器”。
4. 压缩格式与打包选项:LZ4和LZMA怎么选
刚开始学AB包时,很容易忽略BuildAssetBundleOptions。但这个参数直接决定了AB包体积、加载速度、内存表现,选错后果很严重。所以单独拿出来说。
4.1 三种模式对比
| 选项 | 压缩算法 | 包体大小 | 加载方式 | 适用场景 |
|---|---|---|---|---|
| BuildAssetBundleOptions.None | LZMA | 最小 | 整体读入内存并解压 | 首次下载安装包、只加载一次的资源 |
| BuildAssetBundleOptions.Uncompressed | 无压缩 | 最大 | 直接内存映射 | 本地效率极高,但包体大 |
| BuildAssetBundleOptions.ChunkBasedCompression | LZ4 | 中等 | 按需解压,独立Chunk | 90%以上的线上项目推荐 |
LZMA是Unity的默认选项,压缩率最高,但坏处是它把整个AB包当一坨数据压在一起,加载时必须整体解压,一旦这个包有几十上百MB,启动加载就会有明显的白屏等待。
LZ4则聪明很多,把包内资源切成块,每个块单独压缩,加载时按需求解压对应的Chunk,不需要统统解压。代价是压缩率比LZMA差点,但换来的是“用多少解多少”的灵活性。
4.2 我的选型经验
先给一个反向例子。我早期做项目时图省事,所有包都用默认的None(LZMA),抽卡界面加载特效资源时,每次都要先花一两秒去解压巨大的特效包,画面愣住,体验很差。后来改成ChunkBasedCompression,包体只膨胀了不到20%,但加载特效秒开,整个手感完全不一样。
这里有一个取舍:首包场景资源可以用LZMA,因为只在安装后解压一次,后续一直使用解压后的缓存;热更下载的后续资源,如果经常用,尽量用LZ4。另外还有一个关键词是BuildAssetBundleOptions.DisableWriteTypeTree,在某些限制平台上有优化,但新手先别碰,容易引发生成物加载报错。
我个人的建议是,除非你有极端的包体压缩需求,否则默认都使用ChunkBasedCompression。压缩率和体验兼顾,不会有明显短板。
5. 实际项目中绕不开的坑:依赖、缓存与重复打包
打包流程很简单,难是难在各种隐性坑。我按频率排几个最有必要提前知道的。
5.1 同名资源被重复打进多个AB包
这是资源冗余最典型的场景。比如你有Assets/Prefabs/Enemy.prefab和Assets/Prefabs/Boss.prefab都引用了同一个Assets/Models/Sword.fbx。如果你的AB包划分是“每个Prefab一个包”,而且没有把Sword单独设成一个公共包,Unity会自动把它打进两个AB包里。最后出来的包体翻倍,构建时间也变长。
解决思路:
- 把公共资源(模型、贴图、图集)单独设成一个AB包,比如
public/shared_assets。 - 用AssetBundle Browser插件(Unity官方出过)看依赖,检查哪些资源被多个包引用了。
- 直接用依赖分析工具,提前在打包阶段就生成资源依赖报告。
5.2 打包目录里的旧包残留
随着开发迭代,AB包的名称和数量是经常变化的。如果直接用同一个输出目录反复打包,Unity默认不会清理旧的不再使用的包文件。这会导致一个问题:热更CDN上残留了一堆客户端根本不会用到的垃圾包,白白占空间,也容易让排查问题的人迷惑。
我的习惯是打包脚本开头先清空输出目录再生成,尤其到组内协作阶段,大家一拉最新代码就打包,这样能保证最终产物的纯净。
if (Directory.Exists(outputPath)) { Directory.Delete(outputPath, true); } Directory.CreateDirectory(outputPath);5.3 WebGL平台的IDBFS写入失败
看过热搜词的人可能也搜过“unity 发布 webgl 使用 idbfs 写入失败”,这个跟AB包也有直接关系。WebGL平台比较特殊,文件系统完全依赖浏览器IndexedDB模拟,而且默认没有开启IDBFS支持。如果代码里用AssetBundle.LoadFromFile去StreamingAssets目录读AB包(很多WebGL项目会把AB包塞在StreamingAssets里随包发布),在部分浏览器上会报文件写入失败。
标准做法是:WebGL上不要用File读取AB包,改成用UnityWebRequest加载。
UnityWebRequest request = UnityWebRequestAssetBundle.GetAssetBundle(uri); yield return request.SendWebRequest(); AssetBundle ab = DownloadHandlerAssetBundle.GetContent(request);并且浏览器本身有跨域限制,AB包放服务器时需要配置好CORS。另外一定要留意浏览器缓存,有时候你明明更新了服务器上的AB包,但浏览器还给你旧缓存,加载出来内容是旧的。常见绕法是在URL后面拼一个版本号参数。
5.4 Android与iOS路径差异
在移动平台上,Application.streamingAssetsPath在Android上是一个jar包内的路径,不能直接用FileAPI。很多新手在Android上做AB包本地加载,用了LoadFromFile(Application.streamingAssetsPath + "/xxx"),结果返回null。标准方案还是UnityWebRequestAssetBundle.GetAssetBundle配合file://协议,或者把AB包先拷贝到Application.persistentDataPath再LoadFromFile。
iOS相对好一些,但AppStore的安装包会被重新签名压缩,StreamingAssets下的大文件也可能是只读的,所以热更AB包存放路径永远建议走persistentDataPath。
5.5 增量构建:为什么你只想打一个包,却把所有包都重打了
BuildPipeline的增量构建是自己判断依赖关系的,但前提是你的AB包名称没有变、资源没有改变。如果你只是改了一个Prefab的Position,理论上它所属的那个AB包需要重新打,但Unity会把引用到它的所有包也一并标记为过期并重建。这是由依赖链决定的,不是Unity笨。真要实现“改谁打谁”,得引入“分目录分批打包”或者“AB包内容hash比对”的机制,这些都是大型项目的做法。入门阶段先接受这个行为,别硬改。
6. 从手动打包到自动化:这里可以延伸的思路
再讲一个我经常被问到的问题:打包脚本是跑通了一个,但策划和美术不可能每次都能自己选资源、点菜单,产品上线后也没有人力手动去打包,怎么办?
两种常见思路。
6.1 自动化批处理脚本
打AB包这个动作本身可以放进CI流程,比如Jenkins或者GitLab CI。触发条件可以是定时检查、代码提交、或者手动点击一个“出包”按钮。构建机上执行的是同一个BuildAssetBundles方法,只不过把BuildTarget通过命令行参数传入。
实现上有两个关键点:
- 在编辑器下,通过
[MenuItem]拿到菜单入口后,其实也是封装了一层命令行调用而已。 - 所有的包名、输出目录、平台、压缩方式、是否打版本号后缀,都应该抽成可配置参数,不要让逻辑散落在脚本各处。
这样美术在提测时只需要走迭代流程,脚本自动打包、上传CDN、通知测试,不需要碰Unity编辑器。解放生产力,从脚本化开始。
6.2 标准化AB包命名与资源目录规范
第二种是“工程规范先行”。打包脚本再强大,如果美术给资源起的名字乱七八糟,最后出来的AB包依然是一团乱麻。所以稍大一点的项目,建议把AB包名跟资源目录路径强绑定,例如assets_bundles/ui/main_menu就对应Assets/UI/MainMenu/下的所有资源,脚本自动遍历目录生成AB包名,而不是靠人手动去Inspector里配置。这么做至少有几个好处:
- 新成员入职后只要往对应目录放资源,不用学打包知识,也能提交正确的AB包。
- 代码和资源在目录上就能对齐,排查问题更快。
- 脚本生成AB包名时顺便能做资源依赖查重,避免冗余包。
我后来做项目,都是先把目录结构画好,再写生成AB包名的脚本,最后才是打包入口。顺序反了,后面全是坑。
6.3 更上层:为什么不先学Addressable再回来学AB包
我知道现在很多教程都在推Addressable,官方也一直在完善它,它在AB包基础上做了引用计数、依赖管理、远程加载策略等一系列封装。很多人问,那我是不是直接学Addressable就不需要碰裸的AB包了?
我个人的体会是,Addressable确实能解决大部分场景,但底层仍然是AssetBundle,很多高级问题(比如资源加载时序、内存分析、加载失败排查)最终还是得落回到AB包的机制上。所以先把AB包的底层原理捋一遍,再看Addressable,你会觉得它的设计是那么自然;直接上手Addressable,遇到奇怪的内存问题会一头雾水。
从学习路径来说,建议先照着本文步骤手动打一个AB包,跑通全流程,对输出文件和加载API有体感之后,再进Addressable。这跟学编程先写指针、再上框架是一个道理。基础夯实了,上层工具只是表层的便利。
正式入门之后,再去看热搜词里那些“unity游戏优化”“unity数字孪生”“unity全栈开发工程师”相关的问题,你就知道AB包只是资源管理这棵大树的一根枝干,往上还有性能优化、热更体系、多语言、增量发布……路还很长,但方向对了,踩坑都会变成经验。
最后多说一句,我自己在实际项目里总结下来的最笨也最有效的学习方式:不管你用什么加载框架,先别急着封装,用最原始的API写一个能跑通的全流程Demo,然后故意把路径写错、把压缩格式改错、把释放参数调反,亲眼看一下报错和现象。这一套坑踩下来,你在这个知识点上,就比大多数人扎实了。