资讯详情

Unity AssetBundle完整实战:打包策略、加载释放与依赖管理避坑指南

📅 2026/10/6 3:06:00 | 华诺云谱 👁 阅读
Unity AssetBundle完整实战:打包策略、加载释放与依赖管理避坑指南
AssetBundle下面简称 ABUnity 开发里绕不开的一个东西。只要你的项目不是开发完就打个整包丢给用户只要你有热更新、分包下载、控制安装包体积的需求AB 基本是绕不过去的那条路。不少朋友第一次接触 AB 是照着官方例子把 BuildPipeline.BuildAssetBundles 一敲打出几个文件运行时再 LoadFromFile表面能跑结果一上正式项目就乱了资源重复、内存涨个不停、卸载顺序不对导致黑屏、旧缓存一直下不下来。这篇文章就把我在实际项目里反复踩过、最后沉淀下来的完整流程整理出来——从打包配置、分组策略、构建脚本到运行时加载释放、依赖处理和问题排查全部带代码和避坑记录希望对正在做 Unity 客户端开发的你有帮助。1. AssetBundle 到底在解决什么问题1.1 AB 和普通资源管理方式的区别Unity 默认的资源管理方式其实就那几种场景里直接引用的资源、Resources 文件夹下的资源以及放在 StreamingAssets 里的裸文件。Resources 的好处是简单一个Resources.Load就能把资源取出来但坏处也很明显——所有放进 Resources 的内容都会进包包体越撑越大而且 Resources 不具备热更新能力想改一个模型、换一张图只能等版本发版。StreamingAssets 则更像是“随包携带的原始文件”可以自己控制加载逻辑但它不做依赖分析也不帮你管理引用基本等于把所有问题都抛给你。AB 在这套体系里的定位完全不一样它把资源按你定义的分组打成独立包可以放在本地也可以放在远端服务器运行时按需加载。因为你加载的是包不是单个资源文件所以 AB 天然会把资源的依赖关系带出去——比方说一个角色 prefab 引用了材质、贴图、动画片段这些内容会被自动收集进对应的 AB 包或者被单独打进一个公共包由 AB 的依赖机制来维护。这就在“原始文件”和“场景引用”之间找到了一个平衡点既能按需加载又能把引用关系管住。我用一个生活化的类比帮新手理解Resources 相当于把一个行李箱的所有东西摊开摆在桌面上使用方便但桌面面积有限StreamingAssets 相当于你背了一个大麻袋什么都往里塞但麻袋里头东西互相怎么关联得你自己记AB 则像是给每类东西装上独立的收纳盒盒子上还写明了标签和依赖清单要用哪个抽哪个不用的时候整盒撤走内存自然省下来。1.2 什么项目才需要上 AB要不要上 AB这是个需要冷静判断的问题。我见过不少小项目包体本身不到 100MB玩法也简单结果强行上一套 AB 流程更新策略、版本配置、资源服务器全都得搭结果收益极低维护成本却翻了几倍。对于纯单机、离线玩法为主、更新频率低的小型游戏老老实实把资源放在场景引用或者 Resources 里反而是最稳的方案。真正需要 AB 的场景通常是这几类一是包体敏感需要把首包控制在某个体积以下其余资源靠下载补齐二是内容更新频繁比如活动、新角色、新关卡不希望每次更新都发一整个安装包三是大世界或多模块架构某些模块只在特定阶段用到不需要常驻内存。判断标准其实就一句话你的资源是否需要“按需获取”或“按需释放”如果答案是肯定的AB 就是值得考虑的方向。我自己的经验是凡是需要后续持续运营的游戏AB 这一套迟早得补上来。早点规划好分组和版本命名后面加内容的时候会省很多事。如果项目暂时还没到这一步也建议先了解清楚至少知道它怎么工作别等到性能测出问题或者包体超标的时候再临时抱佛脚。2. 打包前的关键准备分组策略和打标方式2.1 在编辑器里怎么给资源设置 AssetBundle 名字AB 的粒度控制从给资源打标开始。在 Unity 编辑器里选中任意资源Inspector 面板最下方有一个AssetBundle下拉框你可以在里面选择一个已有的 bundle 名字或者直接输入新的名字它就会把当前资源归到你指定的包里。这个操作简单但资源一多就非常容易出错手动改 200 个资源能把人点到崩溃。所以实际项目里我更推荐用代码来批量打标既可控又方便后续调整。我经常在项目里放这样一个编辑器菜单using System.IO; using UnityEditor; using UnityEngine; public static class AssetBundleLabelTool { [MenuItem(Tools/AB/自动设置资源分包名称)] public static void SetBundleNamesByFolder() { string root Assets/Art/Characters; string[] guids AssetDatabase.FindAssets(t:Prefab, new[] { root }); int count 0; foreach (string guid in guids) { string assetPath AssetDatabase.GUIDToAssetPath(guid); string fileName Path.GetFileNameWithoutExtension(assetPath); AssetImporter importer AssetImporter.GetAtPath(assetPath); if (importer null) continue; importer.SetAssetBundleNameAndVariant(characters/ fileName.ToLower(), ); count; } AssetDatabase.SaveAssets(); AssetDatabase.Refresh(); Debug.Log($完成共处理 {count} 个资源); } }这段脚本做的事情很简单在指定目录里找到所有 Prefab把每个资源的 AssetBundle 名字设置为characters/文件名。注意我用了characters/作为前缀这实际上是通过路径分隔符来模拟目录层级构建完的 bundle 里也会保持这个层级关系方便后面按分类定位。SetAssetBundleNameAndVariant的第二个参数是变体名这里传空字符串表示不使用变体变体后面单独讲。打标工具的细节有两个必须注意一是AssetDatabase.FindAssets大部分情况下能匹配到资源但如果你有子文件夹嵌套记得检查返回结果是否包含你不想要的内容二是设置完名字后一定要AssetDatabase.SaveAssets加Refresh否则下次构建时可能取到旧状态。2.2 分组策略该把哪些资源放进同一个包打上标签只是第一步真正决定 AB 系统好不好用的是分组策略。分组分得好加载顺畅、内存省、更新粒度小分组分得烂依赖图乱成一团改一个资源要重新下载小半个项目。我这里给出几个我一直在用的分组原则按优先级排序按更新频率分。高频更新的资源活动 UI、新角色外观、数值配置放独立包低频更新的公共资源通用图标、基础材质、公共动画放稳定包。这样更新时只需要增量下载高频包稳定包可以走客户端本地甚至打进首包。按功能模块分。UI、角色、场景、特效这些大类至少分开。我习惯用ui/、characters/、scenes/、fx/做前缀让包名自带分类信息。共享资源单独成包。如果多个 AB 都依赖同一张贴图、同一个材质球把这个共享资源单独打进shared/包否则它会被重复打到每个引用了它的包里导致内存白白翻倍。粒度要适中。单个资源打一个包不是不行但依赖管理的开销会极大反过来把整个项目打成一个包那还不如直接用 Resources。一个包里的资源尽量控制在“它们会同时被用到”的范围内。分组没有绝对标准但你可以用这个公式来做判断依赖关系紧密且生命周期一致的内容放进同包生命周期不同的内容拆开。举个例子UI 主界面和登录界面的图标虽然都是 UI但登录界面只在启动时出现主界面长期常驻那它们就应该拆成两个包——登录界面的包用完可以立刻卸载不会拖累主界面资源。3. 构建脚本把 AB 真正“打”出来3.1 一个干净的构建脚本长什么样给资源打上 AB 名字后下一步就是走BuildPipeline.BuildAssetBundles。这一步我建议用编辑器菜单来做而且要写成脚本方便接入 CI 自动打包。我的常规构建脚本长这样using System.IO; using UnityEditor; using UnityEngine; public static class BundleBuilder { private const string OutputRoot Assets/StreamingAssets/AB; [MenuItem(Tools/AB/Build/Windows)] public static void BuildForWindows() { BuildBundle(BuildTarget.StandaloneWindows); } [MenuItem(Tools/AB/Build/Android)] public static void BuildForAndroid() { BuildBundle(BuildTarget.Android); } public static void BuildBundle(BuildTarget target) { string platformDir target.ToString(); string outputPath Path.Combine(OutputRoot, platformDir); if (Directory.Exists(outputPath)) Directory.Delete(outputPath, true); Directory.CreateDirectory(outputPath); // 指定构建参数LZ4 压缩 确定性构建 AssetBundleManifest manifest BuildPipeline.BuildAssetBundles( outputPath, BuildAssetBundleOptions.ChunkBasedCompression | BuildAssetBundleOptions.DeterministicAssetBundle, target); if (manifest null) { Debug.LogError([AB] 构建失败请查看 Console 日志); return; } // 将本次构建的 Hash 记录到版本文件供更新逻辑使用 File.WriteAllText(Path.Combine(outputPath, version.txt), manifest.GetAssetBundleHash(StreamingAssets).ToString()); AssetDatabase.Refresh(); Debug.Log($[AB] 构建完成输出目录{outputPath}); } }先把构建输出放在Assets/StreamingAssets/AB下是因为 StreamingAssets 随包打包会连同这些 AB 一起进整包方便做本地包资源。如果你只做线上下载模式目录可以放在项目外的任意路径只要构建机和发布流程能拿到产物就行。BuildPipeline.BuildAssetBundles的返回值是AssetBundleManifest这个对象记录着本次构建的所有 bundle 的哈希值。我在写版本文件时取了一个叫 StreamingAssets 的 bundle 的哈希这是 Unity 自动生成的入口 bundle代表整包资源的版本指纹。实际项目中你会发现构建输出目录底下多了一个同名文件夹和.manifest文件那就是一个代表“所有包的清单”的特殊 bundle运行时必须先加载它才能拿到依赖关系后面第四章会用到。3.2 构建参数和压缩方式怎么选刚接触 AB 的人经常会对着BuildAssetBundleOptions的枚举发懵不知道该怎么组合。我把常用的几个解释一遍你就明白它们各自影响什么了。BuildAssetBundleOptions.None是什么压缩都不加包体积最大但加载时不需要解压相当于直接内存映射UncompressedAssetBundle和它类似但语义上更明确ChunkBasedCompression是 Unity 5 之后推荐的 LZ4 压缩它会按块压缩加载时按块解压速度和空间比较平衡还有一档LZMA压缩率最高但整个包解压要一次性完成首次加载有明显的卡顿。这里的取舍和我实际工程里的选择本地资源用ChunkBasedCompression远端更新的资源用LZMA 缓存策略把“首次下载体积小”优先再靠缓存化解解压耗时。DeterministicAssetBundle这个选项我要额外提一下。它保证相同资源和相同版本下构建出来的包哈希一致。这在增量更新里非常关键——如果你每次构建都在内容没变的情况下拿到不同的 Hash客户端就会误判“资源已更新”反复下载相同内容流量和体验都会出问题。所以 CI 上构建时我基本都会带它。另外有两个容易混淆的参数ForceRebuildAssetBundle和IgnoreTypeTreeChanges。前者强制重新构建所有 bundle无视资源时间戳通常只在排查异常时用正常不用加因为它会拖慢整个构建流程后者允许 Unity 跳过类型树的变更检测一般配合代码热更使用普通项目不用碰。3.3 构建产物的版本管理和 CI 接入AB 构建不是打完就结束了你还需要把产物和版本记录一起管理起来。这里分享一个思路构建完成后把输出的每个 bundle 的 Hash 写入一个本地文件或者服务器上的版本清单客户端启动时先请求这份清单对比本地已下载的版本决定哪些 bundle 需要重新下载。如果你们公司有 Jenkins 或者其他 CI 系统构建命令可以走-batchmode -quitUnity.exe -batchmode -quit -projectPath D:/MyProject -executeMethod BundleBuilder.BuildForAndroid -logFile build.log这一步容易踩的坑是执行方法名必须写全包括静态类名和方法名而且这个类必须放在Editor文件夹下否则 Unity 编辑器模式下不会编译它CI 根本找不到入口。我还在build.log输出日志的习惯出问题可以直接翻日志定位比在服务器上瞎猜高效得多。版本管理的另一个细节构建脚本里输出的version.txt内容不要只放一个 Hash建议把时间戳、分支名、构建机器环境也一起写进去格式随意但至少保证“能一眼看出这次构建是哪个分支、哪台机器、哪个时间点产出的”。排查线上问题的时候这些信息能帮你快速锁定是不是构建产物的版本错乱导致资源异常。4. 运行时加载与释放这章最容易出幺蛾子4.1 四种加载方式到底该怎么选运行时把 AB 拉起来Unity 给了好几种 API很多人分不清用哪个。我把它们按使用场景排一下你对着自己的情况做选择。AssetBundle.LoadFromFile/LoadFromFileAsync从本地文件路径加载不走内存拷贝加载速度最快内存占用最低。只要你的 AB 是本地文件包括从 StreamingAssets 读优先用这个方法。AssetBundle.LoadFromMemory/LoadFromMemoryAsync接收byte[]需要先将整个包的字节读进内存再解析。适合你已经通过自己的下载逻辑拿到了完整字节流的场景。但注意加载过程中它会复制一份数据大包容易峰值内存翻倍。UnityWebRequestAssetBundle.GetAssetBundle配合 URL 加载内部有缓存机制可以避免重复下载。适合远端资源但要注意它默认的缓存路径和版本管理方式需要传Caching.GetCache或使用哈希版本做缓存键。AssetBundle.LoadFromStream/LoadFromStreamAsync自定义流加载适合从不可直接寻址的路径读取的场景比如某些平台沙盒能力有限时但用得相对少。实际项目里我最常用的是LoadFromFileAsync和UnityWebRequestAssetBundle的组合本地打包资源用前者线上更新资源用后者。下面是我在项目里使用的简化版 BundleManager 核心逻辑using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.Networking; public class BundleManager : MonoBehaviour { private Dictionarystring, AssetBundle loadedBundles new Dictionarystring, AssetBundle(); private Dictionarystring, string localBundleRoot; private AssetBundleManifest manifest; private string bundleBasePath; public IEnumerator Init(string localBasePath) { bundleBasePath localBasePath; // 1. 先加载主 manifest所有依赖查询都靠它 AssetBundle manifestBundle AssetBundle.LoadFromFile(Path.Combine(localBasePath, StreamingAssets)); if (manifestBundle null) { Debug.LogError([AB] 主 manifest 加载失败); yield break; } manifest manifestBundle.LoadAssetAssetBundleManifest(AssetBundleManifest); manifestBundle.Unload(false); } public IEnumerator LoadBundle(string bundleName) { if (loadedBundles.ContainsKey(bundleName)) yield break; // 2. 先保证依赖包齐全 string[] deps manifest.GetAllDependencies(bundleName); foreach (string dep in deps) { yield return LoadBundle(dep); } // 3. 本地优先本地缺失再走下载 string localPath Path.Combine(bundleBasePath, bundleName); if (File.Exists(localPath)) { AssetBundle bundle AssetBundle.LoadFromFile(localPath); if (bundle ! null) loadedBundles[bundleName] bundle; yield break; } // 4. 远端下载利用 UnityWebRequest 的缓存机制 string url RemoteConfig.BundleServerUrl / bundleName; using (UnityWebRequest request UnityWebRequestAssetBundle.GetAssetBundle(url)) { yield return request.SendWebRequest(); if (request.result ! UnityWebRequest.Result.Success) { Debug.LogError($[AB] 远端加载失败: {bundleName}, {request.error}); yield break; } DownloadHandlerAssetBundle handler request.downloadHandler as DownloadHandlerAssetBundle; if (handler ! null) { loadedBundles[bundleName] handler.assetBundle; } } } public T LoadAssetT(string bundleName, string assetName) where T : Object { if (loadedBundles.TryGetValue(bundleName, out AssetBundle bundle)) { return bundle.LoadAssetT(assetName); } Debug.LogWarning($[AB] bundle 未加载: {bundleName}); return null; } }这段代码把依赖加载、本地加载、远端下载合并到了一个流程里逻辑上能跑通。你需要注意几个我自己踩过的点第一个是 manifest 加载完要立刻Unload(false)因为它只用来读依赖信息不释放会占用一份内存第二个是加载依赖的顺序先依赖后主包否则主包里引用的资源还没就位后面 LoadAsset 可能拿到空引用第三个是下载失败的错误处理这里只打了日志真实项目至少要做重试和失败回滚避免半加载状态污染了loadedBundles。4.2 卸载的坑Unload(false) 和 Unload(true) 到底怎么选AB 的卸载是一个永远绕不开的陷阱。AssetBundle.Unload有两个参数false和true很多初学时理解反了结果要么泄漏要么黑屏。Unload(false)的意思是卸载 bundle 的元数据和资源清单但保留已经加载出来的对象。举个例子你用bundle.LoadAssetGameObject(Hero)加载了一个角色预制体然后调用Unload(false)那这个GameObject对象仍然存在你可以继续实例化和使用但后续如果再想用bundle.LoadAsset加载其他资源就会拿不到。这时你手里等于是一堆“没有来源”的资产它们的引用计数和原生资源不会即时释放只有等你主动销毁并调用一次Resources.UnloadUnusedAssets才会真正清理。Unload(true)则很直接卸载 bundle 同时销毁所有由它加载出来的资源对象。如果你用了Unload(true)之前 Load 出来的Texture、Mesh、GameObject引用会变成僵尸对象再点它们就会报“已被销毁”的错误。所以Unload(true)的适用场景只有一种确认这个 bundle 里的内容在卸载后不再需要包括已经加载出来的资源。那正确的用法是什么我给出一个比较稳的套路如果 bundle 使用频率低、生命周期短比如关卡资源那么在关卡结束切场景时先Resources.UnloadUnusedAssets()确保不再有对象引用这些资源再对 bundle 调用Unload(true)如果 bundle 里有些资源是要长期保留在主界面、常驻内存的那就不要贸然在运行中卸载它等主界面切换或者进设置页时再统一处理。这里有个非常容易翻车的场景你加载了一个 UI bundle里面有一张主界面的背景图和一组设置页图标。你在主界面加载完背景图然后为了省内存对 bundle 调了Unload(true)结果用户切到设置页时那一组图标全变成粉色缺图或报错。因为你把整个 bundle 的资源一起销毁了而不是只释放暂时用不到的那部分。这种需求就不适合直接在运行中卸载更应该把主界面和设置页拆成两个独立 bundle分别管理。4.3 内存监控和重复资源检查AB 用久了内存问题往往不是某一个包太大而是重复加载、没有释放导致的累计泄漏。我自己排查这类问题的路径是这样的先用 Unity Profiler 打开 Memory 面板看AssetBundle、Texture2D、Mesh、AnimationClip这几类对象在游戏运行期间是不是只增不减。如果发现同一种贴图出现了多份那就很可能是同一个资源被不同 bundle 重复打包了。重复打包的根源通常是共享资源没有单独打包。解决方法是把公共贴图、通用材质放到一个shared/包然后让所有引用了它们的包都从shared/里取。如果你怀疑现有项目已经有重复可以写个编辑器检查脚本扫一下所有 bundle 里的资源路径[MenuItem(Tools/AB/检查重复资源)] public static void CheckDuplicateAssets() { string[] bundleNames AssetDatabase.GetAllAssetBundleNames(); Dictionarystring, string pathOwner new Dictionarystring, string(); foreach (string bundleName in bundleNames) { string[] assetPaths AssetDatabase.GetAssetPathsFromAssetBundle(bundleName); foreach (string path in assetPaths) { if (pathOwner.TryGetValue(path, out string oldBundle) oldBundle ! bundleName) Debug.LogWarning($[AB] 重复资源: {path} 同时属于 {oldBundle} 和 {bundleName}); else pathOwner[path] bundleName; } } }这个工具能帮你在编辑器阶段抓到重复打包问题上线运行前先跑一遍。要注意GetAssetPathsFromAssetBundle只能拿到显式归属的资源间接依赖的共享资源不一定在列表里所以它适合做第一道筛查不能完全替代对依赖树的检查。5. 依赖管理和变体最容易把项目搞崩的部分5.1 依赖包加载漏了资源就是一片粉AB 的依赖管理是整套机制里最容易出问题的环节。一个主 prefab 引用了若干材质和贴图这些共享资源如果被单独打到了shared/包那么加载主 prefab 包之前必须先把这个共享包也加载起来。很多人漏了依赖加载结果运行时间LoadAsset出来的 prefab 虽然非空但材质是粉色的、贴图是空白。解决依赖加载的核心是AssetBundleManifest.GetAllDependencies我在 4.1 的代码里已经用了。这里要补充一个细节GetAllDependencies返回的是所有依赖的包名数组包括依赖的依赖但注意它可能返回依赖包本身也算进去实际加载时你要做去重避免同一个依赖包加载两次导致第二次返回another bundle with the same files is already loaded的报错。我在多个项目里用下来的依赖加载顺序是先加载主 manifest再递归加载所有依赖包最后再加载目标主包。每次加载前先查字典已经加载过的直接跳过。如果依赖包加载失败一定要中断后续流程不要冒着半截状态继续往下走宁可让这次资源加载失败并走错误重试也不要让脏状态在内存里蔓延。5.2 变体Variant到底怎么用才不翻车AB Variant 允许你为同一套资源准备不同版本比如同一张界面在不同分辨率下的图集或者同一角色在高画质和低画质下的贴图。设置方式是在打标时把SetAssetBundleNameAndVariant的第二个参数填上例如hd和sd同一个资源的两个版本名字相同变体不同。变体的运行机制是Unity 会把你指定的当前平台变体对应的资源解析进主包这样LoadAsset时你用的资源名字不需要带变体后缀系统自动根据激活的变体选择对应资源。这听起来方便但坑也很深变体和原 bundle 必须保持完全相同的资源路径和文件布局否则加载会出现“资源找不到”的诡异问题而且一次只能激活一个变体想切换必须重新加载相关 bundle做不好就是资源错乱。我个人的建议是项目初期先别急着上变体等确定跨分辨率、跨平台的需求确实出现后再引入变体机制。变体带来的复杂度比普通 AB 高不少尤其是在依赖共享资源时很容易出现变体没有正确匹配导致加载失败的情况。如果非用不可至少保证每套变体都单独验证过加载和卸载流程不要到集成测试阶段才暴露问题。6. 常见报错、排查步骤和我的经验总结6.1 几个高频报错速查表我把这些年遇到的高频错误整理成了一个速查表你可以直接对照排查错误现象可能原因解决思路The AssetBundle xxx cant be loaded because another AssetBundle with the same files is already loaded同名 bundle 被重复加载且底层文件路径重复检查依赖递归是否去重检查loadedBundles字典是否覆盖旧包加载出来的 Prefab 材质是粉色共享资源依赖包未加载用manifest.GetAllDependencies先加载依赖AssetBundle.LoadAsset返回 nullbundle 已卸载或资源名和实际路径不一致确认加载顺序检查Unload是否误执行资源名用完整路径名称同一份资源在内存里出现多份共享资源被重复打进多个 bundle用检查重复资源的编辑器工具扫一遍把共享资源单独成包远端下载的包加载后 Hash 对不上版本缓存过期或者下载中断清除Caching缓存确认服务器版本文件和客户端逻辑一致GetAssetPathsFromAssetBundle找不到资源资源未正确打标或被打进依赖未显示检查 Inspector 里资源是否被分配 bundle 名重新打标后构建6.2 一个排查 AB 内存泄漏的实操案例去年我经手的一个项目玩家反馈玩一段时间后内存持续上涨明显是泄漏。当时我先打开 Memory Profiler 采集了一个内存快照然后发现场景里Texture2D数量非常大点进去看具体资源路径有大量相同名字的 UI 贴图副本每份大约占用 2MB 到 4MB 不等。再查这些贴图来自哪个 AB发现是 UI bundle 里放了一套公共图标但每个功能模块都打了一个 bundle且公共图标被重复打进了这些功能模块的 bundle 里。问题的根源不是没有释放而是同一份资源被打了多份只要加载不同模块就会重新实例化一份。修复方案其实很粗暴把所有公共图标统一挪到shared_ui/包功能模块包只保留模块特有的资源修改构建脚本重新打一次运行后再看Texture2D数量降到之前的一半以下。这个案例说明排查 AB 内存问题先别急着看代码释放逻辑先看是不是重复打包把内存撑起来了。6.3 关于 AB 的几条经验总结说几个我反复踩过、最后内化成规则的东西。第一AB 的构建结果要能和版本号强绑定不要在每次构建时都无条件重新下载所有资源。你需要在版本文件里写入 bundle 的 Hash客户端根据 Hash 判断是否要重新下载。尤其是DeterministicAssetBundle一定要开否则内容没变Hash 每次都变增量更新就形同虚设。第二移动平台上注意 StreamingAssets 的路径问题。Android 的Application.streamingAssetsPath实际指向 APK 包内路径直接用File.Exists判断会失败要先走UnityWebRequest或者用AndroidJNI那套方式访问。很多开发者第一次做移动端 AB 都在这个路径上卡住。第三修改了打包脚本或资源结构后先删掉旧的 StreamingAssets 缓存再构建否则可能出现增量残留。我习惯在构建脚本开头直接Directory.Delete(outputPath, true)清空输出目录宁可多花一点全量构建时间也避免掩耳盗铃式的增量问题。最后再分享一个小技巧AB 加载逻辑一定要做成可日志化、可开关的。我会在 BundleManager 里加一个bool debugBundle字段开关打开时每次加载都输出 bundleName、依赖数量、加载耗时和是否走缓存。项目出了资源问题先开日志跑一遍大多数情况下问题原因就写在日志里了不需要靠猜。AB 这套东西单看每个 API 并不难难的是它和项目结构、版本管理、更新策略纠缠在一起后的整体设计。我写了这么多年客户端回头看最实用的建议还是那句一切从分组做起从构建脚本做起从依赖关系做起先把地基打稳再考虑各种花哨的高级特性。希望对你的项目有帮助。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑