Unity Web 中的内存限制可能会限制您运行的内容的复杂性。
Web 内容在浏览器中运行。浏览器在其内存空间中分配应用程序运行内容所需的内存。可用内存量取决于
注意:有关与 Web 内存相关的安全风险的信息,请参阅安全和内存资源。
以下 Unity Web 内容区域需要浏览器分配大量内存。
Unity 使用内存堆来存储所有 Unity 引擎运行时对象。这些包括托管和原生对象、加载的资源、场景场景包含游戏环境和菜单。可以将每个唯一的场景文件视为一个唯一的关卡。在每个场景中,您放置环境、障碍物和装饰,基本上是分段设计和构建游戏。 更多信息
参见 词汇表 和 着色器在 GPU 上运行的程序。 更多信息
参见 词汇表。这就像 Unity 播放器在任何其他平台上使用的内存。
Unity 堆是分配内存的连续块。Unity 支持自动调整堆大小以适应应用程序的需要。堆大小在应用程序运行时扩展,并且可以扩展到 2GB。Unity 将此内存堆创建为Memory 对象。Memory 对象的 buffer 属性是一个可调整大小的 ArrayBuffer,它保存 WebAssembly 代码访问的原始字节内存。
如果浏览器无法在地址空间中分配连续的内存块,则堆的自动调整大小可能会导致应用程序崩溃。因此,务必使 Unity 堆大小尽可能小。因此,在规划应用程序的内存使用时要谨慎。如果要测试 Unity 堆的大小,可以使用探查器一个帮助您优化游戏的窗口。它显示在游戏的各个区域花费了多少时间。例如,它可以报告渲染、动画或游戏逻辑中花费的时间百分比。 更多信息
参见 词汇表来分析内存块的内容。
您可以使用Web 播放器设置中的内存增长模式选项来控制堆的初始大小和增长。默认选项配置为适用于所有桌面用例。但是,对于移动浏览器,您需要使用高级调整选项。对于移动浏览器,建议将初始内存大小配置为应用程序的典型堆使用情况。
创建 Unity Web 构建时,Unity 会生成一个.data
文件。其中包含应用程序启动所需的所有场景和资源。由于 Unity Web 无法访问真实的的文件系统,因此它创建了一个虚拟内存文件系统,浏览器在此处解压缩.data
文件。Emscripten 框架(JavaScript)在浏览器内存空间中分配此内存文件系统。在内容运行期间,浏览器内存会保留未压缩的数据。为了降低下载时间和内存使用量,请尝试使此未压缩的数据尽可能小。
为了减少内存使用,您可以将资源数据打包到资源包中。资源包提供对资源下载的完全控制。您可以控制应用程序何时下载资源以及运行时何时卸载资源。您可以卸载未使用的资源以释放内存。
资源包
直接下载到 Unity 堆中,因此不会导致浏览器额外分配。
启用数据缓存以自动将内容中的资源数据缓存在用户的计算机上。这意味着您无需在以后的运行中重新下载这些数据。Unity Web 加载程序使用 IndexedDB API 实现数据缓存。此选项允许您缓存浏览器无法本地缓存的文件。
数据缓存使浏览器能够将应用程序数据存储在用户的计算机上。浏览器通常会限制您可以在其缓存中存储的数量以及可以缓存的最大文件大小。这通常不足以使应用程序顺利运行。Unity Web 加载程序使用IndexedDB
API 进行缓存,使 Unity 可以将数据存储在 IndexedDB 中而不是浏览器缓存中。
要启用数据缓存选项,请转到文件>构建设置>播放器设置>发布设置。
垃圾回收是查找和释放未用内存的过程。有关 Unity 垃圾回收工作原理的概述,请参阅自动内存管理。要调试垃圾回收过程,请使用 Unity 探查器。
由于 WebAssembly 的安全限制,不允许用户程序检查本机执行堆栈以防止可能的漏洞利用。
这意味着在 Web 平台上,GC 只能在没有托管代码执行(这可能会引用活动 C# 对象)时运行。这发生在每个渲染的游戏帧的末尾。
换句话说,在 Web 平台上,垃圾回收器不能在执行 C# 代码的中间运行,而只能在每个程序帧的末尾运行。这种差异导致垃圾回收行为在 Web 上与其他平台相比存在一些差异。
由于这些差异,请密切注意每帧执行大量临时分配的代码,尤其是在这些分配可能表现出一系列线性大小增长的情况下。此类分配可能会导致垃圾回收器产生暂时的二次内存增长压力。
例如,如果您有一个长时间运行的循环,以下代码在 Web 上运行时可能会失败,因为垃圾回收器不会在 for 循环的迭代之间运行。在这种情况下,垃圾回收器无法释放中间字符串对象使用的内存,并且会在 Unity 堆中耗尽内存。
string hugeString = "";
for (int i = 0; i < 100000; i++)
{
hugeString += "foo";
}
在上面的示例中,循环结束时hugeString
的长度为 3 * 100000 = 300000 个字符。但是,代码在生成最终字符串之前会生成十万个临时字符串。整个循环中分配的总内存为 3 * (1 + 2 + 3 + … + 100000) = 3 * (100000 * 100001 / 2) = 15 GB。
在原生平台上,垃圾回收器会在循环执行时持续清理字符串的先前临时副本。因此,上述代码不需要总共 15 GB 的 RAM 才能运行。
在 Web 平台上,垃圾回收器直到帧结束才会回收临时字符串副本。因此,上述代码在尝试分配 15 GB 的 RAM 时耗尽了内存。
以下代码显示了此类临时二次内存压力可能发生的第二个示例
byte[] data;
for (int i = 0; i < 100000; i++)
{
data = new byte[i];
// do something temporary with data[]
}
此处,代码临时分配了 1 + 2 + 3 + … + 100000 字节 = 5 GB 的字节,即使仅保留了最后一个 100 KB 数组。这会导致程序在 Web 平台上看似耗尽内存,即使最终输出只需要 100 KB。
为了限制这些类型的问题,请避免执行二次递增数量的临时内存分配的代码结构。相反,要么预先分配最终所需的数据大小,要么使用List<T>
或类似的数据结构,这些数据结构执行几何递增的容量预留以减轻临时内存压力。
例如,对于List<T>
容器,如果您知道数据结构的最终大小,请考虑使用List<T>.ReserveCapacity()
函数来预先分配所需的容量。同样,当缩减先前保存了几兆字节内存的容器大小时,请考虑使用List<T>.TrimExcess()
函数。
注意:当您使用 C# 委托或事件(如Delegate
、Action
、Func
)时,这些类在内部使用与上面类似的线性增长分配。避免使用这些类进行过多的每帧委托注册和注销,以最大程度地减少 Web 平台上垃圾回收器的临时内存压力。