扩展问答:使用 Addressables 优化内存和构建大小

二月份,作为 UnityAccelerate Solutions高级软件开发顾问,我主持了一场关于 Addressables Asset System 的 技术网络研讨会 。线上演示期间,我展示了多种分析工具,用于优化项目的运行时内存和安装包体积。在最后的问答环节,我们收到的问题数量实在太多,没时间一一解答。
以下问答包含了更多我们希望你了解的问题。
问:如果我没有内存问题,轻度游戏(如休闲游戏、街机游戏或益智游戏)是否需要 Addressables 系统?
一个:也许不是,但请记住,Addressables 系统不仅可以提高内存性能。还可以按需加载内容,提高加载速度。用 Addressables 打包内容不用花多少时间就能迭代。比如小的脚本修改不需要你重新构建所有的资产包。
问:场景切换时加载的资源会被释放吗?
一个:有可能。Addressables加载的资产可以随时释放,它们没有引用项,可在场景转换期间从内存中卸载。当从场景非附加转换时,调用 Resources.UnloadUnusedAssets()。这会消耗大量CPU资源,并卸载部分AssetBundles。
问:对象池和 Addressables 能很好地协同工作吗?
一个:是的。你能从Addressables单次加载对象,再复制多个实例形成对象池。当您使用完池后,销毁所有对象并释放用于加载资产的 AsyncOperationHandle。
问:组和包是否会被一次性加载到内存中?
一个:可寻址组是编辑器独有的概念。在运行时,你只会和资产包打交道。资产包只会在需要时加载想要的内容。
例子:您有一个包含 10 个角色的捆绑包。你要求Addressables加载其中三个,那么只有捆绑包的元数据和这三个角色将被加载。
问:如果我想发布资产,我需要保留 AsyncOperationHandle 还是 AssetReference?
一个:我们建议保留并使用该名称,因为当您使用完毕后,您有责任发布内容。
例如,我们团队经常用句柄来避免直接在AssetReference上调用Instantiate/Release。
问:很多小捆绑包有什么缺点?
一个:该文档 列出了过多捆绑的几个缺点。
问:当需要 Bundle 中的某个资产时,同一 Bundle 中的其他资产会有哪些开销?如果我们必须下载这个远程捆绑包,没用到的资产真的不会消耗内存吗?
一个:正确,远程包将被完全下载,然后您才能使用它。
未加载的同一捆资产在运行时的消耗几乎能忽略不计。每次从捆绑包加载资产,你就需要加载包的元数据。这份数据里包含了一个所有资产的目录。资产越多,捆绑包的元数据也就越大。资产越多,捆绑包的元数据也就越大。
这部分的开销可以借助Unity Memory Profiler记录。“All Of Memory”选项卡会列出所有“SerializedFile”对象的清单,每个对象对应一个捆绑包。这些对象就是包的元数据。
在 我们的文档中了解有关此元数据的更多信息。
问:在开放世界环境中工作时,我可以使用哪些捆绑策略来卸载单个资产,而无需半卸载捆绑包并依靠 Resources.UnloadUnusedAssets() 来清理它,而无需将每个资产放在自己的捆绑包中的开销?
一个:要记住的关键是,如果您希望同时卸载内容,则应将内容捆绑在一起。游戏世界的“静态”内容,例如某个生物圈里的树木和岩石可以绑在一起;任何“动态”内容,例如可拾取物品,则应另做捆绑。
这篇博客文章 和链接的 GitHub repo 介绍了开放世界游戏的拆分捆绑包。还介绍了一种去除重复包、减少每包内存开销的方法。第4和第5阶段与开放世界最为相关。
问:我什么时候应该保持“AssetBundle CRC”处于启用状态?
一个:建议的做法是启用此功能,排除远程组的缓存 AssetBundles,并禁用本地组。这个检查仅仅是为了确保数据在下载时没有损坏。本地AssetBundles并没有必要进行检查。
问:在加载和卸载资产时,由于 CPU 性能问题,什么时候不值得使用 Addressables?
一个:由于不需要预先加载所有内容,Addressables 系统对 CPU 加载性能有积极影响。
如果不使用Addressables,场景加载时就必须加载所有内容和引用。如果转移到Addressables,我们就可以选择什么时候加载什么东西。
假如场景里有一个引用了1000中物品栏物品的Inventory Manager。如果不使用Addressables,你就不得不加载所有物品的网格、纹理、音频等等。如果只在稍后加载,那场景的加载会更快。
问:可寻址资产的所有依赖项是否也需要是可寻址的,还是只有在共享时才有必要?
一个:依赖项不需要标记为可寻址。在打包时,必要的依赖项会被自动捆绑到Addressables里。
例如,如果将player预制件标为可寻址,则玩家的模型网格、纹理或音频就不必再手动标为可寻址。打包时,所有Addressables没包含的依赖项都会自动打包到player预制件的包里。
问:如果我忘记发布资产并更改场景,该资产会怎样?
一个:改变场景本质上并不会导致与手柄的交互不佳。但如果你加载了资产并忘记释放句柄,则该资产会在内存中持续存在。
Addressables有一个内部引用计数系统。句柄是我们与该系统交互的方式。加载资源会增加引用计数,释放会减少计数。
创作者需要时刻更新这些个计数。只要数字大于1,资产就会一直存在于内存中。
问:与网络研讨会示例相关,假设我正在制作一款开放世界游戏。Boss会出现在世界的某处。当玩家前往Boss位置时,我该怎么用Addressables呢?我是否应该通过触发器、在与敌人保持一定距离的地方或其他方式发送异步加载剑的命令?
一个:选择何时加载和卸载内容可能是一个微妙的界限。Boss需要在玩家看到它时已经准备就绪,但又不能在玩家还能转身走开时过早地加载。
幸好,你可以不断尝试加载和卸载的时机,没必要第一次就求出最优解。
作为入门级方法,我们建议在玩家靠近某个“区域”时加载其中的内容(比如玩家在靠近地牢入口时加载地牢内的所有内容)。如果这种方法产生了过多的内存压力,你可以更细致地划分加/卸载时机。
如果剑不能及时加载,可以考虑提前触发时机,利用Unity Profiler的CPU模块观察加载的内容、改善资产的加载时间,或用Addressables同步完成加载。
该文档 包含更多详细信息和同步可寻址的代码片段。
问:如果我在场景开始时加载可寻址内容,我是否需要为其设置加载屏幕?
一个:从 Addressables 加载通常以异步方式完成,例如使用 Addressables.LoadAssetAsync()。
部分内容不一定需要在加载界面加载。你可以收集这些AsyncOperationHandles用于随后处理,在离开界面前等待必要项加载完成。
问:可寻址元数据在运行时(在加载任何数据之前)的内存占用是多少?
一个:在 Addressables 初始化期间,将加载目录文件,以便 Addressables 知道如何将标签和地址映射到磁盘或远程位置的资产。目录越大,运行时消耗的内存也越多。
我们可以清楚冗余数据来降低目录大小,比如去除若干不需要的标签或GUID;或减少现有数据的体积,比如把资产组的Internal Asset Naming Mode(内部资产明明模式)设为 GUID,而非更长的文件名或完整路径。目录在运行时所占用的内存可以在Unity Memory Profiler里查看。
问:Unity 编辑器在构建 Addressables 时做了什么?
一个:构建报告日志输出在 /Library 文件夹中。你还能在日志里添加更多详细信息,要向日志添加其他详细信息,请按照此路径选择“使用详细的构建日志”:启用编辑 > 首选项 > 可编写脚本的构建管道 > 使用详细构建日志。
查看有关 如何查看日志的视觉效果和文档。
问:Resources.Load() 是否也存在重复问题?
一个:是的。Addressables内容和Resources内容可以被视为不同的“世界”。/Resources下的一个纹理肯定会有一个Resources文件内的副本。如果Addressables的一捆资产依赖于该纹理,则每一捆都会暗地里复制这份纹理。这就会在磁盘上或内存里复制多份副本文件。
要想避免这种复制,我们可将纹理移出/Resources并添加到Addressables组。
问:当您不使用 Addressables 时,是否会遇到类似的磁盘大小问题,通过删除重复的捆绑包可以解决这些问题?
一个:是的。在讨论会和幻灯片里,我们展示了删去两个重复的水上赛艇场景可以显著减小游戏文件的大小。
问:如何防止着色器变体重复?
一个:着色器可以像任何其他资产一样在相同的过程中进行重复数据删除——在一个组中明确声明它们。
一旦在Addressables的资产组里明确声明了资产的引用,那该资产就不会在数个资产捆里被复制。
对于着色器,常见的做法是用“Shared shaders”组包括进应用生命周期内需要加载到内存中的,且需要由许多资产共享的着色器。
问:两个共享相同预制件的 Unity 场景是否会重复构建尺寸?
一个:这取决于场景所依赖的预制件是否已明确包含在 Addressables 中,以及场景是否位于相同或不同的 bundles 中。
请参阅网络研讨会幻灯片和第 4 阶段的 博客文章 中有关重复如何发生的直观解释。
关键在于,打包到资产捆的所有内容都必须访问所有依赖项。如果把场景打包到一捆资产里,则所有依赖项必须:
- 明确包含在Addressables中
- 或隐含在资产捆的引用中
问:是否可以比较给定组中的重复项,以防止将所有游戏资产打包到孤立的组中?
一个:是的。你能先运行系统自带的去重规则,然后到Addressables Groups窗口亲自把资产划为更恰当的组别。
或者,你可以编写自己的Addressables AnalyzeRules,新规则将出现在Analyze窗口中。Addressables软件包自带的C#规则可作为你的参考。
比如,您可以查找所有以“Character-”开头的重复项。所有潜在的重复文件都可以被放到一个“Shared-Character”组中。
问:您要介绍远程构建和本地路径吗?
一个:我们没有涉及网络研讨会中的远程和本地路径,它们被称为“可寻址配置文件”。但是,我们确实在 本文档中描述了什么是可寻址配置文件以及如何使用它们。
问:Addressables 如何与云内容交付(CCD)配合使用?
一个:本文献讨论了 CCD 集成。
问:您能否给出一些关于实现低分辨率和高分辨率可寻址变化的最佳实践的指点?
一个:您可以在 GitHub 上的 Addressables Sample中找到一个示例。
问:如果捆绑内容被加密了怎么办?问:如果资产捆的内容已加密,UnityDataTool会解密内容吗?
一个:不可以。UnityDataTool 分析内容之前需要解密数据。
问:是否支持从一个 Unity 项目构建捆绑包并在运行时从从不同项目构建的应用程序加载捆绑包?
一个:是的。这是通过同时 使用多个目录 来解决的。
问:使用 InstantiateAsync 有什么缺点吗,或者在某些情况下最好使用 LoadAsync + 手动 Instantiate?
一个:建议使用 Addressables.LoadAssetAsync() 并调用 Object.Instantiate()。Addressables.InstantiateAsync() 的性能成本较大。
问:我有很多 ScriptableObjects,其中至少有 1-2 个精灵被引用为变量。如果我想把这些精灵改为Addressables,是需要一张张地手动更改,还是有什么特殊技巧?
一个:编辑器脚本可能是转换这些引用的方法。
你可以为ScriptableObject添加一个AssetReference字段(暂时保留Sprite字段),然后编写一份编辑器脚本来遍历这些ScriptableObject,查找Addressables里的Sprite资产来找到相应的AddressableAssetEntry,保存好地址或创建AssetReference并保存到ScriptableObject。
最后,您可以删除对Sprite的直接引用,把代码里的引用切换成AssetReference。
问:我可以将可寻址内存用于 WebGL 游戏吗?如果可以,有什么需要注意的特殊事项?
一个:是的,是的。需要注意两点:首先,WebGL 不支持线程,所以不要使用任务。其次,WebGL缓存的方式不同——我们之前看到过缓存远程AssetBundles的问题。
问:如果我使用 Shader.Find(“ShaderName”),这是来自构建还是可寻址?
一个:这些来自 Unity Player 的构建,而不是 Addressables。Shader.Find() 不返回 AssetBundles 的结果。
问:当我有许多名称相似的组时,我该如何组织可寻址组窗口?
一个:为了组织可寻址组 UI,您可以 使用 Dashes 启用组层次结构。把名称相近的组归纳到一起。比如,“Character-person”和“Character-person2”都会出现在“Character”分组的UI。
这不会影响资产捆的创建,这不会影响资产捆的创建,仅仅会改变UI的组织。
在 Addressables 论坛上与我们分享您的反馈。请务必关注其他 Unity 开发人员的新技术博客,这是正在进行的来自战壕系列的技术。
