在对内存泄漏有一个基本印象以后,咱们再来看一下在特定环境——Unity下的内存泄漏。你们都知道,游戏程序由代码和资源两部分组成,Unity下的内存泄漏也主要分为代码侧的泄漏和资源侧的泄漏,固然,资源侧的泄漏也是由于在代码中对资源的不合理引用引发的。html
代码中的泄漏 – Mono内存泄漏算法
熟悉Unity的猿类们应该都知道,Unity是使用基于Mono的C#(固然还有其余脚本语言,不过使用的人彷佛不多,在此不作讨论)做为脚本语言,它是基于Garbage Collection(如下简称GC)机制的内存托管语言。那么既然是内存托管了,为何还会存在内存泄漏呢?由于GC自己并非万能的,GC能作的是经过必定的算法找到“垃圾”,而且自动将“垃圾”占用的内存回收。那么什么是垃圾呢?
咱们先来看一下wikipedia上对于GC实现的简介: 缓存
定义仍是过于冗长,咱们来联想一下生活中,咱们通常把没有利用价值的东西,称为垃圾,也就是没有用的东西,就是垃圾。在GC的世界中,也是同样的,没有引用的东西,就是“垃圾”。由于没有引用了,就意味着对于其余任何对象而言,都认为目标对象对我已经没有利用价值了,那它就是“垃圾”了。根据GC的机制,其占用的内存就会被回收。
基于以上的知识,咱们很容易就能够想到为何在托管内存的环境下,仍是会出现内存泄漏了。这就像现实生活中的宅男宅女,吃了泡面老是忘记把盒子扔到门外的垃圾箱里;从计算机的角度来讲,则是,在某对象超出其做用域时,咱们 “忘记”清除对该无用对象的引用了。
说到这,有的同窗可能会有疑问:我每次在代码中申请的内存都很是小,少则几B,多则几十K,如今设备的内存都比较大(几百M仍是有的吧),即便泄漏会产生什么大影响么?
首先,水滴石穿的典故相信你们都知道,实际代码中,并不是只有显示调用new才会分配内存,不少隐式的分配是不容易被发现的,例如产生一个List来存储数据,缓存了服务器下发的一份配置,产生一个字符串等等,这些操做都会产生内存的分配。你分配几十K,他分配几十K,一下子内存就没了。
其次,有一点须要说明的是,在Unity环境下,Mono堆内存的占用,是只会增长不会减小的。具体来讲,能够将Mono堆,理解为一个内存池,每次Mono内存的申请,都会在池内进行分配;释放的时候,也是归还给池,而不会归还给操做系统。若是某次分配,发现池内内存不够了,则会对池进行扩建——向操做系统申请更多的内存扩大池以知足该次的内存分配。须要注意的是,每次对池的扩建,都是一次较大的内存分配,每次扩建,都会将池扩大6-10M左右(此处无官方数据,是观察所得)。性能优化
上图是某游戏通过Cube测试的结果,能够看到Mono堆内存为39M左右,而建议值通常为 50M。
咱们必须知道,Mono内存泄漏是Unity游戏开发中须要特别重视的部分。服务器
资源中的泄漏 – Native内存泄漏微信
资源泄漏,顾名思义,是指将资源加载以后占有了内存,可是在资源不用以后,没有将资源卸载致使内存的无谓占用。
一样的,在讨论资源内存泄漏的缘由以前,咱们先来看一下Unity的资源管理与回收方式。为何要将资源内存和代码内存分开讨论,也是由于其内存管理方式存在不一样的缘由。架构
上文中说的代码分配的内存,是经过Mono虚拟机,分配在Mono堆内存上的,其内存占用量通常较小,主要目的是程序猿在处理程序逻辑时使用;而Unity的资源,是经过Unity的C++层,分配在Native堆内存上的那部份内存。举个简单的例子,经过UnityEngine命名空间中的接口分配的内存,将会经过Unity分配在Native堆;经过System命名空间中的接口分配的内存,将会经过Mono Runtime分配在Mono堆。 编辑器
了解了分配与管理方式的区别,咱们再来看看回收的方式。如上文所说,Mono内存是经过GC来回收的,而Unity也提供了一种相似的方式来回收内存。不一样的是,Unity的内存回收是须要主动触发的。就比如说,咱们把垃圾扔在门口的垃圾桶里,GC是天天来看一次,有垃圾就收走;而Unity则须要你打个电话给它,通知它有垃圾要回收,它才会来。主动调用的接口是Resources.UnloadUnusedAssets()。其实GC也提供了一样的接口GC.Collect()
用来主动触发垃圾回收,这两个接口都须要很大的计算量,咱们不建议在游戏运行时时不时主动调用一番,通常来讲,为了不游戏卡顿,建议在加载环节来处理垃圾回收的操做。有一点须要说明的是,Resources.UnloadUnusedAssets()内部自己就会调用GC.Collect()。Unity还提供了另一个更加暴力的方式——Resources.UnloadAsset()来卸载资源,可是这个接口不管资源是否是“垃圾”,都会直接删除,是一个很危险的接口,建议肯定资源不使用的状况下,再调用该接口。函数
基于上述基础知识,咱们再来看一下为何会有资源的泄漏。首先和代码侧的泄漏同样,因为“存在该释放却没有释放的错误引用”,致使回收机制认为目标对象不是“垃圾”,以致于不能被回收,这也是最多见的一种状况。工具
针对资源,还有一种典型的泄漏状况。因为资源卸载是主动触发的,那么清除对资源引用的时机就显得尤其重要。如今游戏的逻辑趋于复杂化,同时若是有新成员加入项目组,也未必可以清楚地了解全部资源管理的细节,若是“在触发了资源卸载以后,才清除对资源引用”,一样也会出现内存泄漏了。
遇上了资源回收
错过了资源回收
还有一种资源上的泄漏,是由于Unity的一些接口在调用时会产生一份拷贝(例如Renderer.Material参考https://docs.unity3d.com/ScriptReference/Renderer-material.html),若是在使用上不注意的话,运行时会产生较多的资源拷贝,形成内存的无故浪费。可是此类内存拷贝通常量较少,修复起来也比较简单,这里不作大篇幅的介绍。
根据上文描述,咱们知道只要在回收到来以前,将引用解开就能够避免内存泄漏了,彷佛是个很简单的问题。可是因为实际项目的逻辑复杂度每每超出想象,引用关系也不是简单的一层两层(有时候每每会多达十几层,甚至数十层才链接到最终的引用对象),而且可能存在交叉引用、环状引用等复杂状况,单纯从代码review的角度,是很难正确地解开引用的。如何查找致使泄漏的引用,是修复泄漏的难点和重点,也是本文主要想介绍的部分,下面就针对如何查找引用介绍一些思路和方法。至于时序问题,比较简单,在此不作赘述。
New Memory Profiler For Unity5
Unity的Memory Profiler一直就是一个被用户诟病的地方,对于内存的使用量,被谁使用等信息,没有很好的反映。Unity5做为最新一代的Unity产品,对于这个弱点进行了一些补强,推出了新一代的内存分析工具,较好地解决了上述问题。可是没有提供两次(或屡次)内存快照的比较功能,这点比较遗憾。
注:内存快照比较是寻找内存泄漏的经常使用手段,将两次内存的状态截取出来,进行比较,能够清楚地发现内存的变化,寻找内存的增量与泄漏点。通常会在游戏进关前以及出关后作两次dump,其中新增的内存分配,能够视为泄漏。
因为是Unity官方的工具,网上有比较详细的使用教程,在此不加赘述,能够参考下列连接或Google:
Unity-Technologies MemoryProfiler
memoryprofiler intro
因为Unity5普及度及稳定性还有待提高,公司内广泛仍是4.x的环境,那么上述的新工具就不适用了。有的同窗说,升级一个5的工程来作Memory Profile嘛,这个固然也能够,不过Unity5对于4的兼容性不太好,升级过程当中须要修改很多东西,维护两个工程也是比较麻烦的事。
那么,下面就给出两个在Unity4环境下也可使用的泄漏追踪工具。
Mono内存的放大镜——Cube
Cube是 腾讯游戏下的腾讯WeTest平台上针对Unity项目的性能指标收集工具,经过Cube能够较方便地获取到游戏的各项性能指标,为性能优化提供了方向。同时Cube也是游戏性能一个很好的衡量工具。微信号无法直接点开连接,因此点击“阅读原文”能够进到工具页面。(我真的不是在作广告)
这里咱们利用“MONO内存对象深度分析”的特色。该功能能够容许用户抓取某一时刻的Mono内存状态,而且提供不一样时刻内存状态的比较,快速定位到新增的内存分配。
鉴于Cube官方已经给出了详细的使用说明,就再也不赘述数据的抓取过程。这里简单聊一下如何经过Cube抓取的数据更好地追踪和解决问题。
以下图所示,假设咱们已经抓取了两次数据(snapshot1 & snapshot2),而且进行比较,获得两次内存快照之间新增的分配数据。
比较以后获得以下图所示的一系列数据,总结来讲,就是在某个堆栈,分配了某个类型的对象,占用xx内存。这样的数据会有成千上万条(上文所说,代码中的内存分配,是很是细碎,而且数量极多的,在这里获得了验证),而且其中有不少堆栈是重复的,由于每一次的内存分配(即便是同一处位置产生的分配),都会产生一条记录。无序的数据影响了咱们对数据的处理,这里咱们对数据作一些分析整理。
咱们举一些简单的例子来讲明处理的过程。
每一条记录,都是通过一系列的函数调用(堆栈),最终分配了一些内存,用图形化的方式表示为:
让咱们多加一些数据:
经过对图的观察,咱们发现能够把上述离散的图整理成一棵树:
将全部数据都作一样的归类处理以后,能够获得一棵或多棵这样的分配树。这么作的好处是:
1) 根据函数,能够将内存的分配作一个模块的划分,快速定位到相关的模块。
2) 能够清晰地看到每一层函数的分配总量(如A函数总共分配4096+20+4096B),能够根据占用内存的多少决定修复的优先级。
将对比以后的新增项一一清理以后,就能够基本清除Mono内存的多余分配和泄漏了。
顺藤摸瓜——从Mono中寻找资源引用
在尝试寻找资源引用,修复资源泄露以前,咱们须要先了解一下如何在Unity中定位资源泄漏。
咱们须要使用Unity自带的Memory Profiler(注意不是上文说的Unity5的新Profiler,是老的残疾版Profiler)。举个简单的例子,在Unity编辑器环境下运行游戏工程,通过“大厅”页面,进入到“单局”。此时打开Unity Profiler,切换到Memory并作一次内存采样(具体请参考https://docs.unity3d.com/Manual/ProfilerMemory.html,不赘述)。 在采样的结果中(其中包含采样时刻内存中全部的资源),点开Assets->Texture2D,若是其中能够看到有“大厅”UI使用的贴图(以下图),那么咱们能够定义这张UI贴图,属于资源上的泄漏。
为何说这种状况就属于资源泄漏呢,由于这张UI贴图,是在“大厅”时申请的,可是在“单局”时,它已经不被须要了,但是它还在内存中。这种在不须要的时候,却还存在的内存占用,就是上文咱们定义的内存泄漏。
那么在平时项目中,咱们如何找到这些泄漏的资源呢?
最直观的方法,固然也是最笨的方法,就是在每次游戏状态切换的时候,作一次内存采样,而且将内存中的资源一一点开查看,判断它是不是当前游戏状态真正须要的。这种方法最大的问题,就是耗时耗力,资源数量太多眼睛容易看花看漏。
这里介绍两种讨巧的方法:
1) 经过资源名来识别。即在美术资源(如贴图、材质)命名的时候,就将其所属的游戏状态放在文件名中,如某贴图叫作BG.png,在大厅中使用,则修改成OG_BG.png(OG = OutGame)。这样在一坨IG(IG=InGame)资源里面,混入了一个OG,能够很容易地识别出来,也方便利用程序来识别。这么作还有一个好处,能够强化美术对资源生命周期的认识,在制做资源,特别是规划UI图集时,能够有一个指导意义。
2) 经过Unity提供的接口Resources.FindObjectsOfTypeAll()进行资源的Dump,能够根据需求Dump贴图、材质、模型或其余资源类型,只须要将Type做为参数传入便可。Dump成功以后咱们将结果保存成一份文本文件,这样能够用Beyond Compare对屡次Dump以后的结果进行比较,找到新增的资源,那么这些资源就是潜在的泄漏对象,须要重点追查。
结合上述的方法与思路,应该能够轻松找到泄漏的资源了。
此时咱们再回头看一下Unity Profiler,其实Unity提供了资源索引的查找功能,只不过该功能是以一个树形结构的文原本展现的(以下图)。上文曾提到过,Unity内部的引用关系每每是很是复杂的,可能须要经过十几甚至几十层的引用,才能找到最终的引用者,而且引用关系错综复杂,造成一张庞大的图,此时光靠展开树形结构来查找,几乎是不可能的事了。
介绍完对于Unity内存泄漏的追踪方法,我还想往下多讲一步,只要咱们在平时开发的过程多作思考,防微杜渐,内存泄漏是彻底能够避免的。相对于等泄漏发生了再回头来追查,平时多花点时间清理“垃圾”反而是更加高效的作法。
落地到平时的开发流程中,在这里提出几点建议,欢迎各位大牛补充:
1) 在架构上,多添加析构的abstract接口,提醒团队成员,要注意清理本身产生的“垃圾”。
2) 严格控制static的使用,非必要的地方禁止使用static。
3) 强化生命周期的概念,不管是代码对象仍是资源,都有它存在的生命周期,在生命周期结束后就要被释放。若是可能,须要在功能设计文档中对生命周期加以描述。
相信你们出门旅游,都有看过下图相似的标语,做为一名合格的程序猿,也应该可以处理好代码中的“垃圾”,不要让咱们的游戏成为一个“垃圾场”。
为了不以上手游性能方面对游戏的负面影响,腾讯WeTest平台下的Cube工具能够帮助开发者发现游戏内分类资源的一个占用状况,帮助在游戏开发过程当中不断改善玩家的体验。目前功能还在免费开放中。点击http://wetest.qq.com/cube/当即体验!