我们已经在Unity使用手冊上有对于WebGL内存管理的详尽分析,甚至在Unite Europe 2015与Unite Boston 2015两届大会上,也有专题对其进行深入的解说。然而,这方面的内容依然是用户讨论的热门话题,因此我们意识到应当分享很多其它。希望本文能回答一些被频繁咨询的问题。
Unity WebGL与其他平台有何不同?
一些用户已经熟悉了部分内存有所限制的的平台。而对于其他如桌面和WebPlayer平台。到眼下为止内存还不是问题。
在内存方面,主机平台相对其他平台较为简单。由于您能够准确的知道内存是怎样使用的。
这同意您能够非常好的管理内存,并保证您的游戏内容完美执行。
在移动平台,内存管理变的有些复杂,由于设备种类繁多。但至少您能够选择最低标准的设备,并依据市场情况忽视那些相较于该标准更为的低端设备。
在网页平台,就没有那么轻松了。理想情况下,全部终端用户都拥有64位浏览器和大量内存。但事实却相距甚远。首先。您无法通过不论什么方法知道,正执行您的内容的硬件规格。其次。除了用户的操作系统和浏览器外,您并不知道其他信息。最后。终端用户可能像执行其他网页一样执行您的WebGL内容。
因此这是一个很复杂的问题。
概览
下图是在浏览器上执行Unity WebGL内容时的内存概览:
![](https://i-blog.csdnimg.cn/blog_migrate/b543f95fca7dce0835fd74ad1f25fe29.png)
正如上图所看到的。存在几组内存分配: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堆的设置下):
![](https://i-blog.csdnimg.cn/blog_migrate/c19627fe3627196fe12d9d735455f4bf.png)
.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);
这个文件将会被写入到内存中,而且在浏览器的分析器也能够查看到。
![](https://i-blog.csdnimg.cn/blog_migrate/87d2333a1d8382205e7113ff7549ab3e.png)
请注意: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前的内存使用情况。
![](https://i-blog.csdnimg.cn/blog_migrate/8ccc099d22c1c6915bd5c7a41639e9f3.png)
如您所见,256MB被分配给Unity堆。
下图是没有经过缓存的Asset Bundle下载:
![](https://i-blog.csdnimg.cn/blog_migrate/607a6e2a6cc7dd4ddc203f21ec03bb05.png)
如今看到的是额外的缓存,大约与硬盘中的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的内存使用情况:
![](https://i-blog.csdnimg.cn/blog_migrate/91c10ee9e0f78bd968b493c037e0ad35.png)
如您所见。Unity堆使用了512MB,并额外分配了约4MB的内存。
下图是载入Asset Bundle后的内存情况:
![](https://i-blog.csdnimg.cn/blog_migrate/b520d86a3adcba710ef7b3e74588de1b.png)
额外须要的内存跳到了约167mb。这是该Asset Bundle所需的额外内存(压缩包约为64mb)。下图是js虚拟机垃圾收集器启动后的内存情况:
![](https://i-blog.csdnimg.cn/blog_migrate/2f5eddc965575ed60072b8419993f2e6.png)
能够看到如今有了一些改善。但仍需约85mb的内存,当中大部分内存用于将Asset Bundle缓存到内存文件系统。这些内存即使卸载了Asset Bundle也不会回收。另一点非常重要。当玩家第二次在浏览器中执行游戏时。这些内存会被马上载入,甚至在载入Asset Bundle之前。
下图是Chrome的内存截图以供參考:
![](https://i-blog.csdnimg.cn/blog_migrate/e8048b00048236ac9404fd3a7edea772.png)
相同,在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):
![](https://i-blog.csdnimg.cn/blog_migrate/554e6d9838c46064b25eab1c59bc0378.png)
考虑到那些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)的值。
![](https://i-blog.csdnimg.cn/blog_migrate/1ec6061e9daef44c282be035b46a11c0.png)
所以。为了在不又一次构建项目的前提下,重复測试内存堆的值,推荐使用更改html的方式。一旦您通过此方式发现适合的值,仅仅需在Unity项目设置中更改WebGL Memory Size就可以。
最后,记住Unity的分析器将占用一些来自Unity堆的内存,所以在使用分析器时可能须要添加WebGL内存大小。
问:执行时发生内存溢出。怎样修复?
答:这取决于是Unity,还是浏览器的内存溢出。
这个错误信息将会指出问题所在以及解决的方法:“假设您是该内容开发人员,请在WebGL设置中为您的应用分配很多其它(或更少)的内存。”此时您能够据此调整WebGL内存大小设置。
然而还有非常多能够解决内存溢出的方法。
假设出现下面错误信息:
![](https://i-blog.csdnimg.cn/blog_migrate/80c8733cbec4fb3a76bc56abf16b246a.png)
除了消息内容,您还能够尝试降低代码和数据的大小。
这是由于当浏览器载入网页时。它将试图为一些内容寻找空余的内存。当中最重要的是:代码。数据。Unity堆和被编译的asm.js。
它们可能相当大,尤其是数据和Unity堆内存。这对32位浏览器来说可能是问题。
在一些样例中。虽然存在足够多的空余内存,浏览器仍将载入失败。由于内存是碎片化的。这就是为什么有时候您的内容可能在重新启动浏览器之后,能够成功载入的原因。
还有一种情况是。当Unity 内存溢出时提示下面信息:
![](https://i-blog.csdnimg.cn/blog_migrate/9377ad23c66997143b0c82fc6abbcd8d.png)
在这样的情况下,您须要优化您的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