【Unity】开发WebGL内存概念具体解释和遇到的问题

时间:2021-04-21 10:49:06

自增加unity WebGL平台以来。Unity的开发团队就一直致力于优化WebGL的内存消耗。

我们已经在Unity使用手冊上有对于WebGL内存管理的详尽分析,甚至在Unite
Europe 2015与Unite Boston 2015两届大会上,也有专题对其进行深入的解说。然而,这方面的内容依然是用户讨论的热门话题,因此我们意识到应当分享很多其它。希望本文能回答一些被频繁咨询的问题。

 

Unity WebGL与其他平台有何不同?

一些用户已经熟悉了部分内存有所限制的的平台。而对于其他如桌面和WebPlayer平台。到眼下为止内存还不是问题。



在内存方面,主机平台相对其他平台较为简单。由于您能够准确的知道内存是怎样使用的。

这同意您能够非常好的管理内存,并保证您的游戏内容完美执行。

在移动平台,内存管理变的有些复杂,由于设备种类繁多。但至少您能够选择最低标准的设备,并依据市场情况忽视那些相较于该标准更为的低端设备。



在网页平台,就没有那么轻松了。理想情况下,全部终端用户都拥有64位浏览器和大量内存。但事实却相距甚远。首先。您无法通过不论什么方法知道,正执行您的内容的硬件规格。其次。除了用户的操作系统和浏览器外,您并不知道其他信息。最后。终端用户可能像执行其他网页一样执行您的WebGL内容。

因此这是一个很复杂的问题。



概览



下图是在浏览器上执行Unity WebGL内容时的内存概览:

【Unity】开发WebGL内存概念具体解释和遇到的问题

上图展示了Unity 堆,Unity WebGL内容将须要向浏览器请求额外分配的内存。这是理解WebGL内存管理的重点。从而让您优化项目得以将用户流失率降至最低。



正如上图所看到的。存在几组内存分配:DOM,Unity堆,资源数据和代码,这些内容都会在网页载入时持久存在于内存中。而其他诸如 Asset Bundles, WebAudio 和 Memory FS 何时载入则取决于您的内容执行情况。

(比如:Asset Bundle下载,音频播放等等)



在载入期间, 一些浏览器在asm.js解析和编译时会产生暂时内存分配。这偶尔也会导致部分使用32位浏览器的用户出现内存溢出的问题。



Unity堆



通常来说。Unity堆是指包括了全部Unity特有的游戏对象、组件、纹理、着色器 等等的内存块。



在WebGL平台,Unity堆的大小须要提前获知。浏览器才干对此分配空间。而且内存空间一旦分配,就无法改变内存缓冲区大小。



负责Unity 堆内存分配的代码例如以下:



1.buffer = new ARrayBuffer(TOTAL_MEMORY);



这段代码能够在所生成的build.js中找到。并通过浏览器的JS虚拟机来运行。

TOTAL_MEMORY 是在Player Settings 中的WebGL Memory Size中设置的总内存。默觉得256MB,但这是我们任意设定的值,其实。一个空项目执行仅需16MB。



然而。真实世界中游戏内容可能会须要很多其它的内存空间。大部分情况下都须要256或者386MB。请记住。项目须要的内存越多。可以执行它的终端用户就越少。

源码/编译代码内存



在代码能够被运行之前,它须要例如以下步骤:

  • 下载
  • 拷贝到一个文本域
  • 编译

请谨慎考虑,上述的每个步骤都将请求大量内存。

由于:

  • 下载缓冲区是暂时的。可是源码和编译代码将持久存在于内存中。
  • 下载缓冲区和源码大小,都是Unity所生成的未压缩的js大小。

    依照下面步骤,您能够估算它们须要多少内存:

    • 构建一个公布版本号。
    • 将jsgz 、datagz重命名为*.gz文件,并通过压缩工具对它们进行解包。
    • 解压缩后的大小就是它们在浏览器内存中的大小。
  • 编译代码的大小取决于浏览器。

优化内存的一个简单方法是启用Strip Engine Code。这样您公布的版本号将不包括那些不必需的原生

gid=23" style="word-wrap:break-word; color:rgb(51,51,51); text-decoration:none; font-weight:bold">引擎

代码(比如:假设不须要2D物理模块,它将被剥离)。

请注意:托管代码一定会被剥离。



千万要记住。异常捕捉和第三方插件也将添加代码大小。

正如之前所说,我们已经注意到用户须要加入空值检查和数组边界检測的代码,但不希望完整的异常检測支持会带来过多的内存(及性能)消耗。

要实现这点,您能够通过编辑器脚本传递 

–emit-null-checks 和 –enable-array-bounds-check 到il2cpp,比如



PlayerSettings.SetPropertyString("additionalIl2CppArgs", "--emit-null-checks --enable-array-bounds-check");



最后请记住,构建开发版本号产生的代码尺寸更大,由于它不曾缩减。这不是问题,毕竟终于交给用户的会是公布版。



资源数据



在其他平台上,一个应用能够简单地訪问位于固定存储空间(硬盘。闪存等等)的文件。而在网页平台上这是不可能的,由于出于安全考虑,网页平台无法訪问真正的文件系统。

因此。Unity WebGL 数据(.data文件)一旦被下载,就会永远存储在内存中。

这样做的缺点就是它相对其他平台将须要很多其他的内存(比如5.3中.data文件以lz4压缩的形式存储在内存中)。比如,下图是分析器显示的一个项目生成了约40MB的数据文件(在256MB
Unity堆的设置下):

【Unity】开发WebGL内存概念具体解释和遇到的问题

.data 文件里包括了什么?它是Unity所生成的文件集合,包括下面内容:data.unity3d (全部的场景,它们依赖于Resources目录中的资源和全部内容),unity_default_resources和少量引擎所需的小文件。

为了知晓资源的准确总大小。您须要在公布至WebGL平台后查看Temp\StagingArea\Data文件夹下的data.unity3d (Temp文件夹将会在Unity编辑器关闭时被删除)。另外,您也能够通过查看UnityLoader.js 中的DataRequest差值得知素材资源的准确大小。



new DataRequest(0, 39065934, 0, 0).open('GET', '/data.unity3d');

(这段代码依据Unity版本号不同,写法可能有些差别——演示样例是Unity 5.4)



内存文件系统

尽管不存在真实的文件系统,正如前文所述,您的Unity WebGL内容仍然能够读写文件。相对于其他平台的主要差别在于。WebGL平台的文件输入/输出操作实际上都是对内存的读/写操作。非常重要一点是,这个内存文件系统并不存在于Unity 堆中。因此,它将须要额外的内存。

比如,以下这个输出数组到文件的演示样例:



var buffer = new byte [10*1014*1024];



File.WriteAllBytes(Application.temporaryCachePath + "/buffer.bytes", buffer);



这个文件将会被写入到内存中,而且在浏览器的分析器也能够查看到。

【Unity】开发WebGL内存概念具体解释和遇到的问题

请注意:Unity堆的大小为256MB。



相同,Unity的缓存系统依赖于文件系统,所以WebGL平台整个缓存存储也是在内存中进行的。这意味着像PlayerPrefs和缓存的Asset Bundles也会被持久化到内存中,而不存在于Unity堆中。



Asset Bundles



降低WebGL平台内存消耗的最佳方法之中的一个是使用Asset Bundles (假设您对Asset Bundles不熟悉。请查阅Unity使用手冊或通过教程学习)。然而,依据使用方式不同。它们将会对内存消耗带来巨大影响(Unity堆中和堆外都会受此影响),这将有可能导致您的内容无法执行在32位浏览器上。



假设真的须要使用Asset Bundle,您会将全部资源打包到一个单独的Asset Bundle吗?



千万别这么做。即使那样可能会降低网页载入期间的压力。您仍然须要下载(极可能无比巨大的)Asset Bundle。从而导致内存使用高峰。来看看下载AB前的内存使用情况。

【Unity】开发WebGL内存概念具体解释和遇到的问题

如您所见,256MB被分配给Unity堆。

下图是没有经过缓存的Asset Bundle下载:

【Unity】开发WebGL内存概念具体解释和遇到的问题

如今看到的是额外的缓存,大约与硬盘中的Asset Bundle(约65mb)大小同样,它是通过XHR分配的。这仅仅是一个暂时缓存。但它将导致连续几帧的内存高峰。直至垃圾收集器启动。



怎样最小化内存高峰?为每一个资源创建一个Asset Bundle?想法不错。但明显不合实际。

其实。对于怎样做可以降低内存高峰并没有普遍的标准,这取决于您项目的实际需求。



最后,在资源使用完成后记得通过AssetBundle.Unload卸载Asset Bundle。



Asset Bundle缓存



Asset Bundle缓存与其他平台一样。您仅仅须要使用WWW.LoadFromCacheOrDownload。它们最大的差别就是内存消耗。

在Unity WebGL中。AB缓存依赖于IndexedDB。IndexedDB是由眼下内存文件系统所支持的emscripten编译器实现。



下图使用LoadFromCacheOrDownload下载Asset Bundle的内存使用情况:

【Unity】开发WebGL内存概念具体解释和遇到的问题

如您所见。Unity堆使用了512MB,并额外分配了约4MB的内存。

下图是载入Asset Bundle后的内存情况:

【Unity】开发WebGL内存概念具体解释和遇到的问题

额外须要的内存跳到了约167mb。这是该Asset Bundle所需的额外内存(压缩包约为64mb)。下图是js虚拟机垃圾收集器启动后的内存情况:

【Unity】开发WebGL内存概念具体解释和遇到的问题

能够看到如今有了一些改善。但仍需约85mb的内存,当中大部分内存用于将Asset Bundle缓存到内存文件系统。这些内存即使卸载了Asset Bundle也不会回收。另一点非常重要。当玩家第二次在浏览器中执行游戏时。这些内存会被马上载入,甚至在载入Asset Bundle之前。

下图是Chrome的内存截图以供參考:

【Unity】开发WebGL内存概念具体解释和遇到的问题

相同,在Unity堆外还有其他缓存相关的暂时内存分配,以供Asset Bundle系统使用。坏消息是近期我们发现它比预想的更大。好消息是它将在未来的Unity 5.5 Beta 4,5.3.6 Patch 6和5.4.1 Patch 2中得以修复。

对于更早的Unity版本号,万一您的Unity WebGL内容已经上线或即将公布。而您又不想升级项目,一个高速的变通方法是通过编辑器脚本的设置下面属性:



PlayerSettings.SetPropertyString("emscriptenArgs", " -s MEMFS_APPEND_TO_TYPED_ARRAYS=1", BuildTargetGroup.WebGL);



最小化Asset Bundle缓存内存消耗的长远解决方式是,使用WWW构造器替代LoadFromCacheOrDownload()。或者您使用新的UnityWebRequest API 时。调用UnityWebRequest.GetAssetBundle()不要带有哈希或版本号參数。



其次是在XMLHttpRequest层使用替代的缓存机制。绕过内存文件系统,将下载的文件直接存储到indexedDB中。我们已经开发了这种工具并公布在Asset Store中。您能够免费将它用于您的项目,也能够自己定义以满足特殊需求。



Asset Bundle压缩



Unity 5.3和5.4均支持LZMA和LZ4两种压缩方式。然而,即使使用LZMA(默认)压缩相对于LZ4或未压缩下载的包更小,但它在WebGL平台上还是有些缺点:它会导致明显的执行延迟,而且须要很多其它的内存。因此强烈建议使用LZ4或者未压缩的格式(实际上,Unity 5.5的WebGL平台将不再支持对Asset
Bundle的LZMA压缩)。为了弥补相比LZMA压缩的下载尺寸过大,您可能希望使用gzip/brotli来压缩Asset Bundle,并配置到您的服务端。

查阅Unity使用手冊以获得很多其它关于Asset Bundle压缩的信息



网页音频



音频在Unity WebGL上的实现方式有所不同。这对内存意味着什么?



Unity将会在JavaScript中创建特定的AudioBuffer的对象。以便它们能够通过WebAudio进行播放。

因为WebAudio缓存位于Unity堆外。因此无法通过Unity 分析器进行跟踪分析。您须要使用浏览器专用的工具。来查看音频使用了多少内存。示比例如以下(火狐浏览器, about:memory page):

【Unity】开发WebGL内存概念具体解释和遇到的问题

考虑到那些Audio Buffers保存的是未解压的数据,其可能不适用于大型音频片段资源(比如:背景音乐)。对于那些资源。你可能希望自己编写js插件。以便使用<audio>标签。

这样的方式下音频文件会保持压缩,因此须要的内存更少。



FAQ



问:降低内存使用的最佳实践是什么?

答:概括例如以下:

  • 降低Unity堆的大小
  • 尽可能保持“WebGL Memory Size”足够小
  • 降低代码量
  • 启用Strip Engine Code
  • 禁用异常检測
  • 避免使用第三方插件
  • 降低数据大小
  • 使用Asset Bundles
  • 使用Crunch纹理压缩

问:是否存在可以决定最小WebGL Memory Size的策略?



答:有。最佳策略是使用内存分析器。分析您的内容实际所需的内存大小。然后据此改变WebGL Memory Size。



以空项目为例。内存分析器告诉我们总的使用量仅为16mb(这个值可能在不同Unity版本号上有所不同):这意味着仅仅须设置WebGL Memory Size大于16MB就可以。当然,内存的总使用量将会根据您的内容而有所不同。



然而,假设由于某些原因无法使用分析器,能够简单地通过不断地降低WebGL Memory Size 值,直到发现您的内容真正所须要的最小内存使用量为止。



另外很值得注意的是。不论什么不是16的倍数的值都将被自己主动的四舍五入(在执行时)为下一个16的倍数,这是Emscripten编译器所要求的。



WebGL Memory Size(MB)设置将决定生成的html中TOTAL_MEMORY(bytes)的值。

【Unity】开发WebGL内存概念具体解释和遇到的问题

所以。为了在不又一次构建项目的前提下,重复測试内存堆的值,推荐使用更改html的方式。一旦您通过此方式发现适合的值,仅仅需在Unity项目设置中更改WebGL Memory Size就可以。

最后,记住Unity的分析器将占用一些来自Unity堆的内存,所以在使用分析器时可能须要添加WebGL内存大小。



问:执行时发生内存溢出。怎样修复?



答:这取决于是Unity,还是浏览器的内存溢出。

这个错误信息将会指出问题所在以及解决的方法:“假设您是该内容开发人员,请在WebGL设置中为您的应用分配很多其它(或更少)的内存。”此时您能够据此调整WebGL内存大小设置。

然而还有非常多能够解决内存溢出的方法。

假设出现下面错误信息:

【Unity】开发WebGL内存概念具体解释和遇到的问题

除了消息内容,您还能够尝试降低代码和数据的大小。

这是由于当浏览器载入网页时。它将试图为一些内容寻找空余的内存。当中最重要的是:代码。数据。Unity堆和被编译的asm.js。

它们可能相当大,尤其是数据和Unity堆内存。这对32位浏览器来说可能是问题。

在一些样例中。虽然存在足够多的空余内存,浏览器仍将载入失败。由于内存是碎片化的。这就是为什么有时候您的内容可能在重新启动浏览器之后,能够成功载入的原因。



还有一种情况是。当Unity 内存溢出时提示下面信息:

【Unity】开发WebGL内存概念具体解释和遇到的问题

在这样的情况下,您须要优化您的Unity项目。

问:怎样衡量内存消耗?



答:为了分析内容所使用的浏览器内存,能够使用火狐浏览器的内存工具或Chrome堆快照。但它们不会显示WebAudio内存使用情况,因此还能够获取火狐浏览器的about:memory页面快照,然后通过搜索“webaudio”找到。

假设您须要通过JavaScript分析内存,请尝试使用window.performance.memory(仅仅支持Chrome)。

使用Unity分析器測量Unity堆内存使用。但请注意。您可能须要添加WebGL的内存大小,以便可以使用分析器。



此外,我们一直在致力于开发一个新的工具,以便您能分析公布版本号:构建WebGL版本号。然后訪问http://files.unity3d.com/build-report/就可以使用该工具。

尽管这在Unity5.4下已经可用,但请您注意,这还是正在开发中的功能,而且随时会更改或被删除。但至少如今能够使用它达到測试的目的。



问:WebGL Memory Size的最小值与最大值是多少?



答:16MB是最小的,最大是2032MB,然而我们通常建议保持在512MB下面。



是否可能出于开发目的而须要分配超过2032MB的内存?



这是一个技术上的限制:2048MB(或很多其它)将会超出TypeArray所用的32位有符号整型的最大值。而TypeArray被用于在JavaScript中实现Unity堆。



问:为何Unity 堆大小不可改变?



答:我们一直在考虑使用Emscripten编译器标志ALLOW_MEMO