WebGL 性能注意事项
WebGL:与浏览器脚本交互

以 WebGL 为目标平台时的内存注意事项

Unity WebGL 中的内存可能成为限制可运行内容的复杂性的制约因素,因此我们希望在此提供一些关于如何在 WebGL 中使用内存的说明。

WebGL 内容将在浏览器中运行,因此浏览器必须在浏览器的内存空间内分配内存。可用内存量可能会有很大差异,具体取决于所使用的浏览器、操作系统和设备。决定因素包括浏览器是 32 位还是 64 位进程,浏览器为每个标签使用单独的进程还是让您的内容与所有其他打开的标签共享内存空间,以及浏览器的 JavaScript 引擎需要多少内存来解析您的代码。

Unity WebGL 内容在几个方面需要浏览器分配大量内存:

Unity 堆

这是 Unity 用于存储所有状态、托管的对象和本机对象以及当前加载的资源和场景的内存。此内存类似于 Unity 播放器在其他平台上使用的内存。可在 Unity WebGL 播放器设置中配置此内存的大小(但为了更快迭代,还可编辑向生成的 html 文件写入的 TOTAL_MEMORY 值)。您可以使用 Unity Profiler 来分析和采样此内存的内容。此内存将在 JavaScript 代码中创建为字节的 TypedArray,并要求浏览器能够分配此大小的连续内存块。您将希望此空间尽可能小(如此一来,即便内存有碎片,浏览器也能够分配此空间大小),但大小应足以容纳播放内容的任何场景时所需的所有数据。

资源数据

创建 Unity WebGL 构建时,Unity 将写出一个 .data 文件,其中包含您的内容所需的所有场景和资源。由于 WebGL 没有真正的文件系统,因此在您的内容启动之前将下载此文件,并且在内容运行的整个过程中,未压缩的数据将保存在连续的浏览器内存块中。因此,为了缩短下载时间和降低内存使用量,应尽量保持这些数据尽可能小。请参阅文档的减小文件大小页面以了解如何优化资源的构建大小。

为减少加载时间和资源使用的内存量,可采取的另一项措施是将资源数据打包为 AssetBundle。通过这种做法,您可以完全控制何时需要下载资源,并可在不再需要它们时卸载它们,从而释放它们使用的内存。请注意,AssetBundle 将直接加载到 Unity 堆中,并且不会导致浏览器进行额外的分配(除非您使用 WWW.LoadFromCacheOrDownload 进行资源包缓存;此情况下将使用内存映射式虚拟文件系统,由浏览器的 IndexedDB 提供支持)。

解析代码所需的内存

与内存相关的另一个问题是浏览器的 JavaScript 引擎所需的内存。Unity 将发出非常大的文件,其中包含生成的数百万行 JavaScript 代码,比浏览器中常用的 JavaScript 代码大一个数量级。有些 JavaScript 引擎可能会分配一些相当大的数据结构来解析和优化此代码,因此,在某些情况下加载内容时可能会导致高达数千兆字节 (GB) 的内存峰值。我们希望像 WebAssembly 这样的未来技术最终会消除这一问题,但在此之前,我们可以给出的最佳建议是尽可能减小发出的代码大小。请参阅此处有关分发大小的说明,进一步了解如何做到这一点。

处理内存问题

在 Unity WebGL 构建中看到与内存相关的错误时,必须了解原因是浏览器无法分配内存,还是 Unity WebGL 运行时无法在 Unity 堆的预分配块内分配空闲内存块,这一点很重要。如果原因是浏览器无法分配内存,尝试减小上面所述的一个或多个方面的内存使用量(例如减小 Unity 堆的大小)可能会有所帮助。另一方面,如果是 Unity 运行时无法在 Unity 堆内分配内存块,则可能需要增加堆的大小。

Unity 将尝试解释错误消息以判断问题在于两个原因中的哪一个原因(并提供关于解决方法的建议)。但是,做出此判断并不总是很容易,因为不同的浏览器可能会报告不同的消息,我们可能无法解释所有这些消息。在浏览器中出现一般的“内存不足”错误时,可能是浏览器内存不足的问题(这种情况下可能需要使用更小的 Unity 堆)。此外,有时可能会看到浏览器在加载 Unity 内容时直接崩溃,不会显示可人为解析的错误消息。这种问题可能有很多原因,但通常的原因是 JavaScript 引擎在解析和优化生成的代码时需要的内存太多。

Large-Allocation Http 标头

您的服务器可为您的内容发出 Large-Allocation http 标头。此目的是向支持的浏览器(目前只有 Firefox)告知您的内存需求,从而让浏览器生成一个具有未分段内存空间的新进程,或执行其他内务处理 (housekeeping) 以确保大型分配能够成功。这样就能解决在尝试分配 Unity 堆时的浏览器内存不足问题,尤其是在 32 位浏览器上。

垃圾收集注意事项

在 Unity 中分配托管的对象时,需要在不再使用这些对象时进行垃圾收集。请参阅有关自动内存管理的文档以了解更多信息。在 WebGL 中也是如此。托管的垃圾收集的内存在 Unity 堆内进行分配。

然而,WebGL 有一个不同之处在于可进行垃圾收集 (GC) 的时间点。要执行垃圾收集,GC 通常需要暂停所有正在运行的线程,并检查其堆栈和寄存器是否有加载的对象引用。目前在 JavaScript 中还无法实现这一点。出于此原因,在 WebGL 中,仅当已知堆栈为空的情况下才会运行 GC(当前在每帧之后运行一次)。对于大多数对托管内存进行保守处理并且每帧内的 GC 分配量相对较少的内容而言,这不是问题(可使用 Unity Profiler 对此进行调试)。

但是,如果存在如下代码:

string hugeString = "";

for (int i = 0; i < 100000; i++)
{
    hugeString += "foo";
}

此代码将无法在 WebGL 上运行,因为它不会有机会在循环的迭代之间运行 GC,进而不能释放所有中间字符串对象使用的内存,最终将导致在 Unity 堆中耗尽内存。

WebGL 性能注意事项
WebGL:与浏览器脚本交互