来自优化战壕的故事使用 Addressables 保存内存

高效地将资产流输入和输出内存是任何优质游戏的关键要素。作为我们专业服务团队的一名顾问,我一直在努力提高许多客户项目的绩效。因此,我想与大家分享一些如何利用Unity 可寻址资产系统来加强内容加载策略的技巧。
内存是一种稀缺资源,您必须谨慎管理,尤其是在将项目移植到新平台时。使用 Addressables 可以通过引入弱引用来防止加载不必要的资产,从而改善运行时内存。弱引用意味着你可以控制被引用的资产何时加载到内存,何时退出内存;Addressable 系统也会找到所有必要的依赖关系并加载它们。本博客将介绍在设置项目以使用 Unity 可寻址资产系统时可能遇到的一些情况和问题,并解释如何识别它们并及时解决。

在本系列建议中,我们将使用一个简单的示例,其设置方法如下:
- 我们在场景中有一个 InventoryManager 脚本,其中引用了三个库存资产:剑、老板剑、盾牌预制件。
- 在游戏过程中,并非任何时候都需要这些资产。
您可以在我的 GitHub 上下载此示例的项目文件。我们使用预览包Memory Profiler来查看运行时的内存。在 Unity 2020 LTS 中,您必须先在 "项目设置 "中启用预览包,然后再从 "软件包管理器 "中安装此软件包。
如果您使用的是 Unity 2021.1,请从 "软件包管理器 "窗口的附加菜单 (+) 中选择 "按名称添加软件包"选项。使用名称 "com.unity.memoryprofiler"。
让我们从最基本的实现开始,然后逐步找到设置 Addressables 内容的最佳方法。我们只需在场景中存在的 MonoBehaviour 中将硬引用(在检查器中直接赋值,通过GUID 跟踪)应用到我们的预制板。

加载场景时,场景中的所有对象也会连同它们的依赖关系一起加载到内存中。这意味着我们的 InventorySystem(库存系统)中列出的每个预制板都将与这些预制板的所有依赖项(纹理、网格、音频等)一起驻留在内存中。
在创建构建并使用 Memory Profiler 拍摄快照时,我们可以看到资产的纹理已经存储在内存中,尽管它们都没有实例化。

问题 内存中有我们目前不需要的资产。在有大量库存项目的项目中,这会造成相当大的运行内存压力。
为了避免加载不需要的资产,我们将把库存系统改为使用 Addressables。使用 "资产引用 "而非 "直接引用 "可以防止这些对象与我们的场景一起加载。让我们将库存预制板移到 Addressables 组中,并更改 InventorySystem 以使用 Addressables API 来实例化和释放对象。

构建播放器并拍摄快照。请注意,内存中还没有任何资产,这很好,因为它们尚未实例化。

将所有项目实例化,查看它们是否与内存中的资产一起正确显示。

问题 如果我们将所有物品实例化,然后让老板剑退役,那么我们仍然会在内存中看到老板剑的纹理 "BossSword_E",尽管它并没有被使用。原因是,虽然可以部分加载资产包,但不可能自动部分卸载它们。对于包含许多资产的捆绑包(例如包含我们所有库存预制件的单个 AssetBundle)来说,这种行为可能会带来特别大的问题。在整个 AssetBundle 不再需要之前,或者在我们调用代价高昂的 CPU 操作 Resources.UnloadUnusedAssets() 之前,捆绑中的所有资产都不会卸载。


要解决这个问题,我们必须改变组织 AssetBundles 的方式。目前,我们有一个将所有资产打包到一个 AssetBundle 中的单个 Addressable Group,但我们可以为每个预制件创建一个 AssetBundle。这些粒度更细的 AssetBundles 可减轻大型捆绑包在内存中保留我们不再需要的资产的问题。
做出这种改变很容易。选择一个 Addressable 组,然后选择内容打包和加载>高级选项>捆包模式,再转到检查器,将捆包模式从一起打包改为分开打包。
通过使用"单独打包"建立此 Addressable 组,您可以为 Addressable 组中的每个资产创建一个 AssetBundle。

资产和捆绑包将如下所示:

现在,回到我们最初的测试:在生成我们的三件物品后,再解散老板剑,就不会再在内存中留下不必要的资产了。现在已卸载了老板剑的纹理,因为不再需要整个捆绑包。
问题 如果我们生成所有三个物品并进行内存捕捉,内存中就会出现重复的资产。更具体地说,这将导致纹理 "Sword_N "和 "Sword_D "的多个副本。如果我们只改变捆绑包的数量,怎么会出现这种情况?

要回答这个问题,让我们考虑一下我们创建的三个捆绑包中的所有内容。虽然我们只将三个预制件资产放入捆绑包中,但作为预制件的依赖项,这些捆绑包中还隐含了其他资产。例如,剑的预制资产中还需要包含网格、材质和纹理资产。如果这些依赖项没有明确包含在 Addressables 的其他地方,那么它们会被自动添加到每个需要它们的捆绑包中。

Addressables 包括一个分析窗口,可帮助诊断捆绑布局。打开 "窗口 " > " 资产管理 " > " Addressables " > "分析",运行规则 "捆绑布局预览"。在这里,我们看到 sword 捆绑包中明确包含了 sword.prefab,但还有许多隐式依赖项也被拉入了这个捆绑包。

在同一窗口中,运行 "检查重复的捆绑依赖关系"。该规则根据我们当前的 Addressables 布局,突出显示多个资产捆绑包中包含的资产。

我们可以通过两种方式防止这些资产的重复使用:
1.将 "剑"、"BossSword "和 "盾 "预置块放在同一个捆绑包中,使它们共享依赖关系,或
2.在 Addressables 中的某处明确包含重复的资产
我们希望避免将多个库存预制件放在同一个捆绑包中,以防止不需要的资产在内存中持续存在。因此,我们将把重复的资产添加到它们自己的捆绑包(捆绑包 4 和捆绑包 5)中。

除了分析我们的捆绑包,分析规则还能通过修复选定规则自动修复违规资产。按下此按钮可创建一个名为 "重复资产隔离 "的新 Addressable 组,其中包含四个重复资产。将该组的 "捆绑模式 "设置为 "单独打包",以防止任何其他不再需要的资产在内存中持续存在。

使用这种 AssetBundle 策略可能会导致大规模问题。对于在给定时间加载的每个 AssetBundle,AssetBundle 元数据都会产生内存开销。如果我们将目前的策略扩展到数百或数千个清单项目,那么这些元数据很可能会消耗大量内存。在Addressables 文档中阅读有关 AssetBundle 元数据的更多信息。
在 Unity Profiler 中查看当前 AssetBundle 元数据内存成本。进入内存模块,拍摄内存快照。在 "其他" >" 序列化文件"类别中查找。

每个已加载的 AssetBundle 在内存中都有一个 SerializedFile 条目。该内存是 AssetBundle 元数据,而不是捆绑包中的实际资产。这些元数据包括
- 两个文件读取缓冲区
- 类型树,列出捆绑包中包含的每种独特类型
- 指向资产的目录
在这三个项目中,文件读取缓冲区占用的空间最大。这些缓冲区在 PS4、Switch 和 Windows RT 上各为 64 KB,在所有其他平台上为 7 KB。在上述示例中,1,819 个捆绑包 * 64 KB * 2 个缓冲区 = 227 MB 仅用于缓冲区。
由于缓冲区的数量与资产捆绑包的数量成线性关系,因此减少内存的简单解决方案就是在运行时加载更少的捆绑包。不过,我们以前曾避免加载大型捆绑包,以防止不需要的资产在内存中持续存在。那么,我们如何在保持粒度的同时减少捆绑包的数量呢?
坚实的第一步是根据资产在应用程序中的用途对其进行分组。如果你能根据你的应用做出明智的假设,那么你就可以将那些你知道总是会一起加载和卸载的资产分组,比如那些根据它们所处的游戏关卡分组的资产。
另一方面,您可能无法对何时需要/不需要您的资产做出安全的假设。例如,如果您创建的是一款开放世界游戏,那么您就不能简单地将森林生物群落中的所有物品都归入一个资产包,因为玩家可能会从森林中抓取一件物品,然后在不同的生物群落间携带。整个森林捆绑包仍然保留在内存中,因为玩家仍然需要森林中的一个资产。
幸运的是,有一种方法可以减少捆绑包的数量,同时保持所需的粒度水平。让我们更聪明地处理重复捆绑。
我们运行的内置重复数据删除分析规则可以检测到所有处于多个捆绑包中的资产,并将它们有效地移动到一个可寻址组中。将该组设置为 "单独打包"后,每个捆绑包就会有一个资产。不过,有些重复的资产我们可以安全地打包在一起,而不会带来内存问题。请看下图:

我们知道纹理 "Sword_N "和 "Sword_D "是同一个捆绑包(捆绑包 1 和捆绑包 2)的依赖包。由于这些纹理的母体相同,我们可以放心地将它们包装在一起,而不会造成记忆问题。两种剑的纹理必须始终装载或卸载。我们从不担心其中一个纹理会在内存中持续存在,因为我们从来没有专门使用一个纹理而不使用另一个纹理的情况。
我们可以在自己的Addressables 分析规则中实现这种改进的重复数据删除逻辑。我们将使用现有的 CheckForDupeDependencies.cs 规则。您可以在库存系统示例中查看完整的实施代码。在这个简单的项目中,我们只是将捆绑包的总数从 7 个减少到 5 个。但试想一下,如果您的应用程序在 Addressables 中拥有数百、数千甚至更多的重复资产,那该怎么办?在与 Unknown Worlds Entertainment 合作为其游戏Subnautica 提供专业服务时,在使用内置重复数据删除分析规则后,该项目最初共有 8718 个捆绑包。在应用自定义规则根据资产包的父资产对重复资产进行分组后,我们将其减少到 5 199 个资产包。您可以在本案例故事中了解我们与该团队合作的更多情况。
这意味着捆绑包的数量减少了 40%,但捆绑包中的内容仍然相同,粒度也保持不变。捆绑包数量减少了 40%,运行时 SerializedFile 的大小也同样减少了 40%(从 311 MB 减少到 184 MB)。
使用 Addressables 可以大大减少内存消耗。您可以根据自己的使用情况来组织资产包,从而进一步减少内存。毕竟,内置分析规则是保守的,以适应所有应用。编写自己的分析规则可以实现捆绑布局的自动化,并针对应用进行优化。要抓住内存问题,请继续经常进行 Profile Analyzer 分析,查看捆绑包中显式和隐式包含了哪些资产。查看Addressable Asset System 文档,了解更多最佳实践、入门指南和扩展 API 文档。
如果您想获得更多实践帮助,了解如何利用 Addressables Asset System 改进内容管理,请联系销售人员了解专业培训课程。