最新文章
泰课在线 | 微信拼团成功后如何获取课程?
08-09 17:57
Unity教程 | 使用ARKit为iOS开发AR应用
07-31 17:23
Unity Pro专业版7折订阅四选一工具包之VR开发与艺术设计
07-28 11:47
网友使用虚幻UE4实现CAVE 多通道立体渲染的沉浸式环境
07-27 11:57
VR晕动症调查:未来5年内大部分VR晕动症将得到解决
07-27 11:26
AMD CEO:未来3-5年最重要 希望5年达1亿VR用户
07-27 10:44
Unity游戏Mono内存管理与泄漏
WeTest导读
内存是游戏的关键因素,若内存管理不善,游戏极有可能出现卡顿、闪退等影响用户体验的问题。本文将介绍腾讯游戏在Unity游戏开发过程中常见的Mono内存管理问题,并阐述一系列解决策略和方法。
什么是Mono内存
对于目前绝大多数基于Unity引擎开发的项目,其托管堆内存由Mono分配和管理。“托管”意味着Mono能够自动调整堆的大小以适应所需内存,并适时调用垃圾回收(Garbage Collection,简称GC)操作来释放不再需要的内存,从而降低开发人员在代码内存管理方面的难度。
Unity游戏运行时的内存占用情况可以用相应图示表示。
目前,绝大部分Unity游戏逻辑代码使用的语言是C#,C#代码所占用的内存被称为Mono内存。这是因为Unity通过Mono跨平台解析并运行C#代码,在Android系统上,游戏的lib目录下存在的libmono.so文件,就是Mono在Android系统上的实现。C#代码经Mono解析执行,所需内存自然由Mono分配管理。下面我们将介绍Mono的内存管理策略以及内存泄漏分析。
Mono内存管理策略
Mono通过垃圾回收机制对内存进行管理。Mono内存分为两部分:已用内存(used)和堆内存(heap)。已用内存指Mono实际使用的内存,堆内存指Mono向操作系统申请的内存,两者的差值即为Mono的空闲内存。
当Mono需要分配内存时,会先检查空闲内存是否足够。若足够,则直接在空闲内存中分配;否则,Mono会进行一次GC以释放更多空闲内存。若GC后仍无足够的空闲内存,Mono会向操作系统申请内存并扩充堆内存,具体流程如下图所示。
GC的主要作用是从已用内存中找出不再需要使用的内存并释放。Mono中的GC主要有以下几个步骤:
- 停止所有需要Mono内存分配的线程:确保在进行内存标记和释放操作时,不会有新的内存分配请求干扰。
- 遍历所有已用内存:以全局数据区和当前寄存器中的对象为根节点,按照引用关系进行遍历,找到那些不再需要使用的内存,并进行标记。
- 释放被标记的内存到空闲内存:将标记为不再使用的内存释放,以便后续的内存分配使用。
- 重新开始被停止的线程:恢复之前因GC而停止的线程,使游戏继续正常运行。
除了空闲内存不足时Mono会自动调用GC外,也可以在代码中调用GC.Collect()手动进行GC。但需要注意的是,GC本身是比较耗时的操作,而且由于GC会暂停那些需要Mono内存分配的线程(C#代码创建的线程和主线程),因此无论是否在主线程中调用,GC都会导致游戏一定程度的卡顿,需要谨慎处理。另外,GC释放的内存只会留给Mono使用,并不会交还给操作系统,因此Mono堆内存是只增不减的。
Mono内存泄漏分析
Mono通过引用关系来判断已用内存中哪些是不再需要使用的。Mono会跟踪每次内存分配的动作,并维护一个分配对象表。当进行GC时,以全局数据区和当前寄存器中的对象为根节点,按照引用关系进行遍历,对于遍历到的每一个对象,将其标记为活的(alive)。
假设A是处于全局数据区的一个对象,那么在GC时将作为根节点进行遍历。由于B、C、D对象都可以由A遍历到,因此被标记为活的,而E、F对象则没有被标记。需要注意的是,由于引用关系是单向的,A引用了B并不代表B也引用了A,所以遍历也只能单向进行。
由于GC以全局数据区和当前寄存器中的对象为根节点进行遍历,所以对象被标记意味着该对象可以通过全局对象或者当前上下文访问到,而没有被标记的对象则意味着该对象无法通过任何途径访问到,即该对象“失联”了,GC最终会将所有“失联”的对象内存进行回收。
虽然Mono有完善的GC机制,但仍然可能存在内存泄漏。我们将对象已经不再需要使用却没有被GC回收的情况称为Mono内存泄漏。Mono内存泄漏会使空闲内存减少,GC频繁,Mono堆不断扩充,最终导致游戏内存占用升高。
解决办法
对于Mono内存泄漏问题,一般只能通过猜测并不断修改代码测试的方法来修复,效率较低。腾讯Wetest平台的Cube工具提供了Mono内存快照对比的功能,并包含对象分配堆栈、对象引用关系等详细信息,是定位Mono内存泄漏问题的有力工具。下面结合具体代码,尝试使用Cube定位Mono内存泄漏问题。
首先,我们定义类A,并在A的构造函数中申请了一块int[1000]大小的内存。
接着,我们定义A类型的静态变量objectA,在游戏界面上绘制一个按钮,并在按钮点击事件中给objectA赋值,此时新生成了new int[1000]对象,并由objectA引用。
使用Cube的Mono内存检测功能,在按钮按下之前和按下之后分别进行一次快照,对比两次快照,查看快照间新增对象。可以看到,按钮按下前后新增的最大对象即为代码中生成的new int[1000]对象,并且该对象被引用的次数为1。为了查看详细的引用关系,下载快照文件snapshot2,其中有这样两行数据:
第一行说明在OnGUI函数中生成了一个A类型的对象,其指针为1533098928;第二行说明在OnGUI()->A:.cotr()中生成了一个Int32[]类型的对象,并且该对象被指针为1533098928的对象引用。即new int[1000]对象被objectA引用,这也是导致new int[1000]对象无法被GC回收的原因。而objectA本身是一个静态对象,是GC的根节点,因此没有对象引用。
如果需要让生成的new int[1000]对象被回收,只需将objectA.a设置为null,断绝其引用关系,该对象自然会在GC时被回收。需要说明的是,“Cube”在获取内存快照时会首先进行一次GC,以防止由于没有及时调用GC导致的误判。
游戏中大部分Mono内存泄漏的情况是由静态对象的引用引起的,因此对于静态对象的使用需要特别注意,尽量少用静态对象,对于不再需要的对象,将其引用设置为null,使其可以被GC及时回收。但由于游戏代码复杂,对象间引用关系嵌套,实际操作难度较大。可以先使用Cube工具进行分析,根据Mono内存趋势找出泄漏的具体场景,然后再使用快照对比功能进行详细分析。
腾讯游戏品质管理团队专门打造的工具“Cube”目前已可使用,它可以帮助开发者发现Unity手游内分类资源的占用情况,尤其关注Unity游戏场景中的FPS、CPU、PSS的变化趋势,有助于在Unity游戏开发过程中不断改善玩家的体验。目前该功能免费开放。
关注“泰斗社区”了解更多相关资讯。