使用新版Memory Profiler(内存分析器)检查内存

本文将介绍新Memory Profiler软件包的五种关键流程,帮助您诊断并检查游戏里的内存问题。它们有:
- 监测应用的内存压力
- 查看Unity对象的分布
- 检测配置不当的资产
- 找到意外复制出的对象
- 对比内存快照来检验优化
有关内存分析器的介绍,请参阅最近的博客《 您需要了解的有关内存分析器 1.0.0 的所有信息》。
第一种用法是监测应用对设备内存资源的要求。分析器可以检测应用是否有出现性能问题的风险,或由于消耗了过多内存而被操作系统“驱逐”(eviction)或终止。
首先,我们构建一个 在目标设备上运行的示例游戏。我们必须在实际硬件上截取游戏的内存占用情况,才能查看游戏如何使用着当前可用的内存资源。另外,Unity编辑器和Unity运行时的内存运作并不一样,光是Play Mode(运行模式)下的内存快照并不能很好地代表实际设备上的内存占用。(如果开发的是编辑器工具,比如定制的编辑器窗口,则截取编辑器的内存占用是合适的。)
在到达需要分析内存的位置后,我们可以在Memory Profiler的下拉菜单里将分析器关联到设备上。接着截取内存快照,如下图。

打开此捕获后,内存分析器会在摘要页面顶部将我们应用程序的内存占用量显示为“设备内存使用情况”。

可以看到应用内存占了492.5 MB,总内存有3.50 GB。这时我们需要作出判断,截图位置所占用的物理内存(RAM)是否处于合理区间。别忘了设备的物理内存由所有进程共享。
您会注意到,这个视觉指示器向您显示了 总驻留内存。总占用内存是指有多少应用内存位于物理内存(RAM)内。为什么说这项指标能明确展示出应用当前对设备的内存要求呢?有两个原因。首先,应用所占用的总内存增加,操作系统需要经常起草虚拟内存来输入和移出物理内存,页面频繁出错的可能性也会增加。频繁的页面出错会造成应用出现明显的性能降级。其次,这是因为许多操作系统使用应用程序的驻留内存使用情况来确定其当前的 内存占用量。如果应用内存足迹过多,操作系统会“驱逐”并终止应用,使运行版崩溃。
因此,您可以使用内存分析器 中的设备内存使用情况 可视化指示器来推断应用程序是否可能因捕获时内存过度使用而面临性能问题风险或被操作系统终止。
这与 “已分配内存”(有时也称为“ 已提交内存”)形成对比,您可能会注意到它显示在此指示器下方的各种图形中,并且当前是所有其他视图(例如 Unity 对象)显示的默认选项。Allocated Memory是指应用当前所分配的所有内存,无论是物理内存还是其他内存,它更贴近应用角度下的内存。这部分可用于了解应用当前分配的内存,但要明白应用为硬件带来的内存压力,驻留在物理内存的占用才是关键。
内存分析器的 Unity Objects 选项卡从 Unity Objects 的角度为您提供了应用程序内存的概览;其中包括应用程序的纹理、着色器、网格、材质等等。这部分对大多数Unity用户来说都非常熟悉,因为这是Unity编辑器里开展工作的重心,从这里开始熟悉Memory Profiler将是个不错的起点。它不仅以熟悉的概念解释了应用内存的组成,还能提供Unity相关的上下文,帮忙诊断并修复一系列潜在问题。

要打开Unity Objects视图,您只需如上图打开一张内存快照,并选择Memory Profiler顶部的Unity Objects一栏。
您可以看到 Unity Objects 视图如何让我们快速了解应用程序中 Unity Object 类型 的分布。这样,我们既可以从高层了解在捕获时哪些类型消耗了最多的内存,也可以对此进行推理,例如是否预计某个特定场景会有大量 AudioClip 对象。每个类型还可展开来查看每个被分配了内存的Unity对象,如下方所示。

重要的是要记住,Unity 对象占我们应用程序总分配内存的 一部分 。这部分的总占用量会在表的上方显示,即下方高光部分。

可以看到,“Total Memory In Snapshot”,即应用被分配的总内存量为4.64 GB,其中有2.37 GB是Unity对象。倘若我们使用搜索功能筛选这张表,计量槽也会根据搜索结果进行更新。换句话说,它只显示表格内容的内存量。这能帮您了解自己正在检查整段记录的哪一块,为找到优化方向提供参考。

在内存分析器 1.0 版中,Unity 对象表会显示已分配的内存,或者换句话说,它会显示应用程序中处于活动状态的所有 Unity 对象。我们正在尝试在下一次发布添加Resident Memory视图,让您能查看哪些Unity对象驻留在物理内存中,组成了应用当前的内存足迹。
您可以使用“所有内存”选项卡检查捕获时应用程序内存的剩余部分,其中包括 Unity 对象之外的内存,例如各种 Unity 子系统、仅托管(C#)内存以及 DLL 和可执行文件。
Unity Objects视图可以诊断出许多潜在问题。其中一种问题就是资产配置不当、占用过多内存。
在下方截图可以看到,有很大一部分Unity对象的内存都是纹理。该项目使用了高清渲染管线和大量视觉效果来实现极高的图像保真度。有了这一前提,纹理肯定会占用大量内存,这与截图相符。

可如果展开Texture2D类别,可以注意到有两张纹理的体积看起来比其他的大得多。根据我们对项目的理解,我们惊讶地发现这些纹理比同类纹理(如 HoloTable_Normal 或 HoloTable_Mask)更大,因为我们预计它们的大小相似。

于是,我们在表格内选择其中一张来了解详情、调查原因。Details视图给出了解释——这张纹理是可写入的,即“Read/Write Enabled”(启用读取/写入)。

这也是许多项目的常见问题:意外地勾选了纹理导入设定的“Read/Write”选框。当纹理启用了这个标签,其内存占用量也会翻倍。系统必须保留一份纹理数据的副本才能在CPU上继续访问它。这一问题最明显的现象就是纹理的Total Size(总大小)是预期或类似纹理的两倍。
在禁用两张纹理的“Read/Write”标签后我们再来截取一张截图,可以看到两者的体积变为了一半。

我们正尝试在下一次发布为Unity Objects表格添加一列图形(GPU)内存,使得像这种Unity对象占用图形内存的情况能更容易被发现。
我们经常在Unity项目里看到Unity对象会不经意地被复制。比如,访问MeshRenderer的材质属性很容易意外地多复制出一张材质。如果特定MeshRednerer的每个实例都开始复制,这种情况很容易迅速恶化,并且,这些动态创建的材质必须特地摧毁。
为了帮助找出此类问题,Unity Objects表格带有只显示Unity复制对象的筛选条件。它将筛选出名字和大小完全相同的Unity对象实例。需要注意的是,许多复制是应该的,算不上是问题。比如,数个预制件实例可能带有相同名称和大小的Transform组件,这些都是正常的。我们需要做的是发现那些非故意的复制,正如下方例子所展示的。
下面的捕获是在一个简单的场景中拍摄的,其中包含两个门预制件的实例,并且我们启用了位于 Unity 对象表下方的 “仅显示潜在重复项” 过滤器。系统筛选出了那些带有多个名称、大小相同的实例的Unity对象。

因为我们的场景中有两个门预制件的实例,所以正如预期的那样,我们也拥有所有相关对象的两个实例:MeshRenderer、Transform、GameObject等等。然而,截图里还有两份“Door”材质实例。鉴于两扇门的外观完全相同,两个实例完全可以共享一份材质。因此,这就算非故意的复制,本例中造成这一现象的原因是我们访问了预制件MeshRenderer的材质属性。移出属性的访问权限后再截取一张截图,表格中便不再存在有复制的材质。

需要注意的是,此筛选仅负责显示所有带重复实例的Unity对象。您需要依靠自己对项目的了解来判断复制体是应该出现的还是意外复制出来的。我们建议您关注顶部的 “表中的总内存” 栏,它可以让您直观地了解表中所看到的应用程序分配内存的比例。这可以帮助您了解应该在何处投入优化精力。
Memory Profiler同样提供截图对比功能。我们可以据此对项目做出修改,解决找到的问题,或者测试改动是否产生了预期的效果。不时检验假设是否正确、改动能否在实际硬件上产生预想的作用是非常重要的。这里,我们来看一看对比工作流的一个实例。
下方截图截自我们移动端游戏《Boss Room》的第一关。可以看到占比最大的Unity对象类别是Texture2D。我们点开类别来查找哪些纹理最大,发现部分UI纹理要比游戏的其他部分大得多,每张都超出了1 MB。这让我们产生了一个怀疑:为什么这些纹理比其他纹理大得多,它们需要这样吗?为了发掘出原因,我们在窗口里选中纹理并点击“Select In Editor”按钮,在Project窗口高光显示出资产,找到原纹理。

在监视器里,我们可以看到这些突兀的资产由于没有在尺寸上采用“二的乘方”而未被压缩,文件名的“NPOT”(non-power-of-two)也佐证了这一点。

这就解释了它们为何体积过大。此时,我们可以根据对项目的理解来降低这部分内存占用。这其中,有三种纹理(控制提示)总会同时出现在UI里,而另外三张纹理(哥布林)同样也一定会同时出现。因此,我们可以放心地假定,创建两张Sprite Atlas(精灵图集)来分别保存三张纹理可以降低内存占用,精灵图集可以被压缩,也不会产生额外的纹理。

要对比两张快照,请首先打开第一张快照。这是对比时的“对照组”。接着选择快照上方的“Compare Snapshots”一栏并选择第二张快照。Memory Profiler会显示出两张快照的对比结果,如下图。

为了查看并验证这次修改,即是否降低了Texture2D类的应用内存,我们可以选择Unity Objects栏。视图里,表格会对比被修改的Unity对象,以及修改前后所产生的变化(如下方所示)。

可以看到Texture2D的总大小降低了3.6 MB,纹理数比之前少了四张。展开类别,可以看到原本单独、未压缩的精灵纹理消失了,取而代之的是两张Sprite Atlas,使得内存降低了3.6 MB,Texutre2D对象少了4个。

成功了!我们的假设是正确的,压缩后纹理在内存里的体积被降低了。
