关于 Unity 项目中的 Mono 堆内存泄露
关于 Unity 项目中的 Mono 堆内存泄露
题记:这是补一篇应该在将近一年前就应该写的记录,今天终于补上。
内存泄露是一个老话题了,之前我专门写过一篇 排查 Lua 虚拟机内存泄露 的文章,并且附带了一个工具来查找 Lua 中具体的内存泄露。但是这只是整个 Unity 项目中内存泄漏的一小部分,C# 代码中一般内存泄露可能会更加严重。
我们之前发现无论在 Profiler 还是工具测试,随着战斗的增加,总体内存都是一直在增长,很明显是有了内存泄露。为了首先能够彻底检测到底是哪里出现了泄露,找了很多的工具,也参看了很多的文章,最终感觉首要的问题是需要完整的 C# 内存的快照明细,Mono 提供了一个过时的 HeapShot,看起来就是做这件事情的,但是离直接拿到项目里去使用还有不少的距离。在最后快要放弃打算自己 Hook Mono 的 API 编写时,发现了腾讯的 WeTest Cube 工具。其中有一项是专门用来打印详细内存快照的功能,根据自己的需要来选取内存快照,并且给出测试期间的整体内存走势,使用也很简单,在需要测试的手机上安装 WeTest Cube 软件(注意:手机必须要 Root),然后通过 Cube 里面选择设置,然后启动游戏即可。
启动后在想要打印内存快照的地方,点击屏幕上的 "快照" 按钮,就会在后台生成快照,多点几份然后再双击该按钮退出测试,然后上传数据完成,最后生成的快照数据需要再从 Cube 的网页报告端下载。
由于游戏项目较大,下载后的快照解压缩后基本都在 40+Mb,内容是 json 格式的文本,存取了每一项内存数据的地址,类型,引用链:
{"ptr":-1721623792,"type":"Texture2D","size":16,"stack":"|UnityEngine.GUIStyleState:ProduceGUIStyleStateFromDeserialization (UnityEngine.GUIStyle,intptr)|UnityEngine.GUIStyle:InternalOnAfterDeserialize ()|UnityEngine.GUIUtility:GetDefaultSkin ()|UnityEngine.GUI:DoSetSkin (UnityEngine.GUISkin)|UnityEngine.GUI:set_skin (UnityEngine.GUISkin)|UnityEngine.GUIUtility:BeginGUI (int,int,int)","reference":"|-1721577968"}
ptr - 内存地址
type - 数据类型
size - 内存大小
stack - 该内存被分配时的调用堆栈
reference - 被引用的地址,如果这里有多个,就是被多个其它的地址引用了
所以,依然在主城中打印一份快照,进入战斗打印一份快照,再回到主城打印一份快照,自己编写一个小工具解析两份主城的快照,并且将第二份新增的部分输出成一个单独的文件;然后再编写一个小工具将新增部分根据数据类型 type 来归类,将同类型的 size 相加,最后生成一个文件,里面两列:类型 和 大小。然后用 Excel 打开并按照 大小 降序排列,便可以直接看到那些新增的内存,根据项目情况分析新增部分从 类型 和 大小 两个角度来讲是否应该出现就立即分析出潜在的泄露,然后把泄露的 类型 拿到原始的快照(第二次主城快照)中去搜索,然后查看该类型到底被什么地方引用,一直追溯下去,便能找到最终的引用点,说明是因为“它”的引用才造成泄露。
接下来的事情就是去项目中实际分析代码,查看创建和释放的地方是否有纰漏,需要比较耐心的去项目代码中查找和分析,其实这都是苦活,脏活儿,要的都是耐心,你需要从成千上万条数据中揪出可能的泄露。
后来做完这次优化,我再次感受到,出现这些泄露的原因归根结底还是代码不够规范引起的,不注重必要的初始化和释放成对调用等,大致总结起来有以下几点:
- 错误使用了过多的
readonly,并且引用了其他的类型,甚至项目引用,最终形成引用环,并且环的某一个节点又被全局对象引用,造成“蝌蚪的尾巴被拴住”的情况,因此连带了很多的对象无法释放。 - 每一个类应该是有
Initialize就有Release,但实际情况大多是有Initialize而没有Release。 - 注意 Coroutine,如果使用了全局的
MonoBehaviour对象A去StartCoroutine其他对象B的函数,那么这时B对象会被A对象引用,如果协程为正确执行结束在无线等待,那么B也会永远被A对象引用而不得释放,即使StopAllCoroutine也不行。 - 慎用
Delegate,这时大家经常使用的功能,但是也容易造成内存泄露,如果只是+=而没有在合适的地方-=,那么代理的方法所属的对象是会被一直引用而无法释放的。
总结起来很简单,这次优化就大致发现这几条,但是都不经意间,日积月累,引起了比较严重的问题,所以不要小看代码规范,和框架的遵守。
下面是项目优化前后的内存走势,总时长相同。
优化前:

优化后:

以上可以看出,经过一轮粗犷优化后内存依然保持小幅的总体增长,但是比优化前的总体大小已经大幅降低,并且涨幅也更加平缓。接下来可能会需要更大的精力和更多的手段去查找剩下的“一点点”内存泄露。
关于 Unity 项目中的 Mono 堆内存泄露的更多相关文章
- javascript中的栈堆内存
<--------栈内存---------> 俗称叫做作用域(全局作用域/私有作用域) >为js代码提供的执行环境(执行js代码的地方) >基本数据内省是直接存放在栈内存中的 ...
- 关于Unity项目中创建项目遇到的一些问题
1.Unity调用Android的方法默认不是在UI线程执行,所以在Android上写一些页面的重绘的方法,让Unity去调用时,注意要在Android中添加对应的runOnUiThread才可以: ...
- Android中使用Handler造成内存泄露的分析和解决
什么是内存泄露?Java使用有向图机制,通过GC自动检查内存中的对象(什么时候检查由虚拟机决定),如果GC发现一个或一组对象为不可到达状态,则将该对象从内存中回收.也就是说,一个对象不被任何引用所指向 ...
- Android中使用Handler造成内存泄露
1.什么是内存泄露? Java使用有向图机制,通过GC自动检查内存中的对象(什么时候检查由虚拟机决定),如果GC发现一个或一组对象为不可到达状态,则将该对象从内存中回收.也就是说,一个对象不被任何引用 ...
- Android 中 Handler 引起的内存泄露
在Android常用编程中,Handler在进行异步操作并处理返回结果时经常被使用.其实这可能导致内存泄露,代码中哪里可能导致内存泄露,又是如何导致内存泄露的呢?那我们就慢慢分析一下.http://w ...
- Android中Handler引起的内存泄露
在Android常用编程中,Handler在进行异步操作并处理返回结果时经常被使用.通常我们的代码会这样实现. 1 2 3 4 5 6 7 8 9 public class SampleActivit ...
- 【转载】java项目中经常碰到的内存溢出问题: java.lang.OutOfMemoryError: PermGen space, 堆内存和非堆内存,写的很好,理解很方便
Tomcat Xms Xmx PermSize MaxPermSize 区别 及 java.lang.OutOfMemoryError: PermGen space 解决 解决方案 在 catalin ...
- Unity项目中的资源管理
这是我在2017金山技术开放日分享的部分内容.从贴图资源格式配置的介绍开始,引申出资源配置工具,最后再谈谈一整套项目资源管理方案.在GitHub上可以获取到资源配置工具的代码,是基于下面理念的一份简单 ...
- Unity 项目中委托Delegate用法案例
Unity中Delegate的用法场景 本文提供全流程,中文翻译. Chinar 坚持将简单的生活方式,带给世人!(拥有更好的阅读体验 -- 高分辨率用户请根据需求调整网页缩放比例) Chinar - ...
随机推荐
- bzoj 3678 wangxz与OJ
3678: wangxz与OJ Time Limit: 10 Sec Memory Limit: 128 MBhttp://www.lydsy.com/JudgeOnline/problem.php ...
- web项目中的 log4net的配置
最近用log4net,网上查了很多资料,照着网上的配置大多都不管用,可能我还是有什么地方配置的不对.看出来的朋友平指出.下面是我自己亲测的,可以用! 1.web项目中的web.config 配置log ...
- 【洛谷 P2604】 [ZJOI2010]网络扩容(最大流,费用流)
题目链接 第一问就是简单的最大流. 第二问,保留第一问求完最大流的残量网络. 然后新建一个源点,向原源点连一条流量为k,费用为0的边. 然后所有边重新连一起(原来的边保留),费用为题目所给,最小费用即 ...
- HDU 1999 不可摸数 (模拟)
题目链接 Problem Description s(n)是正整数n的真因子之和,即小于n且整除n的因子和.例如s(12)=1+2+3+4+6=16.如果任何数m,s(m)都不等于n,则称n为不可摸数 ...
- Django之利用ajax实现图片预览
利用ajax实现图片预览的步骤为: 程序实现的方法为: 方法一: upload.html <!DOCTYPE html> <html lang="en"> ...
- 47、Python面向对象中的继承有什么特点?
继承的优点: 1.建造系统中的类,避免重复操作. 2.新类经常是基于已经存在的类,这样就可以提升代码的复用程度. 继承的特点: 1.在继承中基类的构造(__init__()方法)不会被自动调用,它需要 ...
- c语言学习笔记.指针.
指针: 一个变量,其值为另一个变量的地址,即,内存位置的直接地址. 声明: int *ptr; /* 一个整型的指针,指针指向的类型是整型 */ double *ptr; /* 一个 double 型 ...
- Msql中的触发器
解发器 当执行某种操作时解发的行为. 比如, 当表变动时触发的动作. 像商城订单, 当下单时, 库存减少. 语法: create trigger trigger_name after/befor in ...
- 问题解决:The content of the adapter has changed but ListView did not receive a notification
1. 不要在后台线程中直接调用adapter 2. 不要在后台线程中修改adapter绑定的数据 如果对adapter或者adapter绑定的数据是在线程中,加上runOnUiThread就可以了 r ...
- What I Learned as a Junior Developer Writing Tests for Legacy Code(转载)
I go to the gym and lift weights because I like the feeling of getting stronger and better. Two mont ...