关于 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 - ...
随机推荐
- NOIP模拟5
期望得分:100+100+100=300 实际得分:72+12+0=84 T1 [CQOI2009]中位数图 令c[i]表示前i个数中,比d大的数与比d小的数的差,那么如果c[l]=c[r],则[l ...
- CF821 E. Okabe and El Psy Kongroo 矩阵快速幂
LINK 题意:给出$n$条平行于x轴的线段,终点$k$坐标$(k <= 10^{18})$,现在可以在线段之间进行移动,但不能超出两条线段的y坐标所夹范围,问到达终点有几种方案. 思路:刚开始 ...
- [洛谷P4609] [FJOI2016]建筑师
洛谷题目链接:[FJOI2016]建筑师 题目描述 小 Z 是一个很有名的建筑师,有一天他接到了一个很奇怪的任务:在数轴上建 \(n\) 个建筑,每个建筑的高度是 \(1\) 到 \(n\) 之间的一 ...
- new Date('2014/04/30') 和 new Date('2014-04-30') 的区别
new Date('2014/04/30') Wed Apr 30 2014 00:00:00 GMT+0800 (中国标准时间) new Date('2014-04-30'); Wed Apr 30 ...
- 【Ural】1519. Formula 1 插头DP
[题目]1519. Formula 1 [题意]给定n*m个方格图,有一些障碍格,求非障碍格的哈密顿回路数量.n,m<=12. [算法]插头DP [题解]<基于连通性状态压缩的动态规划问题 ...
- 【BZOJ】2599: [IOI2011]Race 点分治
[题意]给一棵树,每条边有权.求一条简单路径,权值和等于K,且边的数量最小.N <= 200000, K <= 1000000.注意点从0开始编号,无解输出-1. [算法]点分治 [题解] ...
- 使用Burpsuite爆破弱口令教工号
使用Burpsuite爆破弱口令教工号 发表于 2015-11-18 | 分类于 Burpsuite | 1条评论 | 26次阅读 准备 所谓工欲善其事,必先利其器,首先当然是要下 ...
- 千字短文解决工程师们关于SPI的迷糊!
串行外设接口 (SPI) 总线是一个工作在全双工模式下的同步串行数据链路.它可用于在单个主控制器和一个或多个从设备之间交换数据.其简单的实施方案只使用四条支持数据与控制的信号线(图 1): 图1:基本 ...
- python 协程嵌套
import asyncio import time now = lambda: time.time() async def do_some_work(x): print('Waiting: ', x ...
- OAuth认证与授权
什么是OAuth授权? 一.什么是OAuth协议 OAuth(开放授权)是一个开放标准. 允许第三方网站在用户授权的前提下访问在用户在服务商那里存储的各种信息. 而这种授权无需将用户提供用户名和密 ...