Unity 内存泄漏学习总结

内存泄漏的定义及其危害

1、定义:内存泄漏指的是由于疏忽或错误造成程序未能释放已经不再使用的内存。
内存泄漏并非指内存在物理上的消失,而是应用程序分配某段内存后,由于设计错误,导致在释放该段内存之前就失去了对该段内存的控制,从而造成内存的浪费。

2、危害:在操作系统中,为了避免无内存可分配的清空,通常会将不归还内存的应用程序kill掉。由此可以看出,内存泄漏的危害性和严重性,如果持续泄漏,将因内存占用过大而导致应用崩溃。当然泄漏还有其他的危害,例如内存被无用对象占用,导致接下来的内存分配需要更高的时间成本,从而造成游戏的卡顿等等。

Unity中的内存泄漏

对内存泄漏有了初步的认知之后,再来了解一下在Unity环境中的内存泄漏。
一般来说,游戏程序是由代码和资源两个部分组成,Unity下的内存泄漏也主要分为代码侧的泄漏资源侧的泄漏。当然,资源侧的泄漏也是因为在代码中对资源的不合理引用引起的。

一、代码侧的泄漏 —— Mono内存泄漏

使用Unity进行开发,我们就应该了解,Unity是使用基于Mono的C#作为脚本语言(也存在其他脚本语言,但在此不进行讨论),它是基于Garbage Collection(以下简称GC)机制的内存托管语言。
GC的解释

原理

垃圾回收器有两个基本的原理:
1、考虑某个物体在未来的程序执行中,将不会被访问。
2、回收这些物体所占用的存储器。

收集器实现

下面是wikipedia(维基百科)关于GC实现上的介绍:
GC的实现

但是,内存托管本身并非是万能的,GC能做的是通过一定的算法找到“垃圾”,并且自动将“垃圾”占用的内存回收。想要理清这方面的关系,我们就要先了解什么是垃圾。

Q:什么是垃圾呢?
A:在结合上文中对GC的原理和实现机制介绍,我们大致可以推断出,对于GC来讲,没有引用的物体(对象),就是“垃圾”。由于没有引用了,就意味着对于其他任何对象而言,目标对象都已经对其失去了利用价值,那么它就成为了“垃圾”(与实际生活相同,我们把对我们没有利用价值的东西,成为垃圾)。那么根据GC的机制,其占用的内存就会被回收。

了解了GC中垃圾的意义,我们就可以得知,在计算机的角度来说,在某对象超出了其作用域时,我们忘记清除对该无用对象的引用,这种情况就是在托管内存的环境下,出现的内存泄露。这种泄露可能在单独一次看来会非常小,但是实际却对内存有着“千里之堤毁于蚁穴”的深远影响。因为在实际代码中,并非只有显示调用的new才会分配内存,还存在许多隐式的分配,例如产生一个List数组、缓存服务器下发的数据、生成一个字符串等等,这些操作都会产生内存的分配。

在对GC进行了一定的延伸之后,我们再说回Unity本身,在Unity环境下,Mono堆内存的占用,是只会增加不会减少的。具体来说,可以将Mono堆,理解为一个内存池,每次Mono内存的申请,都会在池内进行分配;释放的时候,也是归还给池,而不会归还给操作系统。如果某次分配,发现池内内存不够了,则会对池进行扩建——向操作系统申请更多的内存扩大池以满足该次的内存分配。需要注意的是,每次对池的扩建,都是一次较大的内存分配,每次扩建,都会将池扩大6-10M左右。所以,Mono内存泄漏是Unity游戏开发中需要特别重视的部分。

二、资源侧的泄漏 —— Native内存泄漏

资源泄漏:是指将资源加载之后占有了内存,但是在资源不使用之后,没有将资源卸载导致内存的占用。

在讨论资源内存泄漏的原因之前,我们先来看一下Unity的资源管理与回收方式。为什么要将资源内存和代码内存分开讨论,也是因为其内存管理方式存在不同的原因。

上文中说的代码分配的内存,是通过Mono虚拟机,分配在Mono堆内存上的,其内存占用量一般较小,主要目的是程序猿在处理程序逻辑时使用;而Unity的资源,是通过Unity的C++层,分配在Native堆内存上的那部分内存。举个简单的例子,通过UnityEngine命名空间中的接口分配的内存,将会通过Unity分配在Native堆通过System命名空间中的接口分配的内存,将会通过Mono Runtime分配在Mono堆

Mono内存是通过GC来回收的,而Unity也提供了一种类似的方式来回收内存。不同的是,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的角度,是很难正确地解开引用的。如何查找导致泄漏的引用,是修复泄漏的难点和重点。

内存泄漏常见的情况及其避免方法

1、static关键字
静态实例所持有的静态/非静态成员变量都需要及时置空,或者直接置空该静态实例
非静态实例所持有的静态成员变量需要及时置空
instance不一定是置空操作,指向别的实例也可以,即instance = new InstanceClass()

2、远程下载Texture
从远程下载一张贴图Texture,并赋值给Image _img,这也是新手常会忽略的会导致内存溢出的情况。彻底的unload内存的流程应该是这样的:
(1)以下三选一都能使Image与Texture之间的引用断开,可根据业务需求选择

  • Destroy_img所在gameObject 即 Destroy(_img.gameObject)
  • Destroy_img组件即Destroy(_img)
  • 将_img.sprite置空,即_img.sprite = null

(2)将引用到Image的变量置空,即_img = null
(3)当然如果对Texture缓存或者对由Texture转换所得的Sprite进行缓存了,还需对这些缓存引用置空,即 _tempSprite = null 或 _texDict.Clear() 或_texDict = null
(4)以上步骤任意顺序执行完后再调用Resources.UnloadunusedAsset()即可将Texture从内存彻底卸载

3、Resources&Instantiate Gameobject
调用Resources.Load得到original之后,再实例化(Instantiate)得到instance。
(1)先说unload original的内存,调用Resources.UnloadunusedAsset()就能将original从内存彻底unload,当然如果有变量引用这original,在调用Resources.UnloadunusedAsset()前,需对这些引用置空。

所以如果original不能复用的情况下,建议将original定义为局部变量,使其作用域在Start()内,这种写法更简洁。在unload original的内存时,直接调用Resources.UnloadUnusedAssets()即可。

这一点就比较有意思了,无需Destroy instance即可 unload original,这说明 Instantiate后得到的instance 对original不存在"引用依赖"关系。

(2)以上是unload original的内存 ,接下来是unload instance的。
这一部分内存,只需要Destroy(instance.gameObject),对引用到instance的变量置空。最后在调用Resources.UnloadUnusedAssets()即可将unload instance。

4、场景中原来存在的Gameobject与Resources Instantiate 得到的Gameobject
这两者的内存unload还是还一些差别的。

(1)前者举个实例来说:场景中原来存在一个Cube,Destroy(cube)后调用Resources.UnloadUnusedAssets()虽能unload Cube的内存,但却不能unload Cube所引用到的资源,所以还需要对Cube所引用资源置空,即对material置空,这样才能从彻底unload Cube。
(2)后者要卸载内存则少了对所引用到的资源置空的步骤。

5、Resources.UnloadAsset
Texture2D可以通过该API卸载,但是TextAsset并不能。

修复内存泄漏的工具

下面推荐一些工具可以对内存泄漏问题进行排查修复:

1、Memory Profiler和New Memory Profiler For Unity5

首先,Unity在内存分析工具方面给与了自己的支持。Unity Bitbucket 提供的开源内存可视化工具可实现 Unity 中的内存问题的最佳诊断。
具体链接:
Memory Profiler
https://docs.unity3d.com/Manual/ProfilerMemory.html
New Memory Profiler For Unity5
https://docs.unity.cn/cn/2019.4/Manual/BestPracticeUnderstandingPerformanceInUnity2.html

2、Mono内存的放大镜——Cube

Cube是 腾讯游戏下的腾讯WeTest平台上针对Unity项目的性能指标收集工具,通过Cube可以较方便地获取到游戏的各项性能指标,为性能优化提供了方向。同时Cube也是游戏性能一个很好的衡量工具。

四、避免内存泄漏的建议

1、 在架构上,多添加析构的abstract接口,提醒团队成员,要注意清理自己产生的“垃圾”。
2、 严格控制static的使用,非必要的地方禁止使用static。
3、 强化生命周期的概念,无论是代码对象还是资源,都有它存在的生命周期,在生命周期结束后就要被释放。如果可能,需要在功能设计文档中对生命周期加以描述。

文章总结归纳自:
1、http://t.csdn.cn/bv0Fo
2、http://t.csdn.cn/NZnxE

猜你喜欢

转载自blog.csdn.net/ProSWhite/article/details/132489284
今日推荐