

通过对游戏在各种平台和设备上的性能进行剖析和优化,您可以扩大玩家基础并增加成功的机会。
此处的信息摘自 终极 Unity 游戏剖析指南(Unity 6 版),这是一本由外部和内部 Unity 游戏开发、剖析和优化专家共同创建的电子书。
内存分析与运行时性能大体无关,但对于测试硬件平台的内存限制或游戏崩溃时非常有用。如果您想通过实际增加内存使用量来提高CPU/GPU性能,这也可能是相关的。
在Unity中,有两种分析应用程序内存使用情况的方法:
- 内存分析器模块:这是一个内置的分析器模块,可以在常规分析器中提供有关应用程序内存使用情况的基本信息。
- 内存分析器:这是一个作为Unity包提供的专用工具,您可以将其添加到项目中。它在Unity编辑器中添加了一个额外的内存分析器窗口,您可以使用它更详细地分析应用程序的内存使用情况。可以存储和比较快照以便查出内存泄漏,或者查看内存布局以查出内存碎片问题。我们将在本指南的后面部分详细介绍这一点,并在此处关注您需要考虑的一般事项。
这两个工具都使您能够监控内存使用情况,定位应用程序中内存使用高于预期的区域,并查找和改善内存碎片。

理解和预算目标设备的内存限制对于多平台开发至关重要。在设计场景和关卡时,您需要遵循为每个目标设备设定的内存预算。通过设定限制和指导方针,您可以确保应用程序在每个平台的硬件规格范围内良好运行。
您可以在开发者文档中找到设备内存规格。
围绕网格和着色器复杂性以及纹理压缩设置内容预算也可能是有用的。这些都影响分配的内存量。在项目开发周期中可以参考这些预算数字。
确定物理RAM限制
由于每个平台都有内存限制,您的应用程序需要为每个目标设备设定内存预算。使用内存分析器查看捕获的内存使用快照。硬件资源快照(见下图)显示了物理随机存取内存(RAM)和视频随机存取内存(VRAM)的大小。这个数字并没有考虑到并非所有的空间都可以使用。然而,它提供了一个有用的粗略数字来开始工作。
交叉参考目标平台的硬件规格是个好主意,因为这里显示的数字可能并不总是完整的。开发者工具包硬件有时会有更多内存,或者您可能正在使用具有统一内存架构的硬件。

识别您支持的每个平台上RAM规格最低的硬件,并以此指导您的内存预算决策。请记住,并非所有的物理内存都可以使用。例如,一个控制台可能运行一个虚拟机来支持旧游戏,这可能会占用部分总内存。根据您的具体情况,考虑使用一个百分比(例如,总数的80%)作为团队的使用标准。对于移动平台,您还可以考虑将规格分为多个层次,以支持更高端设备的更好质量和功能。
考虑为较大团队设定每个团队的预算
一旦您定义了内存预算,考虑为每个团队设定内存预算。例如,您的环境艺术家在加载的每个关卡或场景中获得一定量的内存,音频团队获得音乐和音效的内存分配,等等。虽然这看起来可能很严格,但可以将其视为指导创意决策与资源成本之间的指南。
随着项目的进展,灵活处理预算是很重要的。如果一个团队的预算低于预期,将剩余分配给另一个团队,如果这能改善他们正在开发的游戏领域。
一旦您决定并设定了目标平台的内存预算,下一步是使用分析工具帮助您监控和跟踪游戏中的内存使用情况,使您能够做出明智的决策并根据需要采取行动。
要高层次地确定内存使用何时开始接近平台预算,请使用以下“草稿”计算:
系统使用的内存(或如果系统使用显示为0,则为总保留内存)+ 未跟踪内存的粗略缓冲 / 平台总内存
当这个数字开始接近您平台的内存预算的100%时,请使用内存分析器包来找出原因。
在Unity 6中,内存分析器模块的许多功能被内存分析器包所取代,但您仍然可以使用该模块来补充您的内存分析工作。例如:
- 识别GC分配:尽管这些在模块中显示,但使用项目审计器或深度分析更容易追踪。
- 快速查看堆的已用/保留大小
- 着色器内存分析
- 在内存分析器模块中使用详细视图深入了解使用最多内存的最高内存树。
这里有更多资源可以帮助您探索Unity分析器的其他用例和功能:
您通常希望使用具有大量可用内存的强大开发者系统进行分析(存储大型内存快照或快速加载和保存这些快照的空间很重要)。
内存分析与CPU和GPU分析是不同的,因为它本身可能会产生额外的内存开销。您可能需要在高端设备(具有更多内存)上分析内存,但特别要注意低端目标规格的内存预算限制。
质量级别、图形层级和AssetBundle变体等设置在更强大的设备上可能具有不同的内存使用情况。考虑到这一点,这里有一些细节需要记住,以便从内存分析中获得最大收益:
- 质量和图形设置会影响用于阴影图的渲染纹理的大小。
- 分辨率缩放会影响屏幕缓冲区、渲染纹理和后处理效果的大小。
- 纹理设置会影响所有纹理的大小。
- 最大 LOD 可能会影响模型等。
如果您有像 HD(高清)和 SD(标准清晰度)版本的 AssetBundle 变体,并根据目标设备的规格选择使用哪个版本,您可能会根据您正在分析的设备获得不同的资产大小。
- 目标设备的屏幕分辨率将影响用于后处理效果的渲染纹理的大小。
- 设备支持的图形 API 可能会影响着色器的大小,具体取决于它支持(或不支持)的变体。
- 使用不同质量和图形设置以及 AssetBundle 变体的分层系统是能够针对更广泛设备的好方法。
例如,您可以在 4GB 移动设备上加载 AssetBundle 的 HD 版本,而在 2GB 设备上加载 SD 版本。然而,请考虑上述内存使用的变化,并确保测试这两种类型的设备,以及具有不同屏幕分辨率或支持的图形 API 的设备。
注意:Unity 编辑器通常会显示更大的内存占用,因为从编辑器和分析器加载了额外的对象。此外,纹理内存占用更高,因为它们在编辑器中都被强制启用读/写。

内存性能分析器包可以帮助您理解和优化项目的内存使用。它允许您在特定时刻捕获应用程序内存的“快照”,无论是在 Unity 编辑器中还是在目标设备上运行的 Player 构建中。
这些快照提供了内存使用的全面细分,显示了整个引擎中的分配情况。这帮助您识别过度或不必要内存使用的来源,追踪内存泄漏,并检查堆碎片等问题。
安装内存性能分析器包后,通过 窗口 > 分析 > 内存性能分析器 打开它。
内存性能分析器的顶部菜单栏允许您更改播放器选择目标并捕获或导入快照。左上角的目标选择下拉菜单允许您通过将内存性能分析器连接到远程设备,直接在目标硬件上分析内存。请注意,在Unity编辑器中进行分析会由于编辑器和其他工具添加的开销而给出不准确的数字。

在内存分析器窗口的左侧是快照组件。使用此功能来管理和打开或关闭保存的内存快照。快照组件提供两种视图,单个快照和比较快照。
与分析器类似,内存分析器允许您并排加载和比较两个内存快照。使用此比较来跟踪内存随时间的增长,分析场景之间的使用情况,或识别潜在的内存泄漏。
内存分析器在主窗口中有多个选项卡,允许您深入挖掘内存快照,关键的选项卡包括摘要、Unity对象和所有内存。让我们详细看看这些选项。

摘要选项卡为您提供项目内存使用情况的高层快照,快照是在内存捕获时拍摄的。当您想要快速且信息丰富的概述而不深入详细分析时,它非常完美。
此视图突出显示关键指标,可以帮助您快速发现潜在的内存问题或意外的使用模式。在比较快照或调试内存使用情况时,它尤其有用。让我们看看它的一些关键部分。
提示:在右侧面板(见下图)中,您会找到有关快照的有用上下文信息。这些可以提醒您可能的问题或指导您解释结果。
设备上的内存使用情况:这显示了应用程序在物理内存中的占用。它包括在捕获时内存中所有的 Unity 和非 Unity 分配。
分配的内存分布:此视图可视化分配的内存在不同内存类别中的分布。
注意 未跟踪* 内存条。它对应于 Unity 通过其内存管理系统不跟踪的内存。这些分配可能来自本地插件和驱动程序。使用特定平台的分析器来分析目标设备的未跟踪内存使用情况。
托管堆利用率:在此视图中,您将获得 Unity 的脚本虚拟机管理的内存的细分,包括用于托管对象的托管堆内存、可能之前被对象使用或在上次堆扩展期间保留的空堆空间,以及虚拟机本身使用的内存。
顶级 Unity 对象类别:这显示了在快照中使用最多内存的 Unity 对象类型(例如,Texture2D、网格、GameObject)。

Unity 对象选项卡 显示任何分配内存的 Unity 对象、对象使用的本地和托管内存量,以及总和。使用此信息来识别可以消除重复内存条目的区域或查找使用最多内存的对象。通过搜索栏,您可以找到表中包含您输入的文本的条目。
默认情况下,表按分配大小以降序列出所有相关对象。您可以单击列标题名称按该列对表进行排序或更改列的排序方式(升序或降序)。
在优化内存使用和旨在为内存预算有限的硬件平台更有效地打包内存时,利用这一点。

首先分析内存分析器快照,以识别高内存使用区域。一旦捕获或加载内存分析器快照,使用Unity对象选项卡检查类别,按内存占用大小从大到小排序。
项目资产通常是内存的最高消耗者。使用表格模式,定位纹理、网格、音频剪辑、渲染纹理、着色器变体和预分配缓冲区。这些通常是优化内存使用时的良好起点。项目审计器在这里是一个很好的补充工具,因为它可以提供一些关于如何减少资产内存使用的建议(确保资产在导入设置检查器中正确设置是一个好的起点)。
定位内存泄漏
内存泄漏是指未使用的资产、对象或资源未从内存中正确释放的情况。这可能导致内存使用逐渐增加以及性能问题或崩溃。
内存泄漏通常发生在:
- 通过代码未手动从内存中释放对象。
- 由于另一个对象仍持有对它的引用,导致对象意外地保留在内存中。
内存分析器具有比较快照模式,可以通过比较特定时间范围内的两个快照来帮助查找内存泄漏。此比较可以揭示在应该被释放时仍然保留在内存中的对象。
在Unity游戏中,内存泄漏的一个常见场景是在卸载场景后。如果仍然存在对卸载场景中的对象的引用,这些对象可能不会被正确地垃圾回收。
定位应用程序生命周期中的重复内存分配
通过多个内存快照的差异比较,您可以识别在应用程序生命周期中持续内存分配的来源。
以下部分提供一些提示,以帮助识别项目中的托管堆分配。

Unity Profiler 中的内存分析器模块以红线表示每帧的托管分配。这通常应该是 0,因此该线中的任何峰值都表示您应该调查的托管分配帧。

CPU 使用分析器模块中的时间线视图以粉色显示分配,包括托管分配,使其易于关注。


CPU 使用分析器中的层次结构视图允许您单击列标题以将其用作排序标准。按 GC 分配排序是关注这些内容的好方法。

项目审计员,作为 Unity 6.1 中引入的一个包,是一个强大的分析工具,旨在帮助开发者优化性能,维护最佳实践,并识别项目中的潜在问题和瓶颈。
项目审计员扫描整个项目,并提供有关低效的详细报告,例如重度脚本调用、未使用的资产、过多的实体计数等。
项目审计员涵盖几个不同的领域:
性能优化:它识别可能影响项目运行时性能的问题,例如过多的垃圾生成、不必要的对象分配或昂贵的函数调用。
代码和资产审查:它突出显示未使用的资产、低效的代码模式或可以重构的过时 API。这有助于减少构建大小,提高整体项目可维护性并优化内存使用。
诊断和最佳实践:它根据 Unity 的最佳实践提供建议,并突出显示与项目设置相关的错误或警告,例如缺失的引用或次优的玩家或质量设置。
可定制的报告:它将结果组织成类别,使优化优先级变得简单。您还可以创建自定义规则,以根据您的特定项目或需求量身定制分析。
💡提示:
- 在开发的关键阶段运行项目审计员(例如,在里程碑、测试版发布、最终构建之前)。定期审计有助于及早发现性能瓶颈、未使用的资产或过时的代码,防止问题在项目扩展时变得更大。
- 您可以将项目审计员的运行自动化,作为 CI 或构建设置的一部分(如手册中这里所示),并使用报告确保没有人检查任何添加新问题的资产或代码(使用这里详细说明的 API)。
- 如果有特定的内容您想确保在游戏中捕获,您可以添加自己的规则;例如纹理设置、大小或更复杂的规则。有关如何执行此操作的更多详细信息,请参见手册中的此页面。
项目审计员生成的报告按严重性分类(主要、适中和信息)。首先关注最严重的问题,因为它们通常突显出性能关键的问题,例如内存过度分配或过度垃圾收集。它们也可能出现在更频繁调用的代码路径中,比如更新,在这些路径中,它们带来的任何性能问题对玩家来说会更加明显。
项目审计员还检查设置,如玩家设置和质量设置,并提出您可能更改的建议。使用此功能确保您的构建目标、分辨率、文本压缩或其他项目设置针对您预期的平台进行了优化。
域重载
Unity编辑器允许您配置有关进入播放模式的设置;此页面提供了更多详细信息,但您通常可以通过禁用域重载来加快编辑器迭代时间。然而,这将不再在每次进入播放模式时重置您的脚本状态,因此您必须在代码中手动执行此操作。
项目审计员中的代码区域可以分析您项目中的脚本,以帮助您找到需要重置脚本变量的地方。在域重载视图中修复所有显示的问题被认为是最佳实践,然后禁用域重载。要填充此视图数据,您必须在首选项窗口中启用使用Roslyn分析器设置。然后,您可以按照手册中的说明逐一解决问题。一旦所有问题都得到解决,您可以在进入播放模式时禁用域重载。

Unity使用博姆-德梅尔斯-韦瑟垃圾收集器在不再需要时自动清理内存。GC停止运行您的程序代码,并且仅在其工作完成后恢复正常执行。
虽然自动管理很方便,但不必要或频繁的分配可能导致性能问题,因为垃圾收集器必须暂停您的游戏以清理未使用的内存(也称为GC峰值)。以下是一些常见的陷阱:
字符串:在C#中,字符串是引用类型,而不是值类型。这意味着每个新字符串都将在托管堆上分配,即使它只是暂时使用。减少不必要的字符串创建或操作。避免解析基于字符串的数据文件,如 JSON 和 XML,而是将数据存储在 ScriptableObjects 或像 MessagePack 或 Protobuf 这样的格式中。如果需要在运行时构建字符串,请使用 StringBuilder 类。
Unity 函数调用:某些 Unity API 函数会创建堆分配,特别是那些返回临时托管对象数组的函数。缓存对数组的引用,而不是在循环中间分配它们。此外,利用某些避免生成垃圾的函数。例如,使用 GameObject.CompareTag 而不是手动将字符串与 GameObject.tag 进行比较(因为返回一个新字符串会产生垃圾)。
您还可以使用项目审计器列出这些替代方案;这可以帮助确保您在可能的情况下使用非分配版本。
装箱:装箱发生在值类型(例如,int、float、struct)被转换为引用类型(例如,object)时。避免将值类型变量作为引用类型变量传递。这会创建一个临时对象,并且随之而来的潜在垃圾会隐式地将值类型转换为类型对象(例如,int i = 123; object o = i)。相反,尝试提供您想要传递的值类型的具体重写。泛型也可以用于这些重写。
协程:虽然 yield 不会产生垃圾,但创建一个新的 WaitForSeconds 对象会。缓存并重用 WaitForSeconds 对象,而不是在 yield 行中创建它。
LINQ 和正则表达式:这两者都会在后台生成垃圾。如果性能是一个问题,请避免使用LINQ和正则表达式。编写for循环,并使用列表作为创建新数组的替代方案。
泛型集合和其他托管类型:不要在Update中每帧声明和填充一个列表或集合(例如,玩家周围一定半径内的敌人列表)。相反,将列表作为MonoBehaviour的成员,并在Start中初始化它。在使用之前,每帧简单地用Clear清空集合。
尽可能进行时间垃圾收集
如果你确定垃圾收集的冻结不会影响游戏中的特定时刻,可以使用System.GC.Collect触发垃圾收集。一个经典的例子是用户在菜单中或暂停游戏时,这时不会引起注意。
请参见理解自动内存管理,了解如何利用这一点。
使用增量垃圾收集器来分配GC工作负载
增量垃圾收集使用多个较短的中断来分配工作负载,而不是在程序执行期间创建单个长时间的中断。如果垃圾收集导致不规则的帧率,请尝试此选项以查看是否减少GC峰值问题。使用性能分析器验证其对应用程序的好处。
请注意,在增量模式下使用GC会为某些C#调用添加读写障碍,这会带来一些开销,可能会增加到每帧约1毫秒的脚本调用开销。为了获得最佳性能,理想情况下在主要游戏循环中没有GC分配,这样你就不需要增量GC来保持平滑的帧率,并且可以在用户不会注意到的地方隐藏GC.Collect,例如在打开菜单或加载新关卡时。在这种优化场景中,你可以执行完整的非增量垃圾收集(使用System.GC.Collect())。
要了解更多关于内存分析器的信息,请查看以下资源:

您可以在Unity最佳实践中心找到更多针对高级Unity开发者和创作者的最佳实践和技巧。从超过30个指南中选择,这些指南由行业专家、Unity工程师和技术艺术家创建,将帮助您高效地使用Unity的工具集和系统进行开发。