关于 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 对象 AStartCoroutine 其他对象 B 的函数,那么这时 B 对象会被 A 对象引用,如果协程为正确执行结束在无线等待,那么 B 也会永远被 A 对象引用而不得释放,即使 StopAllCoroutine 也不行。
  • 慎用 Delegate,这时大家经常使用的功能,但是也容易造成内存泄露,如果只是 += 而没有在合适的地方 -=,那么代理的方法所属的对象是会被一直引用而无法释放的。

总结起来很简单,这次优化就大致发现这几条,但是都不经意间,日积月累,引起了比较严重的问题,所以不要小看代码规范,和框架的遵守。

下面是项目优化前后的内存走势,总时长相同。

  • 优化前:

  • 优化后:

以上可以看出,经过一轮粗犷优化后内存依然保持小幅的总体增长,但是比优化前的总体大小已经大幅降低,并且涨幅也更加平缓。接下来可能会需要更大的精力和更多的手段去查找剩下的“一点点”内存泄露。

关于 Unity 项目中的 Mono 堆内存泄露的更多相关文章

  1. javascript中的栈堆内存

    <--------栈内存---------> 俗称叫做作用域(全局作用域/私有作用域) >为js代码提供的执行环境(执行js代码的地方) >基本数据内省是直接存放在栈内存中的 ...

  2. 关于Unity项目中创建项目遇到的一些问题

    1.Unity调用Android的方法默认不是在UI线程执行,所以在Android上写一些页面的重绘的方法,让Unity去调用时,注意要在Android中添加对应的runOnUiThread才可以: ...

  3. Android中使用Handler造成内存泄露的分析和解决

    什么是内存泄露?Java使用有向图机制,通过GC自动检查内存中的对象(什么时候检查由虚拟机决定),如果GC发现一个或一组对象为不可到达状态,则将该对象从内存中回收.也就是说,一个对象不被任何引用所指向 ...

  4. Android中使用Handler造成内存泄露

    1.什么是内存泄露? Java使用有向图机制,通过GC自动检查内存中的对象(什么时候检查由虚拟机决定),如果GC发现一个或一组对象为不可到达状态,则将该对象从内存中回收.也就是说,一个对象不被任何引用 ...

  5. Android 中 Handler 引起的内存泄露

    在Android常用编程中,Handler在进行异步操作并处理返回结果时经常被使用.其实这可能导致内存泄露,代码中哪里可能导致内存泄露,又是如何导致内存泄露的呢?那我们就慢慢分析一下.http://w ...

  6. Android中Handler引起的内存泄露

    在Android常用编程中,Handler在进行异步操作并处理返回结果时经常被使用.通常我们的代码会这样实现. 1 2 3 4 5 6 7 8 9 public class SampleActivit ...

  7. 【转载】java项目中经常碰到的内存溢出问题: java.lang.OutOfMemoryError: PermGen space, 堆内存和非堆内存,写的很好,理解很方便

    Tomcat Xms Xmx PermSize MaxPermSize 区别 及 java.lang.OutOfMemoryError: PermGen space 解决 解决方案 在 catalin ...

  8. Unity项目中的资源管理

    这是我在2017金山技术开放日分享的部分内容.从贴图资源格式配置的介绍开始,引申出资源配置工具,最后再谈谈一整套项目资源管理方案.在GitHub上可以获取到资源配置工具的代码,是基于下面理念的一份简单 ...

  9. Unity 项目中委托Delegate用法案例

    Unity中Delegate的用法场景 本文提供全流程,中文翻译. Chinar 坚持将简单的生活方式,带给世人!(拥有更好的阅读体验 -- 高分辨率用户请根据需求调整网页缩放比例) Chinar - ...

随机推荐

  1. Ping命令网络监测

    按照由近到远原则: 1. ping 127.0.0.1 先检查TCP/IP协议栈是否正常. 2. ping 本地ip 检查网卡是否工作正常. 3. ping 网关地址 检查和网关连接性. 4. pin ...

  2. ASP .NET登录界面用户验证码代码

    //ASP .NET用户登录界面经常用到验证码代码如下 private void Page_Load(object sender, System.EventArgs e) { // 在此处放置用户代码 ...

  3. 【CodeForces】914 E. Palindromes in a Tree 点分治

    [题目]E. Palindromes in a Tree [题意]给定一棵树,每个点都有一个a~t的字符,一条路径回文定义为路径上的字符存在一个排列构成回文串,求经过每个点的回文路径数.n<=2 ...

  4. 【CodeForces】626 F. Group Projects 动态规划

    [题目]F. Group Projects [题意]给定k和n个数字ai,要求分成若干集合使得每个集合内部极差的总和不超过k的方案数.n<=200,m<=1000,1<=ai< ...

  5. 【洛谷 P4437】 [HNOI/AHOI2018]排列(贪心,堆)

    题目链接 如果\(j<=k,a_{p[j]}!=p[k]\)可以理解为如果\(a_{p[j]}=p[k]\),那么\(k\)一定要放在\(j\)前面,也就是\(a_j\)在\(j\)前面. 于是 ...

  6. 【leetcode 简单】第十四题 最后一个单词的长度

    给定一个仅包含大小写字母和空格 ' ' 的字符串,返回其最后一个单词的长度. 如果不存在最后一个单词,请返回 0 . 说明:一个单词是指由字母组成,但不包含任何空格的字符串. 示例: 输入: &quo ...

  7. 微信小程序迁移到头条小程序工具

    最近公司需要将微信小程序迁移到头条小程序,比较得知微信和头条小程序的写法类似,只有文件名称不同,相关的指令不同,以及头条在ttml绑定的数据不可使用function,于是就写了node脚本来实现这些重 ...

  8. docker-It's possible that too few managers are online. Make sure more than half of the managers are online.

    问题:docker ---- It's possible that too few managers are online. Make sure more than half of the manag ...

  9. 一文轻松搞懂redis集群原理及搭建与使用

    今天早上由于zookeeper和redis集群不在同一虚拟机导致出了点很小错误(人为),所以这里总结一下redis集群的搭建以便日后所需同时也希望能对你有所帮助. 笔主这里使用的是Centos7.如果 ...

  10. http 之cookie和session

     cookie和session 关于http: 1.http是:无状态.短连接 2.http的请求生命周期:给服务端发送一个请起头,通过域名提取url,通过路由关系匹配,再通过函数+html进行模板加 ...