开局先说一个可能有点扎心的事实AssetBundle这套东西Unity官方文档写了十年教程千千万但真正敢说自己玩懂了原生AB工作流的人远没有想象中那么多。很多人一上来就上Addressables或者项目里早早集成了YooAsset这类第三方框架反而把最底层的原生AssetBundle工作流给绕过去了。但恰恰是这套“原始”流程藏着Unity资源管理最核心的底层逻辑——加载、依赖、释放、增量更新所有上层框架都是在它上面做了一层封装。你绕开了它一时爽等遇到莫名其妙的贴图花屏、模型Missing、包体积失控、更新包拆不明白的时候回头再补底层的账成本是翻倍的。这篇就基于我在多个上线项目里折腾原生AssetBundle的经验好好聊清楚原生AssetBundle工作流是什么、为什么这么设计、以及一套可用到生产环境的完整流程该怎么搭。这篇不是给你念官方文档是给你一份能直接拿来用的实践手册。1. 内容整体设计与思路拆解1.1 原生AssetBundle到底解决了什么问题AssetBundle下面简称AB本质上是Unity提供的一种资源打包格式。它允许你把任意资源——模型、贴图、材质、预制体、音频、甚至场景——打成一个个独立于主程序的压缩包在运行时按需加载到内存里使用。这事儿的价值在哪儿往大了说是解放了游戏和应用的体积管理。没有AB之前所有资源全部打进安装包安装包体积直逼几个G首包下载时间感人还没法做资源修复出个Bug只能重新发版。有了AB之后你可以把一部分资源挪出安装包放到服务器上玩家运行时拉取更新资源的时候只需要拉增量包不用重新下载整个App。往小了说AB让内存可控这件事成为可能。你可以只加载当前场景真正需要的资源用完就释放而不是一口气把所有东西都塞进内存。对移动端这种内存敏感的平台来说这件事是生死线。所以原生AB工作流本质上是做三件事打包、加载、管理。打包决定了资源怎么组织加载决定了怎么把资源用起来管理决定内存和依赖怎么不出事。1.2 为什么还需要“工作流”这个词很多人有个误区以为AB就是个简单的API调用BuildPipeline.BuildAssetBundles一执行资源就全打包好了运行时AssetBundle.LoadFromFile一加载就完事儿了。真这么简单的话市场上就不会有Addressables、YooAsset、CatAsset这一堆框架冒出来了。AB真正困难的地方在于资源与资源的依赖关系异常复杂。一个UI预制体可能引用了一张图集图集又引用了ShaderShader又可能引用了材质变体一个角色模型身上挂了动画控制器、音效、特效特效里又嵌套了另一个预制体。这些依赖关系在编辑器里是Unity编辑器帮你自动维护的打包成AB之后就得靠你设计的规则来保证它们能正确解析。规则设计得不合理就会出现两类经典翻车现场一个公共图集被100个AB包引用你把图集打进了其中1个AB包结果另外99个包加载时找不到图集全部变成紫粉色。一张贴图被多个AB包各自打包了一份副本包体积直接翻倍而且内存里同时存在多份相同贴图白白浪费几十MB。所以“工作流”这个词强调的是一套从资源规范、打包策略、依赖处理到运行时加载、释放、更新的体系化方案。原生AB自在2015年Unity 5.0重构之后它的打包模型一直是同一套底层逻辑变的是你要怎么围绕它搭建自己的流程。1.3 不同规模项目的方案选型逻辑我自己经历过几种不同体量的项目选型逻辑是完全不同的小型独立游戏资源量小场景切换少其实可以用最简单的打包策略——一个场景一个AB包甚至全打成一个包都行。这种情况下用原生AB手写一套轻量级管理代码完全够用没必要引入框架。中型项目资源量开始上来需要做增量更新需要分批加载释放原生AB的裸奔模式就不太够了。这时候要么自己封装一套管理模块打包清单、依赖预加载、引用计数回收这套代码量不小但可完全掌控。大型项目团队人多、资源量巨大、更新频繁这时候直接上Addressables或者YooAsset会更稳妥。它们把依赖管理、异步加载、远程资源分组、更新重试这些脏活累活都封装好了团队只需遵守资源规范即可。但注意即便最终选了框架方案理解原生AB工作流仍然是必要前提。框架只是帮你封装了默认行为遇到边缘Case的时候你还是要懂底层原理才能排查问题。2. 核心细节解析与实操要点2.1 AB包的组织方式与依赖规则打包之前先要搞清楚一个最基础的问题什么该打成一个包什么该拆开打官方给过一个很抽象的建议把“同时加载、同时更新”的资源放在同一个包把“可能被多人引用”的资源单独拆包。实际操作中我总结出这样一套相对稳妥的组织规则按UI界面维度每个主界面如主城、商店、背包、战斗结算打一个AB包。界面内所有图集、预制体、动画、音频资源都放进同一个包界面整体加载和卸载。这种方式对“界面型”游戏非常友好。按角色/单位维度每个角色或单位的所有资源打成一个AB包。模型、贴图、动画、技能特效预制体都包在一起用到这个角色时一次性加载切换角色时整体释放。公共资源单独拆包图集、Shader、公共字体、常用材质、公共粒子特效这些被多个包引用的资源必须单独打成公共包并且严格保证一个资源只属于一个AB包不允许重复打包。这套规则的底层逻辑是让资源To包是“多对一”的关系包To包是“一对多”的依赖关系且尽量无环。每个资源只归属一个包每个包依赖若干个公共包加载时只需要先加载被依赖的包然后加载目标包依赖树就清晰了。反面教材我在实际项目里见过很多美术把一堆散JPG贴图直接拖进Prefab程序打包时按文件夹打结果A工程的Prefab引用了B文件夹的贴图打出来的AB包A里多塞了一张B的贴图副本B的包更新了贴图A包里的副本还是旧版本。这种问题排查起来极其痛苦因为不用Binary比对工具根本看不出来。所以资源规范这件事一定要在项目早期就定死。2.2 打包参数与平台差异Unity官方的BuildAssetBundleOptions有几组参数是每一次打包都要认真选择的直接影响包体大小、加载速度和兼容性。先看压缩方式对应的是BuildAssetBundleOptions的三个选项LZMA压缩率最高但解压时要整包解压到内存加载慢且临时占内存大适合大体积的基础包或不常加载的资源包。LZ4压缩率略低于LZMA但支持按需读取加载时只解压用到的部分内存占用友好适合绝大多数运行中频繁加载的包。不压缩Uncompressed包体最大但加载最快适合追求极致加载速度且不Care磁盘占用的场景。我在移动端项目里的默认选择是LZ4。原因很简单现在包体大小瓶颈往往不在安装包而在热更新下载量LZ4的压缩率跟LZMA的差距在贴图这类“本身已压缩”的资源上并不大。而LZ4的按需解压特性对加载性能的提升是实打实的。还有一个Manifest文件的问题。BuildPipeline.BuildAssetBundles打包完成后会自动生成一个与输出目录同名的Manifest文件里面记录了所有AB包的哈希值、CRC校验码、依赖关系列表。这个文件是增量更新和依赖加载的“地图”运行时必须先加载它才能知道目标AB包依赖了哪些其他包。很多新手踩的坑就是只把AB包传到服务器上忘了把Manifest文件也一起传结果客户端一加载就报依赖缺失。平台方面AB包与平台强相关。在Windows上打出来的包Android上用不了iOS上也不行反过来同样。Shader的变体集合在不同图形API下的表现也不一样。所以打包机上必须配置成对应平台构建前切平台这一步是不能省的。同时注意同一个版本的同一批资源在不同平台上打出的AB包的Hash值也不一样所以更新服务器上的包必须严格按平台分目录存放版本清单也要按平台分别维护。2.3 原生打包方式与脚本化构建Unity编辑器手动打包很简单菜单里Windows - AssetBundle - Build AssetBundles就行或者编辑器扩展面板上点一下按钮。但生产环境绝不能依赖手动操作原因有三个人力资源浪费打包一次动辄十几分钟让程序干等就是浪费。手动操作不可复现今天少勾了个选项明天多选了个目录出的包就变了没法追溯。增量更新需要版本记录手动打包没法稳定生成可追溯的版本号。所以生产环境必须走脚本化构建。核心代码大致长这样using UnityEditor; using System.IO; public static class BuildBundleTool { // 用命令行参数触发打包也可以加到菜单里 [MenuItem(Tools/Build/AB)] public static void BuildAB() { string outputPath Assets/StreamingAssets/AB/ GetPlatformFolder(); if (!Directory.Exists(outputPath)) { Directory.CreateDirectory(outputPath); } BuildPipeline.BuildAssetBundles( outputPath, BuildAssetBundleOptions.ChunkBasedCompression, // LZ4 GetBuildTarget() ); } private static BuildTarget GetBuildTarget() { // 根据当前EditorUserBuildSettings.activeBuildTarget返回 return EditorUserBuildSettings.activeBuildTarget; } private static string GetPlatformFolder() { // 按BuildTarget返回对应平台目录名: Android/iOS/PC } }这段代码核心点在于传入的outputPath必须是已经存在的目录BuildAssetBundles不会自动帮你在还不存在的目录下输出结果会直接报错。另外每次打包前把旧的AB文件先清掉再重新打包避免上次构建残留的废弃AB包被一起打进更新清单白白增加玩家下载量。除了这些脚本化打包一定要带上打包时间和版本号信息。我习惯把版本号写进一个Version.txt文件放在输出目录里并同步生成一份所有AB包的哈希清单。这份清单既给增量更新系统用也给QA同事手动验证上传包有没有传错用。2.4 增量更新的核心逻辑与Manifest用法增量更新的核心是比较两次打包之间的差异。AB包的Manifest文件里记录了每个包的大小和Hash值Hash值变了说明内容变了没变说明这套资源跟上一个版本完全一致不需要重新下载。具体到客户端更新流程客户端启动后先去服务器拉取最新的Version.txt和主Manifest文件。比较服务器Manifest与本地缓存的Manifest找出Hash值不一致的AB包列表。只下载这些有变化的包下载完成后校验CRC再写入本地缓存替换旧包。这套逻辑里最容易出问题的点是Manifest文件本身是否有缓存。如果客户端缓存了旧的Manifest就拿旧依赖关系去解析新版资源很可能因为依赖包路径变了导致加载失败。所以Manifest文件和Version.txt这种元信息文件每次拉取都不允许走缓存必须强制重新下载。也因为增量更新依赖Hash比较打包机的时间稳定性就变得很关键。如果打包过程中有额外日志或文件写进AB目录会导致同一份代码资源在不同时间打出不同的包增量更新变成全量重下。这种情况我遇到过一次排查了好久发现是杀毒软件在打包输出目录里生成了临时文件把目录污染了。3. 实操过程与核心环节实现3.1 从零搭建原生AB打包流程现在带着你走一遍完整的从零搭建流程。这个流程默认是Unity 2021版本但也适用于2019 LTS和2018 LTS。第一步整理目录规范。在Assets下建立如下目录结构Assets/ ├── Art/ │ ├── UI/ │ │ ├── Atlas/ // 公共图集单独成包 │ │ └── Prefabs/ // 界面预制体按界面分目录 │ ├── Characters/ │ │ ├── Hero_001/ // 每个角色一个目录目录内资源统一打一个包 │ │ └── Hero_002/ │ └── Effects/ // 公共特效 ├── Scripts/ └── Editor/ └── Build/ // 编辑器扩展脚本放这里第二步给每个需要打AB的目录/资源设置AssetBundle名称。选中一个目录在Inspector底部AssetBundle输入框里填写包名。注意命名规范比如uiview_maincity、char_hero_001这样的格式包名可读性很重要别用数字ID命名不然打包脚本里报错你连哪个包都不认识。设置好名称后打包时AssetBundleName不为空的资源会自动归入对应的包未设置名称的资源则不会打包进任何AB包。第三步处理依赖资源分包的自动化检查。这一步非常关键但也最容易被忽略。你的工程里一定存在这种情况Hero_001目录里的模型材质引用了Art/Common目录下的一张共享受控贴图。如果Art/Common没有设置AB名称这张贴图会被自动打进Hero_001的AB包而Hero_002也引用了它又会被重复打包一次。为了杜绝这个问题可以写一个编辑器检查脚本在打包前扫描所有AB包中是否包含不属于该包目录的资源引用严格检查这套规则[MenuItem(Tools/Build/Check Asset Reference)] public static void CheckAssetReference() { // 遍历所有已设置ABName的资源 // 检查它的所有依赖资源是否也设置了ABName // 如果依赖资源没有ABName则给出警告 // 这个检查结果直接影响是否允许打包 }这类检查脚本建议加在打包流程的第一步一旦有未分包依赖就中断打包并报错。宁可第一天慢一点打这个“约束”也别在往后每一天都为资源乱引用买单。第四步写入程序集里的打包、打标记脚本把构建流程串起来。综合版大致是清理旧的AssetBundleName标记避免改名后的残留标记产生垃圾包按目录结构批量设置AssetBundleName执行资源引用完整性检查调用BuildAssetBundles生成哈希清单并打印打包耗时把一段不能手动干预的Check流程前置整体就能当自动化流水线跑。3.2 运行时加载与卸载的正确姿势打包完成后到客户端这边就是加载逻辑的设计。加载AB包的API并不多核心就这几个AssetBundle.LoadFromFile从本地文件加载LZ4和未压缩包推荐用这个方式边读边取资源不常驻内存整包。AssetBundle.LoadFromMemory把整个AB数据Load进内存再解析。这个API存在明显性能问题官方都不建议在移动端用主要给某些特殊场景应急使用比如AB数据是从不可写入磁盘的内存区域拿到的。我自己只在调试时用它生产环境坚决不用。UnityWebRequestAssetBundle从服务器下载远程AB包。下载完你可以选择缓存到本地下次走LoadFromFile路径或者不缓存直接LoadFromMemory。一套相对稳妥的加载流程是这样的根据某UI界面名查配置表拿到该界面AB包名和依赖列表。先检查所有依赖公共包是否已在内存中用字典记录不在则LoadFromFile加载。再加载目标界面AB包。从AB包中LoadAsset获取预制体。记录该界面对所有AB包的引用数并记录该AB包被哪些界面引用。对应的卸载流程是界面关闭后Unload所有从该AB包实例化出来的资源对象让引用计数减一。当某AB包引用计数归零调用Unload(false)表示移出AB包的元数据但允许实例化的对象继续在场景中存在。注意Unload(false)不等于销毁已实例化的物体它只是释放AB包本身的内存文件映射和资源头信息。实际对象还要等Destroy。这里有个常见误解——Unload(false)之后已经实例化的Prefab还能继续用但对Prefab内部的纹理、材质这些资源如果它们没有被Assets池回收再次加载同一个AB包时可能得到空引用。所以实践中我倾向于只对界面层的“临时资源”做Unload(false)场景级的常驻资源用Unload(true)做彻底清理。3.3 启动引导界面与版本检查实现一套完整的AB热更新通常需要一个启动引导场景。这个场景不加载任何AB资源只用最原始的Unity GUI或UGUI做进度条显示流程大致如下解析本地版本号。请求服务器版本信息。请求并下载最新Manifest。对比本地与远程Manifest计算出待下载AB包列表。串行或并行下载每下载一个更新UI进度条。下载完成后进入游戏主场景开始正常的AB加载流程。版本检查阶段最重要的细节是不要让这个流程卡死在网络超时。移动端网络环境很烂弱网、断网、切后台的情况都很常见。所以引导流程里要包含超时重试机制比如每个请求超时5秒失败后重试3次同时要支持断点续传下载UnityWebRequest自带这个能力但前提是服务器要支持Range请求头。断点续传这块多说一句。用UnityWebRequest下载AB包时如果下载了一半断网重试时服务器返回的响应头要支持Range这样可以只下载剩余部分。很多小公司自建的静态文件服务器没配置这个导致断点续传实际上是全部重下量大时很影响体验。using UnityEngine.Networking; IEnumerator DownloadAB(string url, string savePath) { var request UnityWebRequest.Get(url); request.downloadHandler new DownloadHandlerFile(savePath); yield return request.SendWebRequest(); if (request.result ! UnityWebRequest.Result.Success) { // 断线重试逻辑 yield return RetryDownload(url, savePath); } }3.4 多个AB包的依赖加载顺序问题当AB包之间的依赖关系复杂到一定层级时加载顺序就成了一个隐藏很深的“定时炸弹”。举例A包依赖B包B包依赖C包而A包是界面包B是角色包C是公共图集包。如果你的代码只加载A包A包内部的依赖标记会告诉Unity它依赖B和C但Unity不会自动帮你把B和C加载进来。你要自己写逻辑加载A之前先解析A的Manifest得到依赖包列表然后按依赖顺序逐级加载先加载C再加载B最后加载A。这个递归过程在包数量少的时候还好说几十个包打底的项目里如果每次都递归查询Manifest再顺序加载性能上会有开销。优化方式是把依赖关系做成一张缓存表启动时一次性解析主Manifest把“每个包依赖哪些包”解析进内存字典。加载某个包时直接查字典拿依赖列表而不是每条去读Manifest文件。依赖加载的另一个坑是循环依赖。AB在打包时遇到循环依赖会直接报错或打出异常包这就要靠前期的资源规范来避免。如果美术那边经常乱引用那就需要写出资源引用检查工具发现循环引用直接标记出来打回修改。4. 常见问题与排查技巧实录4.1 资源重复打包与包体积异常膨胀现象工程里总共200MB资源打出来的AB包加起来有400MB有些贴图在多个包中重复出现。排查思路Unity在打包AB时如果一个资源被多个AB包引用且它自己没有设置AB名称它会被分别嵌入每个引用它的包中。所以第一排查思路就是检查所有资源是否都正确设置了ABName。具体排查方法使用UnityEditor.AssetDatabase.GetDependencies过滤出所有被引用但没有ABName的资源列表逐个检查设置。这个问题的根治办法不只是“发现一个改一个”而是要在项目规范层面定死所有共享资源一律单独分包不想被打进AB包的资源如Shader也要设置一个专属包名哪怕是public包。没有ABName的资源默认就不允许被打进AB包里这条规矩要写进团队规范。4.2 加载AB后预制体资源丢失或显示异常现象LoadAsset成功获取Prefab实例化后模型是粉色Missing Shader、贴图全糊、或者干脆是空壳。排查思路这类问题绝大多数是依赖没加载完整造成的。只加载了顶层AB包没有把依赖包加载进来Unity运行时找不到Shader材质和图集贴图自然各种资源显示异常。排查步骤建议先通过Profiler检查运行时有没有报缺失资源的警告再用编辑器下的AssetBundle Browser查看目标包依赖列表确认依赖包数量是否被正确加载。实践中你若发现依赖包已经加载了但仍显示异常那就要考虑是不是版本不匹配。本地缓存的AB包跟服务器上资源不同步贴图被更新成了新版本但依赖包的Hash没匹配上也会出现这类问题。4.3 Unload时机不当导致的内存泄漏现象运行内存持续增长Profiler里能看到大量Texture、Mesh资源没有被回收。排查思路看Profiler如果AssetBundle Unload(false)之后资源仍没释放很可能是代码中持有对象引用。有经验的开发者都知道一个重要边界**Unload(false)只卸载AB包本身的内存映射不会卸载从该AB包创建出来的对象。**只有在加载时使用LoadAsset得到的Object如果不再使用要先Destroy并置空引用等场景切换或者GC后才真正释放。比较稳妥的做法是给每个加载出来的资源对象做生命周期管理界面关闭时统一销毁再Unload(false)。这样从源头杜绝忘记置空引用的问题。4.4 热更新下载大包失败率高的排查现象微端启动后10MB以内的小包下载很顺利100MB左右的大包经常下载失败或者进度条卡住。排查思路第一反应永远是看服务器配置而不是业务代码。确认服务器是否支持大文件断点续传Range请求头是否返回206而不是200CDN节点缓存策略是否把AB包缓存成了不可分段的状态。我也踩过一个更不起眼的坑某些负载均衡设备对超过50MB的文件有默认的读写超时配置导致下载到一半被无故断开。这种问题在测试环境下不会暴露直到上生产才可见。因此大包下载的容错机制一定要做失败自动重试、断点续传、重试次数限制、下载完成后CRC校验。5. 原生AB工作流的扩展与进阶思考5.1 从原生AB到框架方案的变化点了解完原生AB再去看Addressables和YooAsset你会有一种“原来如此”的感觉。Addressables做的事核心是把资源分类成GroupGroup又绑定到不同的BuildProfile然后它替你管理依赖追踪、远程加载、引用计数。本质它仍然调用BuildPipeline.BuildAssetBundles做底层打包只是在上面封装了一个可寻址的AssetReference系统。它的价值在于你不用再手动维护AssetBundleName甚至不用关心某资源被打进哪个AB包只要逻辑上引用它就行。YooAsset则是国内社区非常活跃的框架它吸收了Addressables的设计思路但又针对国内项目的热更新与资源分包场景做了很多增强比如预下载队列、强更弱更的模式、资源分析工具。它同样需要你理解AB原理否则连它报的错误都看不懂。5.2 用原生AB还是上框架的判断标准如果只看团队平均水平我的建议是5人以内小团队做中小型项目原生AB 自己的轻量封装就够想挑战一下完整的热更流程也行资源量庞大且团队超过10人直接上成熟框架把时间花在业务上。原生AB的优势是透明可控所有逻辑都是自己写的出问题愿意花时间都能查清楚。劣势是完整度不够一个可靠的原生AB工作流至少要包含构建工具、依赖解析、加载缓存、引用计数、更新器、清单校验、断点下载、补丁打包这么几大模块从头开发工作量不小踩坑成本也高。框架方案反过来Surface做好了一切但遇到问题要读懂框架内部实现才能定位调试难度不降反升。所以两者没有绝对的“谁更好”只有“谁更适合你的项目”。5.3 原生AB在小游戏平台的落地思路现在的Unity项目不止移动端和PC微信小游戏、抖音小游戏这类平台也占了不小比例。小游戏平台对AB的加载有一些特殊限制无法直接读本地文件路径需要走小游戏的本地缓存接口AB包内的代码逻辑无法直接执行需要配合WASM或Unity的WebGL构建。而且小游戏平台对大包体积卡得很严分包规则也更碎。所以AB在小游戏平台的落地通常需要把纹理、音频改成平台支持的压缩格式加载流程也要配合平台的缓存管理接口。原生AB在桌面和移动端跑通后迁移到小游戏平台仍需要不少定制工作如果你做的小游戏项目建议直接调研平台官方推荐的资源管理方案而不是硬套原生AB那套。说到底AB这套东西是多少年积累下来的核心资源管理结构它背后的概念——数据与元信息分离、依赖显式化、资源按需加载、增量差分更新——是放之四海而皆准的工程思想。你掌握了原生AB的完整工作流再去看任何资源管理框架都是一通百通这才是值得花时间吃透它的真正理由。最后分享一个经验之谈AB这套工作流在你首次全流程跑通之前的痛苦程度是最高的。但一旦跑通后面无论是出包、更新、查问题全链路都在你的掌控之中。这个通关后的控制感是直接用别人框架体会不到的。