unity WebGL

自加入Unity WebGL平台以来,Unity的开发团队就一直致力于优化WebGL的内存消耗。我们已经在Unity使用手册上有对于WebGL内存管理的详尽分析,甚至在Unite Europe 2015与Unite Boston 2015两届大会上,也有专题对其进行深入的讲解。然而,这方面的内容依旧是用户讨论的热门话题,因此我们意识到应当分享更多。希望本文能回答一些被频繁咨询的问题。

Unity WebGL与其它平台有何不同?
一些用户已经熟悉了部分内存有所限制的的平台。而对于其它如桌面和WebPlayer平台,到目前为止内存还不是问题。

在内存方面,主机平台相对其它平台较为简单,因为您可以准确的知道内存是如何使用的。这允许您可以很好的管理内存,并保证您的游戏内容完美运行。在移动平台,内存管理变的有些复杂,因为设备种类繁多,但至少您可以选择最低标准的设备,并根据市场情况忽视那些相较于该标准更为的低端设备。

在网页平台,就没有那么轻松了。理想情况下,所有终端用户都拥有64位浏览器和大量内存,但事实却相距甚远。首先,您无法通过任何方法知道,正运行您的内容的硬件规格。其次,除了用户的操作系统和浏览器外,您并不知道其它信息。最后,终端用户可能像运行其它网页一样运行您的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,这样您发布的版本将不包含那些不必需的原生引擎代码(例如:如果不需要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堆的设置下):



.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堆的大小为256MB。

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

Asset Bundles

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

如果真的需要使用Asset Bundle,您会将所有资源打包到一个单独的Asset Bundle吗?

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


如您所见,256MB被分配给Unity堆。下图是没有经过缓存的Asset Bundle下载:


现在看到的是额外的缓存,大约与硬盘中的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堆使用了512MB,并额外分配了约4MB的内存。

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


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


可以看到现在有了一些改善,但仍需约85mb的内存,其中大部分内存用于将Asset Bundle缓存到内存文件系统。这些内存即使卸载了Asset Bundle也不会回收。还有一点很重要,当玩家第二次在浏览器中运行游戏时,这些内存会被立即加载,甚至在加载Asset Bundle之前。

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


同样,在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):


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

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)的值。


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

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

问:运行时发生内存溢出,如何修复?

答:这取决于是Unity,还是浏览器的内存溢出。这个错误信息将会指出问题所在以及解决办法:“如果您是该内容开发者,请在WebGL设置中为您的应用分配更多(或更少)的内存。”此时您可以据此调整WebGL内存大小设置。然而还有很多可以解决内存溢出的方法。如果出现以下错误信息:


除了消息内容,您还可以尝试减少代码和数据的大小。这是因为当浏览器加载网页时,它将试图为一些内容寻找空余的内存,其中最重要的是:代码,数据,Unity堆和被编译的asm.js。它们可能相当大,尤其是数据和Unity堆内存,这对32位浏览器来说可能是问题。

在一些例子中,尽管存在足够多的空余内存,浏览器仍将加载失败,因为内存是碎片化的。这就是为什么有时候您的内容可能在重启浏览器之后,可以成功加载的原因。

另一种情况是,当Unity 内存溢出时提示以下信息:


在这种情况下,您需要优化您的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_MEMORY_GROWTH,来允许调整堆大小,但目前还是决定不用该标志,因为它会禁用一些Chrome中的优化。我们还未对这个影响做一些真正的基准测试。我们预计使用该标志可能会导致内存问题更严重。如果您遇到Unity堆过小,以至于无法满足所需内存的情况。这时就需要更多内存,那么浏览器就必须分配一个更大的堆,从旧堆中复制一切,然后再释放旧堆。这样做,它需要同时维持新堆和旧堆两份内存(直到完成复制),从而需要更多的总内存。因此,相比使用预定固定内存的方式内存占用更大。

问:为什么32位浏览器在64位操作系统上会内存溢出?

答:32位浏览器运行时的内存限制是一样的,无论操作系统是64或32位。

结论

最后建议使用浏览器专用的工具,来分析您的Unity WebGL内容,因为Unity分析器无法追踪超出Unity堆之外的内存分配。

你可能感兴趣的:(U3D)