
图集1. 问题概述在 Unity Addressables 项目中一个常见但容易误解的现象是只修改或新增了几张 UI 图片最终却有大量 Prefab 所在的 AssetBundle 发生变化热更包远大于图片本身。这通常不是 Prefab 被批量修改也不一定是打包过程随机漂移而是图集作为公共依赖发生变化后依赖身份沿 Addressables 的 Bundle 依赖图向上传播导致直接或间接依赖该图集的 Bundle 获得新的 BundleHash 和文件名。典型链路如下Sprite 新增、删除、像素或导入设置变化 - SpriteAtlas 重新 Pack - AtlasCache / 图集纹理内容变化 - 图集 Bundle 的内容与 BundleHash 变化 - 依赖图集的 UI / Prefab Bundle 依赖信息变化 - 这些 Bundle 获得新的 BundleHash 或构建结果 - Catalog 将其识别为新版本 - 客户端需要重新下载如果变化的是一个被大量界面引用的公共图集几 KB 的源图片变化可能放大成数十 MB 的热更下载量。2. 先区分三种“变化”分析报告前必须区分以下三类变化。2.1 源资源变化源文件或导入结果确实发生变化例如PNG 像素、尺寸、透明通道变化新增或删除 Sprite.meta中的 TextureImporter、压缩格式、Sprite 切片信息变化SpriteAtlas 的 Packables、Packing Settings、Platform Settings 变化Unity 或构建脚本改变了最终导入结果。这是最上游的真实变化。2.2 Bundle 内容变化图集重新 Pack 后生成的纹理页、Sprite 映射或序列化数据发生变化图集 Bundle 的二进制内容与 BundleHash 随之变化。图集的实际纹理页通常表现为Library/AtlasCache/...变化。即使.spriteatlas文件本身没有 Git diff只要其成员 Sprite 变化AtlasCache 仍会重新生成。2.3 依赖身份变化Prefab 或其他显式资源自身的 AssetHash、资源集合都没有变化但其依赖的图集 Bundle 已经变成了新版本于是承载 Prefab 的 BundleHash 也发生变化。因此“某个 Prefab Bundle 变化”并不等于“Prefab 文件被修改”。更准确的表达是Prefab 所在 Bundle 的显式资源内容没有变化但它记录或参与计算的依赖关系发生了变化最终生成了新的 Bundle 标识或构建结果。3. SpriteAtlas 为什么会因为成员图片变化而变化SpriteAtlas 是一份打包规则和成员集合描述不是最终运行时纹理本身。构建时Unity 会根据 SpriteAtlas 配置收集 Sprite读取各 Sprite 的导入结果并执行裁剪透明区域旋转、紧密打包和排列Padding、Extrude 等边缘处理根据平台设置进行纹理压缩生成一个或多个图集纹理页生成 Sprite 到图集区域的映射及相关序列化数据。所以以下任何一项变化都可能让整张图集重新生成增加一张图片删除一张图片替换图片像素改变图片宽高改变单图或图集的平台压缩设置改变 Max Texture Size、Padding、Rotation、Tight Packing改变 Packables 所指向的文件夹内容改变 Sprite 切片、Pivot、Border 或 Mesh Type。图集采用二维排布。一张图片的尺寸变化可能改变后续许多 Sprite 的排布位置甚至使一页图集变成多页。因此不能把图集变化量简单理解为“被修改图片的压缩后大小”。3.1.spriteatlas没有 Git diff图集仍会变化这是正常现象。例如 SpriteAtlas 的 Packables 指向一个目录Assets/LoadResources/UI/Common/Sprites向该目录新增 PNG 后.spriteatlas配置文件不需要变化但构建时收集到的成员集合已经不同最终图集自然不同。判断图集是否真实变化应查看成员 Sprite 及其.metalayout 中图集资源的 AssetHashLibrary/AtlasCache页面图集 Bundle 解包后的纹理与序列化对象而不能只检查.spriteatlas的 Git diff。4. 为什么依赖图集的 Prefab Bundle 也会变化4.1 Prefab 引用的不是一份独立图片副本UGUI 的Image通常通过 Sprite 引用指向某个资源。该 Sprite 被 SpriteAtlas 收集后运行时还需要加载对应图集页才能取得纹理数据。当 Prefab 与图集被打进不同 Bundle 时关系近似为UIPrefab.bundle - SpriteAtlas_Common.bundlePrefab Bundle 是依赖者图集 Bundle 是被依赖者。4.2 Addressables 必须维护一致的依赖版本Addressables Catalog 和 AssetBundle 构建结果需要明确当前资源位于哪个 Bundle加载它之前需要加载哪些依赖 Bundle这些 Bundle 对应哪个构建版本和文件缓存中已有的 Bundle 是否仍可复用。图集 Bundle 变化后如果依赖者仍保留旧的依赖身份就可能出现 Catalog 指向新图集但依赖者仍与旧构建结果绑定的情况。为了保证依赖图与缓存一致构建系统会重新计算相关 Bundle 的构建哈希。最终常见结果是旧UIHome_abcd.bundle - CommonAtlas_1111.bundle 新UIHome_efgh.bundle - CommonAtlas_2222.bundle即使UIHome.prefab的 YAML 一个字节都没变UIHome所在 Bundle 也可能获得新的 BundleHash 或文件名。4.3 直接依赖与间接依赖传播不仅发生在直接引用图集的 Bundle 上也可能沿依赖图继续向上传递页面 Bundle - 公共控件 Bundle - 公共图集 Bundle公共图集变化后公共控件 Bundle 的依赖身份变化如果页面 Bundle 又依赖公共控件它也可能受到影响。这就是依赖级联。4.4 “Bundle 变化”不一定等于“业务内容重新序列化”需要谨慎区分Prefab 的业务字段是否变化Prefab 的 AssetHash 是否变化承载 Prefab 的 BundleHash 是否变化Bundle 二进制是否存在对象级内容变化只是 Bundle 设置或依赖集合发生变化。如果报告将其归入Bundle Setting/Dependencies Changed它表达的是显式资源内容未变化、BundleHash 变化。此时不应表述为“所有 Prefab 都被修改了”。5. 为什么公共图集特别容易放大热更设一个公共图集自身 Bundle 为 3 MB被 150 个 UI Bundle 依赖这些 UI Bundle 平均约 0.2 MB。修改一张几 KB 的图片后热更量可能近似为图集 Bundle 3 MB 依赖图集的 UI Bundle 30 MB 其他直接修改内容 5 MB -------------------------------- 热更下载量 38 MB真正修改的源图片可能只有几十 KB但客户端按 Bundle 下载无法只下载 Bundle 内变化的几 KB。影响程度主要由以下因素决定图集 Bundle 自身大小直接和间接依赖该图集的 Bundle 数量依赖者 Bundle 的总大小公共图集变更频率Bundle 命名是否包含 HashCatalog 和缓存更新策略是否使用内容更新构建及相应限制。6. 治理方案6.1 按业务域拆分图集这是处理高 fan-out 的首选方案。不推荐SpriteAtlas_Common 商店图标 活动图标 首页图标 资源栏图标 图鉴图标 通用按钮推荐SpriteAtlas_CommonStable 长期稳定的基础控件 SpriteAtlas_Shop 商店 SpriteAtlas_ResourceBar 资源栏 SpriteAtlas_Home 首页 SpriteAtlas_Activity_xxx 活动 SpriteAtlas_IllustratedBook 图鉴拆分原则不是“图越少越好”而是同时考虑业务边界加载生命周期变更频率使用范围图集利用率Bundle 数量和请求数量。6.2 公共图集只放稳定资源公共图集适合长期不变的按钮底图多模块共享且版本稳定的基础图标与整个 UI 生命周期一致的资源。公共图集不适合频繁更新的活动资源新功能迭代中的临时图标只在单个页面使用的业务图片商店商品、赛季、礼包等高频运营内容。公共依赖的关键风险不是大小本身而是风险约等于依赖者总大小 x 变更频率6.3 高频运营资源独立管理对于更新频繁且只在有限页面使用的 Sprite可以使用业务独立 SpriteAtlas按活动或版本拆图集必要时将单图作为独立 Addressable对远程运营图片使用专门的下载和缓存系统。代价是 Bundle 数、网络请求数、内存碎片和加载复杂度可能上升需要结合实际运行指标权衡。6.4 控制图集收集目录将一个大目录直接作为 Packable 使用方便但容易让新图片意外进入公共图集。建议公共图集使用明确、受控的资源目录新业务资源默认放入业务目录Code Review 检查新增图片落点避免一个目录同时被多个启用的 SpriteAtlas 收集建立图集成员列表或归属检查工具。6.5 避免图集相互重叠两个 SpriteAtlas 收集同一文件夹或同一 Sprite会造成打包归属不稳定或难以理解AtlasCache 冗余依赖图复杂一次图片变化影响多个图集冗余纹理和热更量增加。应扫描所有启用图集的 Packables GUID检查是否有同一目录被多个图集收集。6.6 保证构建可复现并非所有图集变化都来自 Git。若相同 Revision、相同参数重复构建仍产生不同图集 Hash应检查Unity 和 Addressables/SBP 版本是否一致构建机 Library、ArtifactDB、AtlasCache 状态构建前是否存在资源覆盖或动态生成平台压缩工具版本TextureImporter 是否在构建过程中被脚本修改SpriteAtlas 后处理脚本是否依赖时间、遍历顺序或机器状态Git LFS 文件是否完整大小写路径与 GUID 是否一致构建前清理策略是否一致。复现测试应使用完全相同的Git Revision 子模块 Revision 构建参数 Unity 版本 构建机/缓存策略8.7 不要盲目拆成大量小 Bundle拆分可以降低热更放大但过度拆分也有成本Catalog 条目增多网络请求增多Bundle Header 和文件系统开销增加加载调度更复杂内存与缓存碎片增加首屏加载可能出现更多依赖等待。最佳粒度通常是“相同业务、相同生命周期、相近变更频率”的资源放在一起而不是每张图片一个 Bundle。7. 容易采用但无效的处理方式7.1 只优化 PNG 文件大小压缩 PNG 可以减少图集自身大小但无法解决大量依赖 Bundle 换 Hash。根因是 fan-out 时应优先缩小依赖范围。7.2 只确认.spriteatlas没修改成员图片和.meta变化同样会重新生成图集。必须检查完整成员集合。7.3 把所有变化都归因于 Unity 随机性如果依赖图能从 changed atlas 反向到达绝大多数 rehashed Bundle这就是可解释的确定性级联不是随机构建。7.4 强行保持依赖者 Bundle 文件名不变这可能破坏 Catalog、缓存和依赖版本一致性。正确方向是调整资源边界和依赖结构而不是绕过 Hash 更新机制。7.5 每次构建都彻底删除全部缓存清缓存有助于验证可复现性但不是公共依赖 fan-out 的解决方案。确定性依赖级联在干净缓存下仍然会发生。8. 推荐的日常防线提交前新图片是否进入了Common等高 fan-out 目录图片是否真的跨多个模块复用.meta是否一并提交是否被多个 SpriteAtlas 重复收集是否改变了图片尺寸或平台导入设置。构建后热更大小是否与真实改动量成比例Bundle Setting/Dependencies Changed是否突然增加哪个真实变化源触发的下游 Bundle 最多冗余资源数是否上升相同 Revision 重复构建能否得到稳定结果。发布前阈值建议项目可逐步建立自动告警例如单个资源导致超过 50 个 Bundle rehash单个资源导致超过 10 MB 下游下载依赖变化占热更包超过 50%无法解释的 abnormal Bundle 超过指定数量或大小公共图集在非整包窗口发生变化。阈值应根据项目正常基线调整不宜直接照搬。9. 常见问题Q1Prefab 没有 Git diff为什么它的 Bundle 仍然变化因为 BundleHash 不只反映 Prefab 文件本身还可能受 Bundle 设置和依赖身份影响。图集依赖变化后Prefab 所在 Bundle 可能需要获得新的构建 Hash。Q2图集配置没变为什么图集变了图集是根据成员 Sprite 构建的。成员图片、.meta或导入结果变化都会改变最终 AtlasCache 和图集纹理。Q3这是不是 Addressables 的 Bug依赖变化导致依赖者 Hash 更新通常是设计行为。真正需要治理的是公共依赖范围过大、图集变更频繁或构建不具备可复现性。Q4把图集和所有 Prefab 打进同一个 Bundle 能解决吗可能减少跨 Bundle 依赖但会形成更大的整体更新单元。任何一处变化都可能重下整个大 Bundle通常只是把问题从依赖级联变成大包整体更新。Q5把每张图片都作为独立 Addressable 是否最好不是。它会增加 Bundle、请求、Catalog、加载调度和内存管理成本。应按业务、生命周期与变更频率做适度拆分。Q6怎样证明是正常级联而不是非确定性 rehash使用旧、新 layout 建依赖图。如果 rehashed Bundle 能沿依赖关系追到真实变化的图集并且相同 Revision 重复构建结果稳定就是确定性级联。如果大量 Bundle 无法追到任何真实变化应进一步检查参数、缓存和构建工具链。10. 总结图集变化引起其他 Bundle 变化本质上是三个机制叠加SpriteAtlas 会根据成员 Sprite 重新生成实际纹理页Addressables/SBP 会将 Bundle 内容、设置和依赖关系纳入构建结果Bundle 是客户端下载和缓存的最小更新单元无法只下载其中变化的几张图片。所以真正有效的优化方向不是阻止 Hash 正常变化而是降低高频资源进入公共图集的概率按业务和变更频率拆分图集缩小公共依赖的 fan-out用 layout 依赖图区分正常级联和异常 rehash保证构建参数与缓存策略可复现建立热更放大倍数和异常依赖的持续监控。一句话概括公共图集越稳定越好变化越频繁的资源依赖边界应越局部。Shader1. Shader 变化会不会连带 Material 和 Prefab Bundle图集的结论不能原样套用到 Shader。假设三者分别打成三个 BundlePrefab.bundle - Material.bundle - Shader.bundleShader 变化后可以确定的是 Shader Bundle 的内容可能发生变化Material Bundle 和 Prefab Bundle 是否继续变化则取决于跨 Bundle 的对象映射、序列化数据和构建使用信息是否也发生变化。更准确的判断是Shader.bundle 通常变化 Material.bundle 可能变化 Prefab.bundle 更不一定变化1.1 区分 Shader 的三层身份分析“Shader 的物理位置是否变化”时需要区分三个概念。逻辑 Bundle 身份这是 SBP 构建阶段使用的内部逻辑名称例如group_shader.bundleShader 对象在序列化文件中的身份Material 的m_Shader最终是一个跨序列化文件的对象引用。概念上包含外部序列化文件 Shader 对象的 pathID / serializationIndexAddressables 最终输出文件名根据 Bundle 内容 Hash 和 Group 的 Bundle Naming Mode最终文件可能是旧a84fc0e7.bundle 新93bd128a.bundleShader 源码或编译结果变化时最终文件内容和 Hash 文件名通常会变化但逻辑 Bundle 名、Shader GUID、pathID 或 serializationIndex 不一定变化。1.2 Material Bundle 保存的不是最终 CDN Hash 文件名Material Bundle 中保存的是序列化层面的外部对象引用概念上类似Material.m_Shader - 外部逻辑文件group_shader.bundle - Shader 对象pathID 123456最终应该下载哪个 Hash 文件由 Addressables Catalog 和 Bundle 依赖数据解析。例如旧构建 Cataloggroup_shader.bundle - a84fc0e7.bundle Materialgroup_shader.bundle / pathID 123456 新构建 Cataloggroup_shader.bundle - 93bd128a.bundle Materialgroup_shader.bundle / pathID 123456在这个例子中Shader 编译数据变化导致 Shader Bundle 和 Catalog 变化但 Material 中保存的外部对象引用没有变化因此 Material Bundle 可以保持不变。Prefab 又只引用 Material所以 Prefab Bundle 也可以保持不变。这意味着依赖 Bundle 的最终 Hash 文件名变化不等于所有依赖者的序列化引用都会变化。1.3 Shader 内容变化与 Shader 映射变化不是一回事如果修改 Shader 后仍满足Shader GUID 不变 Shader 所属逻辑 Bundle 不变 Shader pathID 不变 Shader serializationIndex 不变 Material Keyword 和属性 不变那么可能只有 Shader Bundle 变化Shader.bundle 变化 Material.bundle 不变 Prefab.bundle 不变 Catalog 更新到新版 Shader Bundle如果 Shader 修改同时改变了对象的跨 Bundle 映射例如旧group_shader.bundle / serializationIndex 5 新group_shader.bundle / serializationIndex 8或者 Shader 被移动到另一个 Bundle旧shader_common.bundle 新shader_character.bundleMaterial 的BuildReferenceMap就会变化。SBP 的 Bundle 写入 Hash包含hashes.Add(Command.GetHash128());hashes.Add(UsageSet.GetHash128());hashes.Add(ReferenceMap.GetHash128());hashes.Add(Info.GetHash128());hashes.Add(HashingMethods.Calculate(hashObjects).ToHash128());hashes.Add(DependencyHash);hashes.Add(BuildInterfacesWrapper.ShaderCallbackVersionHash);此时 Material Bundle 很可能重新写入并产生新的 ContentHash。Prefab 是否继续变化再看 Prefab 到 Material 的映射及 UsageSet 是否受到影响。1.4 哪些 Shader 改动容易向上传播以下变化更容易连带 Material、Prefab 或 Scene BundleShader 被移动到其他 Addressables GroupShader 所属逻辑 Bundle 名称发生变化Shader Bundle 中增删对象导致序列化布局变化Shader 对象的 pathID 或 serializationIndex 变化Material 的 Shader Keyword、Pass、RenderQueue 或属性变化Shader Variant 集合或 stripping 结果变化BuildUsageTagSet或BuildReferenceMap变化Render Pipeline、Graphics Settings 或 Graphics API 变化ShaderCallbackVersionHash变化Built-in Shader Bundle 的生成方式变化Shader 没有独立打包而是作为隐式依赖进入多个 Bundle开启Unique Bundle IDs后内部 Bundle 身份随相关资源 Hash 变化。如果 Shader 不是独立 Bundle而是被隐式打入多个 Material、Prefab 或 Scene Bundle那么 Shader 内容变化会直接改变这些 Bundle 的二进制内容影响范围通常比单纯的外部依赖映射变化更大。1.5 当前项目Unique Bundle IDs的影响当前工程配置为m_UniqueBundleIds:0即Unique Bundle IDs关闭。对应配置位于Assets/AddressableAssetsData/AddressableAssetSettings.assetAddressables 的AddHashToBundleNameTask会在该选项关闭时直接返回if(!aa.Settings.UniqueBundleIds)returnReturnCode.Success;因此当前项目通常不会在依赖分析阶段把资源 Hash 追加到内部逻辑 Bundle 名。最终输出文件仍可按照 Group 的命名模式使用BundleDetails.Hash但该 Hash 文件名由 Catalog 负责解析不需要直接写进 Material 或 Prefab 的对象引用。这也是为什么在当前项目中不能使用下面这个过度简化的公式MaterialBundleHash Hash(MaterialBundle ShaderBundleHash) PrefabBundleHash Hash(PrefabBundle MaterialBundleHash)实际机制更接近外部对象映射 / UsageSet / 序列化数据发生变化 - Bundle 写入任务 Hash 变化 - 重新生成序列化文件 - 序列化文件 ContentHash 变化 - BundleDetails.Hash 变化 - 最终输出文件名与 Catalog 更新1.6 为什么图集比普通 Shader 修改更容易影响引用者普通 Shader 源码变化可能只改变同一个 Shader 对象内部的编译数据其 GUID、逻辑 Bundle、pathID 和外部对象映射仍保持稳定。SpriteAtlas 重新 Pack 则更容易改变Sprite 对象集合Sprite 子资源和 Atlas 数据对象图集页数量Sprite 的序列化排列pathID 或 serializationIndexPrefab 到 Sprite 的BuildReferenceMap。因此常见差异是Shader 内容变化 - 外部对象映射可能保持不变 SpriteAtlas 重新 Pack - Sprite 外部对象映射更容易变化这不是绝对规则。Shader Bundle 如果发生对象增删、变体布局变化或构建参数漂移同样可能产生很大的 fan-out。1.7 Shader 变化的推荐排查方式先检查 Comparison 报告中的分类Modify / Shader Modify / Material Modify Bundle Bundle Setting/Dependencies Changed然后按以下顺序判断确认 Shader、Material、ShaderVariantCollection 和渲染配置的 Git diff确认 Shader Bundle 是否为真实内容变化用 layout 检查 Material、Prefab、Scene Bundle 是否直接或间接依赖它判断依赖者是content_changed还是只有 BundleHash 变化对可疑 Material Bundle 比较BuildReferenceMap对应的外部对象映射必要时解包比较 Shader、Material 对象及 CAB/资源流若 Git 无 Shader 相关变化检查 variant stripping、Graphics API、Quality、Render Pipeline、构建机和缓存差异。如果出现Shader 显式资源 AssetHash 不变 Shader 资源集合不变 Shader 依赖集合不变 Git 中 Shader 和渲染配置不变 但 Shader BundleHash 或大小变化应优先怀疑 Shader 编译、Variant 裁剪或构建参数漂移而不是直接认定为正常依赖级联。1.8 Shader 场景总结三者分别打 Bundle 时不能仅凭逻辑引用链断言全部变化Prefab.bundle - Material.bundle - Shader.bundle正确结论是Shader 代码变化会改变 Shader Bundle 的内容和最终文件 Hash只要逻辑 Bundle 身份及 Shader 对象的跨 Bundle 映射不变Material Bundle 就不必变化。只有引用映射、UsageSet、序列化对象或相关构建数据也发生变化时Material Bundle 才会被连带修改Prefab Bundle 是否继续变化需要再看它到 Material 的映射。