- 用户指南
- 资源商店 (Asset Store)
- 资源服务器 (Asset Server)(仅限团队许可证)
- 缓存服务器(仅限团队许可证)
- 幕后场景
- 创建游戏
- 运行时实例化预设 (Prefabs)
- 变换 (Transforms)
- 物理
- 添加随机的游戏元素
- 粒子系统(Particle Systems)
- Mecanim 动画系统
- 旧动画系统
- 导航网格 (Navmesh) 和寻路 (Pathfinding)(仅限专业版 (Pro))
- Sound (音频侦听器)
- 游戏界面元素
- 多玩家联网游戏
- iOS 开发入门
- Android 开发入门
- Blackberry 10 开发入门
- Metro:入门指南
- 本地客户端开发入门
- FAQ
- Advanced
- Vector Cookbook
- 资源包(仅限专业版)
- Graphics Features
- 资源数据库 (AssetDatabase)
- 构建播放器管道
- 分析器(仅限专业版)
- 光照贴图快速入门
- 遮挡剔除(仅限专业版)
- 相机使用技巧
- 运行时加载资源
- 通过脚本修改源资源
- 用程序生成网格几何体
- 富文本
- 在 Unity 工程 (Project) 中使用 Mono DLL
- 事件函数的执行顺序
- 移动优化实用指南
- Unity XCode 工程结构
- 优化图形性能
- 减少文件大小
- 理解自动内存管理
- 平台依赖编译
- 泛型函数
- 调试
- 插件(专业版/移动版特有功能)
- 文本场景文件格式(仅限专业版)
- 流媒体资源
- 启动时运行编辑器脚本代码
- 网络模拟
- VisualStudio C 集成
- 分析
- 检查更新
- 安装多版本 Unity
- 故障排除
- Unity 中的阴影
- Unity 中的 IME
- 对集成显卡进行优化
- 网络播放器 (Web Player) 部署
- 使用网络播放器中的信任链系统
理解自动内存管理
在创建对象、字符串或数组时,其存储所需的内存将从一个被称为堆的中央池分配。若项目不再使用,它之前占用的内存可以释放回收,用来存储其他数据。过去通常由程序设计员使用相应的函数调用,明确分配并释放这些堆内存块。现在,像 Unity Mono 引擎之类的运行时间系统可以自动管理内存。自动内存管理比明确分配/释放需要的编码工作更少,并大大降低了潜在的内存泄漏(内存分配,但从未被释放的情况)。
值和引用类型
在调用函数时,其参数值将复制到专为此次调用保留的内存区域。数据类型只会占用几个字节,可以快速简单地复制。但在通常情况下,对象、字符串和数组要大得多,如果这些类型的数据定期复制的话将会变得相当低效。幸运的是,我们没必要这样做;大型项目的实际存储空间从堆中分配并且只使用一个小小的“指针”值来记住它的位置。自此,在参数传递期间只需复制指针。只要运行时间系统可以找到指针识别的项目,就可以在必要时经常使用单个数据副本。
直接存储并在参数传递过程中复制的类型被称为值类型。它包括整型、浮点型、布尔型和 Unity 的结构类型(例如,Color 和 Vector3)。被分配在堆然后通过指针访问的类型被称为引用类型,因为存储在变量中的值只是“指向”真实的数据。引用类型包括对象、字符串和数组等。
分配和垃圾收集
内存管理会跟踪堆中已确定未使用的区域。在请求新的内存块时(例如当一个对象被实例化),管理器将选择未使用的区域分配给内存块,然后将分配的内存从已知未使用空间中删除。后续请求都以同样的方式处理,直到有没有足够大的未使用空间来分配所需的块大小。此时,堆中分配的所有内存几乎不可能都在使用中。只有在参考变量仍可找到引用时,才可以访问堆上的引用项目。如果内存块的所有引用都已经消失(例如,引用变量已经重新分配,或者它们成为超出范围的局部变量),那么,它所占用的内存可以安全地重新分配。
若要决定哪些堆块不再被使用,内存管理器将在所有当前活动的引用变量中搜索,并将其引用的块标记为“活动”块。完成搜索之后,活动块之间的所有空间都将被内存管理器视为未使用,可以用于后续分配。显然,定位和释放未使用内存的过程被称为垃圾收集(简称为 GC)。
优化
垃圾收集为自动执行,且程序设计员不可见,但实际上收集过程需要在后台占用大量 CPU 时间。若使用恰当,自动内存管理的整体性能一般会等于或高于手动分配。但是,程序设计员应避免失误引发不必要的回收器触发和执行停顿。
现在也存在一些非主流的算法,虽然第一眼看上去并无恶意,但却能成为的 GC 的噩梦。重复字符串串联便是一个典型的例子:-
function ConcatExample(intArray: int[]) { var line = intArray[0].ToString(); for (i = 1; i < intArray.Length; i++) { line += ", " + intArray[i].ToString(); } return line; }
此处的关键细节是新的块没有一个接一个恰当地添加到字符串。实际情况是,每次循环的时候,之前的 line 变量的内容成为死链 — 每次分配的都是一个由原块和结尾处新块组成的全新的字符串。随着 i 值的增加,字符串也将不断变长,消耗的堆空间量也随之增加,因此,每次调用此函数,都极有可能使用数百个字节的空闲堆空间。如需串联大量字符串,那么最好使用 Mono 库的 System.Text.StringBuilder 类。
但是,如果不频繁调用,字符串联不会引发过多的问题,并且 Unity 通常采用帧更新。如下所示:-
var scoreBoard: GUIText; var score: int; function Update() { var scoreText: String = "Score: " + score.ToString(); scoreBoard.text = scoreText; }
… 每次更新时都将分配新的字符串,并且持续产生新的垃圾。大多数字符串可以通过更新文本保存,除非 score 发生变化:-
var scoreBoard: GUIText; var scoreText: String; var score: int; var oldScore: int; function Update() { if (score != oldScore) { scoreText = "Score: " + score.ToString(); scoreBoard.text = scoreText; oldScore = score; } }
另一个潜在问题发生在函数返回数组值时:-
function RandomList(numElements: int) { var result = new float[numElements]; for (i = 0; i < numElements; i++) { result[i] = Random.value; } return result; }
在创建有填充值的新数组时,此类函数非常美观实用。但是,如果反复调用,每次都将分配新的内存。由于数组可能非常大,空闲的堆空间可能快速消耗完,导致频繁的垃圾收集。避免这个问题的一种方式是利用数组是一种引用类型。作为参数传递到函数的数组可以在函数内修改,在函数返回之后结果将保留。上述函数通常可作如下替换:-
function RandomList(arrayToFill: float[]) { for (i = 0; i < arrayToFill.Length; i++) { arrayToFill[i] = Random.value; } }
这项操作会简单地用新值取代现有的数组内容。虽然这需要在调用的代码中完成数组初始分配(这似乎不够美观),但是此函数在调用时不会产生任何新的垃圾。
请求收集
如上所述,最好尽量避免分配。但考虑到它们无法被完全消除,可以使用两种主要策略将其对游戏设置的入侵降至最低:-
快速、频繁垃圾收集的小堆
此策略最好用于拥有长期游戏设置的游戏,此类游戏考虑的主要问题是平稳的帧速率。这些游戏的主要特点是频繁分配小堆,但是这些堆只会短暂使用。在 iOS 上使用这一策略时,一般堆大小为 200KB,在 iPhone 3G 上,垃圾收集将花费大约 5ms 时间。如果堆增加至 1MB,垃圾收集将花费 7ms。因此,在某些需要以规定帧间隔进行垃圾收集的情况下,这将是非常有用的功能。通常,垃圾收集的频率将高于必要的次数;但它可以快速处理,几乎不会对游戏造成影响。
if (Time.frameCount % 30 == 0) { System.GC.Collect(); }
但请应该谨慎使用该技巧,并检查分析器统计信息,以确保它可以真正减少游戏的垃圾收集时间。
缓慢但很少进行垃圾收集的大堆
此策略适用于很少进行分配(收集也相应减少)的游戏,可以在游戏暂停时处理。如果堆尽可能大,但不会大到系统内存低而导致 OS 无法运行游戏时,它非常有用。如果可能的话,Mono 运行时间会避免自动扩展堆。您可以在启动时预先分配某些占位符空间(例如,对于仅因影响内存管理器的而被分配的“无用”对象,可对其实例化),手动扩展堆:-
function Start() { var tmp = new System.Object[1024]; // make allocations in smaller blocks to avoid them to be treated in a special way, which is designed for large blocks for (var i : int = 0; i < 1024; i++) tmp[i] = new byte[1024]; // release reference tmp = null; }
在游戏设置中可以容纳垃圾收集的的停顿之间,不应完全填满足够大的堆。若发生此类停顿,可以明确地请求垃圾收集:-
System.GC.Collect();
同样,也应该谨慎使用该策略,并注意分析器的统计信息,而是不是假设它已经取得预期效果。
可重复使用的对象池
在很多情况下,可以通过减少创建和销毁的对象数目来避免产生垃圾。您可能反复遇到游戏中的某些对象,比如爆弹,尽管只有少数对象会立即爆炸。这种情况通常可以重复使用对象而无需摧毁旧对象,然后使用新对象替换。
请参阅此处,了解更多有关对象池 (Object Pool) 及其实现的详细信息。
更多信息
内存管理是一个微妙复杂的话题,已经投入了大量的学术精力。如果有兴趣了解更多相关知识,那么 memorymanagement.org 是不错的资源网站,它刊登了大量出版物和在线文章。更多有关对象池的信息,请访问 Wikipedia 页面和 Sourcemaking.com。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论