了解 Unity WebGL 中的内存

有些用户已经熟悉了内存有限的平台。对于其他来自台式机或网络播放器的用户来说,在此之前这从来都不是问题。
在这方面,以控制台平台为目标相对容易,因为你可以清楚地知道有多少内存可用。这样,您就可以对内存进行预算,保证内容的运行。在移动平台上,由于有许多不同的设备,情况要复杂一些,但至少你可以选择最低规格,并决定在市场层面将低端设备列入黑名单。
在网络上,你根本无法做到这一点。理想情况下,所有终端用户都拥有 64 位浏览器和大量内存,但这与现实相去甚远。此外,也无法知道内容运行的硬件规格。你只知道操作系统和浏览器,其他就不知道了。最后,终端用户可能在运行 WebGL 内容的同时也在运行其他网页。这就是为什么这是一个棘手的问题。
以下是在浏览器中运行 Unity WebGL 内容时的内存概览:

此图显示,在 Unity 堆之上,Unity WebGL 内容需要在浏览器内存中进行额外分配。了解这一点非常重要,这样才能优化项目,从而最大限度地降低用户流失率。
从图中可以看到,有几组分配:DOM、Unity 堆、资产数据和代码,网页加载后,这些数据和代码将持久存在内存中。其他内容,如资产包、WebAudio 和内存 FS 将根据内容中发生的情况(如资产包下载、音频播放等)而有所不同。
在加载时,asm.js 的解析和编译过程中还会有一些浏览器的临时分配,有时会导致一些使用 32 位浏览器的用户出现内存不足的问题。
一般来说,Unity 堆是包含所有 Unity 特定游戏对象、组件、纹理、着色器等的内存。
在 WebGL 上,需要提前知道 Unity 堆的大小,以便浏览器为其分配空间,而且缓冲区一旦分配,就不能缩小或增大。
负责分配 Unity 堆的代码如下:
buffer = new ArrayBuffer(TOTAL_MEMORY);
这些代码可以在生成的 build.js 中找到,并将由浏览器的 JS VM 执行。
TOTAL_MEMORY 由播放器设置中的 WebGL 内存大小定义。默认值为 256MB,但这只是我们任意选择的一个值。事实上,一个空项目只需 16 MB。
不过,真实世界的内容可能需要更多,大多数情况下需要 256 或 386 MB。请记住,所需的内存越大,能够运行它的最终用户就越少。
在执行代码之前,必须先执行代码:
已下载。
复制到一个文本块中。
编译。
请注意,每个步骤都需要占用大量内存:
- 下载缓冲区是临时的,但源代码和编译代码在内存中是持久的。
- 下载缓冲区和源代码的大小都是 Unity 生成的未压缩 js 的大小。估算它们需要多少内存:
- 发布
- 将 jsgz 和 datagz 重命名为 *.gz,然后用压缩工具解压缩
- 其未压缩的大小也将是浏览器内存中的大小。
- 编译代码的大小取决于浏览器。
一个简单的优化方法是启用 "剥离引擎代码 "功能,这样在构建时就不会包含不需要的本地引擎代码(例如...):如果不需要,2d 物理模块将被删除)。请注意:请注意:托管代码总是被剥离。
请记住,异常支持和第三方插件会增加代码量。尽管如此,我们还是遇到过一些用户,他们需要在标题中加入空值检查和数组边界检查,但又不希望因为完全异常支持而产生内存(和性能)开销。要做到这一点,可以通过编辑器脚本等方式向 il2cpp 传递--emit-null-checks和--enable-array-bounds-check参数:
PlayerSettings.SetPropertyString("additionalIl2CppArgs", "--emit-null-checks --enable-array-bounds-check");
最后,请记住,开发构建会产生较大的代码,因为这些代码没有经过最小化处理,不过这并不重要,因为您只打算向最终用户发送发布构建......对吗?)
在其他平台上,应用程序只需访问永久存储(硬盘、闪存等)上的文件即可。在网络上,这是不可能的,因为无法访问真正的文件系统。因此,Unity WebGL 数据(.data 文件)一旦下载,就会存储在内存中。缺点是与其他平台相比,它需要额外的内存(从 5.3 版开始,.data 文件以 lz4 压缩方式存储在内存中)。例如,下面是剖析器对一个生成约 40 MB 数据文件(256 MB Unity 堆)的项目的描述:

数据文件中有什么内容?这是 unity 生成的一系列文件:data.unity3d(所有场景、其附属资产和 Resources 文件夹中的所有内容)、unity_default_resources 和引擎所需的一些小文件。
要了解资产的确切总大小,请在为 WebGL 构建后查看 Temp\StagingArea\Data 中的 data.unity3d(请记住,当 Unity 编辑器关闭时,Temp 文件夹将被删除)。或者,您也可以查看传递给 UnityLoader.js 中 DataRequest 的偏移量:
new DataRequest(0, 39065934, 0, 0).open('GET', '/data.unity3d');
(这段代码可能会根据 Unity 版本的不同而有所变化,这是 5.4 版本的代码)
虽然没有真正的文件系统,但正如我们前面提到的,Unity WebGL 内容仍然可以读/写文件。与其他平台的主要区别在于,任何文件 I/O 操作实际上都是在内存中读写。需要注意的是,该内存文件系统不在 Unity 堆中,因此需要额外的内存。例如,假设我将一个数组写入文件:
var buffer = new byte [10*1014*1024];
File.WriteAllBytes(Application.temporaryCachePath + "/buffer.bytes", buffer);
文件将被写入内存,这也可以在浏览器的剖析器中看到:

请注意,Unity 堆大小为 256MB。
同样,由于 Unity 的缓存系统依赖于文件系统,因此整个缓存存储都备份在内存中。这是什么意思?这意味着PlayerPrefs和缓存 Asset Bundles 等内容也将在 Unity 堆之外的内存中持久存在。

如您所见,已为 Unity 堆分配了 256 MB。这是在下载不带缓存的资产包之后的情况:

现在你看到的是一个额外的缓冲区,大小大约与磁盘上的 bundle 相同(约 65 MB),由XHR 分配。这只是一个临时缓冲区,但它会导致内存峰值持续几帧,直到它被垃圾回收。
那么如何尽量减少内存峰值呢?虽然这个想法很有趣,但并不实用。
最重要的是,没有通用的规则,你确实需要做对你的项目更有意义的事情。
最后,请记住在使用完毕后通过AssetBundle.Unload卸载资产包。
资产捆绑缓存的工作原理与其他平台相同,您只需使用WWW.LoadFromCacheOrDownload 即可。但有一个相当大的区别,那就是内存消耗。在 Unity WebGL 上,AB 缓存依靠IndexedDB来持久存储数据,问题是 DB 中的条目也存在于内存文件系统中。
让我们看看使用 LoadFromCacheOrDownload 下载资产包之前的内存捕获:

如您所见,512MB 用于 Unity 堆,约 4MB 用于其他分配。这是在加载捆绑包之后:

所需的额外内存跃升至 ~167MB。这就是这个资产包(约 64mb 压缩包)所需的额外内存。这是在 js 虚拟机垃圾回收之后:

情况稍好,但仍需要 ~85MB 的内存:其中大部分用于缓存内存文件系统中的资产包。即使卸下捆绑包,你也无法取回这些记忆。同样重要的是要记住,当用户第二次在浏览器中打开您的内容时,甚至在加载捆绑包之前,该内存块就会被立即分配。
这是 Chrome 浏览器的内存快照,以供参考:

同样,在 Unity Heap 之外,我们的资产捆绑系统还需要另一种与缓存相关的临时分配。坏消息是,我们最近发现它比预期的要大得多。不过好消息是,即将发布的 Unity5.5 Beta 4、5.3.6 补丁 6和5.4.1 补丁 2 已经修复了这一问题。
对于旧版本的 Unity,如果您的 Unity WebGL 内容已经发布或即将发布,而您又不想升级项目,那么可以通过编辑器脚本快速设置以下属性:
PlayerSettings.SetPropertyString("emscriptenArgs"," -s MEMFS_APPEND_TO_TYPED_ARRAYS=1",BuildTargetGroup.WebGL);
要尽量减少资产包缓存内存开销,一个长期的解决方案是使用WWW 构造函数而不是LoadFromCacheOrDownload(),或者使用UnityWebRequest.GetAssetBundle()(如果使用的是新的UnityWebRequestAPI,则无需哈希值/版本参数)。
然后在 XMLHttpRequest 层使用另一种缓存机制,绕过内存文件系统,将下载的文件直接存储到索引数据库中。这正是我们最近开发的产品,它可以在资产商店中找到。请在您的项目中随意使用,并根据需要进行定制。
Unity WebGL 上的音频以不同方式实现。这对记忆意味着什么?
Unity 将在 JavaScript 中创建特定的AudioBuffer对象,以便通过 WebAudio 播放。
由于 WebAudio 缓冲区位于 Unity 堆之外,因此 Unity 剖析器无法跟踪,您需要使用浏览器专用工具检查内存,以查看音频使用了多少内存。下面是一个示例(使用 Firefox 的about:memory页面):

需要注意的是,这些音频缓冲区保存的是未压缩的数据,对于大型音频片段资产(如背景音乐)来说可能并不理想。对于这些人,您可能需要考虑编写自己的 js 插件,以便使用 <audio> 标记来代替。这样,音频文件就能保持压缩状态,从而使用更少的内存。
以下是摘要:
缩小统一堆的大小:
尽可能减小 "WebGL 内存大小
减少代码量
启用剥离引擎代码 禁用异常 尽量避免使用第三方插件
减少数据大小:
使用资产包 使用 Crunch 纹理压缩
是的,最佳策略是使用内存分析器分析内容实际需要多少内存,然后相应地更改 WebGL 内存大小。
让我们以一个空项目为例。内存分析器告诉我,"已用总内存 "刚刚超过 16MB(不同版本的 Unity 可能会有所不同):这意味着我必须将 WebGL 内存大小设置为比这更大的值。显然,"已用总量 "会根据您的内容而有所不同。
不过,如果由于某种原因无法使用 Profiler,您只需不断减少 WebGL 内存大小值,直到找到运行内容所需的最小内存量。
还需要注意的是,由于 Emscripten 的要求,任何数值如果不是 16 的倍数,都会在运行时自动四舍五入到下一个倍数。
WebGL 内存大小 (mb) 设置将决定生成的 html 中的 TOTAL_MEMORY(字节)值:

因此,为了在不重新构建项目的情况下重复计算堆的大小,建议修改 html。找到满意的值后,就可以在 Unity 项目中更改 WebGL 内存大小。
值得庆幸的是,这并不是唯一的方法,下一篇关于 Unity 堆的博文将尝试为这个问题提供更好的答案。
最后,请记住,Unity 的剖析器将使用已分配堆中的部分内存,因此在剖析时可能需要增加 WebGL 内存大小。
这取决于是 Unity 内存不足还是浏览器的问题。错误信息将说明问题所在和解决方法:"如果您是该内容的开发者,请尝试在 WebGL 播放器设置中为您的 WebGL 构建分配更多/更少的内存"。然后可以相应调整 WebGL 内存大小设置。不过,您还可以采取更多措施来解决 OOM 问题。如果收到此错误信息:

除了信息中提到的方法,您还可以尝试减小代码和/或数据的大小。这是因为当浏览器加载网页时,它会尝试为几样东西寻找空闲内存,其中最重要的是:代码、数据、统一堆和编译后的 asm.js。它们可能相当大,尤其是 Data 和 Unity 堆内存,这对 32 位浏览器来说可能是个问题。
在某些情况下,即使有足够的可用内存,浏览器仍然会因为内存碎片而失效。这就是为什么有时重新启动浏览器后,您的内容可能无法成功加载的原因。
另一种情况是,当 Unity 运行到内存不足时,会提示类似这样的信息:

在这种情况下,您需要优化 Unity 项目。
要分析内容占用的浏览器内存,可以使用 Firefox内存工具或 Chrome 浏览器堆快照。不过请注意,它们不会显示 WebAudio内存,为此您可以使用 Firefox 中的about:memory页面:拍摄快照,然后搜索 "webaudio"。如果需要通过 JavaScript 配置内存,请尝试window.performance.memory(仅限 Chrome 浏览器)。
要测量 Unity 堆内的内存使用情况,请使用 Unity Profiler。不过请注意,为了能够使用剖析器,您可能需要增加 WebGL 内存大小。
此外,我们还开发了一个新工具,可以让您分析构建中的内容:要使用它,请创建 WebGL 版本,然后访问http://files.unity3d.com/build-report/。尽管该功能在 Unity 5.4 中已经可用,但请注意该功能仍在开发过程中,可能随时更改或删除。不过,我们暂时将其用于测试目的。
16 是最小值。最大值为 2032,但我们一般建议保持在 512 以下。
这是技术上的限制:2048 MB(或更大)将溢出 JavaScript 中用于实现 Unity 堆的 TypeArray 的 32 位有符号整数大小。
我们一直在考虑使用 ALLOW_MEMORY_GROWTH emscripten 标志来调整堆的大小,但目前决定不这样做,因为这样做会禁用 Chrome 浏览器中的某些优化功能。我们还没有对其影响进行真正的基准测试。我们预计,使用这种方法可能会使内存问题更加严重。如果 Unity 堆太小,无法容纳所有所需的内存,需要增加,那么浏览器就必须分配一个更大的堆,将旧堆中的所有内容复制过来,然后再取消分配旧堆。这样一来,新堆和旧堆同时需要内存(直到完成复制),从而需要更多的总内存。因此,内存使用量会比使用预定的固定内存大小时更高。
无论操作系统是 64 位还是 32 位,32 位浏览器都会遇到相同的内存限制。
最后一项建议是使用特定于浏览器的工具对 Unity WebGL 内容进行剖析,因为正如我们所描述的,Unity 的剖析器无法跟踪 Unity 堆之外的分配。
希望这些信息对您有用。如果您有其他问题,请随时在这里或WebGL 论坛提问。
更新:
我们谈到过代码使用的内存,也提到过 JS 源代码会被复制到一个临时文本块中。我们发现,blob没有被正确删除,因此它实际上是浏览器内存中的永久分配。在 about:memory 中,它被标记为 memory-file-data:

其大小取决于代码的大小,对于复杂的项目,可以轻松达到 32 或 64 MB。值得庆幸的是,5.3.6 第 8 补丁、5.4.2 第 1 补丁和 5.5补丁已经修复了这一问题。
在音频方面,我们知道内存消耗仍然是一个问题:目前不支持音频流,音频资产目前以未压缩的形式保存在浏览器内存中。因此,我们建议使用 <audio> 标签来播放大型音频文件。为此,我们最近发布了一个新的 Asset Store软件包,帮助您最大限度地减少流音频源的内存消耗。来看看