unity内存优化

发布时间 2023-12-19 21:43:19作者: mc宇少

原文:

https://community.uwa4d.com/blog/detail?id=1542444347202879489&entrance=0

https://community.uwa4d.com/blog/detail?id=1542444346179469314&entrance=0

1.资源内存占用

在一个复杂的大中型项目中,资源的内存占用往往占据了总体内存的70%以上。因此,资源使用是否恰当直接决定了项目的内存占用情况。一般来说,一款游戏项目的资源主要可分为如下几种:纹理(Texture)、网格(Mesh)、动画片段(AnimationClip)、音频片段(AudioClip)、材质(Material)、着色器(Shader)、字体以及文本(TextAsset)等等。其中纹理、网格、动画片段和音频片段则是最容易造成加大内存开销的资源。

1.纹理

一个6万面片的场景,网格资源最大才不过10MB,但一个2048x2048的纹理,可能直接就达到16MB。可见一个模型纹理的占用要远大于网格的占用。

(1)纹理格式

它不仅影响着纹理的内存占用,同时还决定了纹理的加载效率。一般来说,我们建议开发团队尽可能根据硬件的种类选择硬件支持的纹理格式,比如Android平台的ETCiOS平台的PVRTC、Windows PC上的DXT等等。

由于ETC和PVRTC都是有损压缩格式,当纹理色差范围跨度较大时,均不可避免地造成不同程度“阶梯”状的色阶问题,但不压缩又会导致很大的内存占用问题:一张1024x1024的纹理,如果不开启Mipmap,并且为PVRTC格式,则其内存占用为512KB,而如果转换为RGBA32位,则很可能占用达到4MB。所以更好的选择是尽量减少纹理的色差范围,使用压缩格式。

etc1不支持alpha通道的问题:在Android平台上,对于使用OpenGL ES 2.0的设备,其纹理格式仅能支持ETC1格式,这个时候可以使用alpha通道分离技术(分两张贴图存,用的时候再合在一起)。目前已经有越来越多的设备支持了OpenGL ES 3.0,这样Android平台上你可以进一步使用ETC2甚至ASTC,这些纹理格式均为支持透明通道且压缩比更为理想的纹理格式。如果你的游戏适合人群为中高端设备用户,那么不妨直接使用这两种格式来作为纹理的主要存储格式。

(2)纹理尺寸

能用512x512就不要用1024x1024,后者的内存占用是前者的四倍。

(3)Mipmap

为贴图生成小尺寸版本以应对离得远的情况。但是UI的贴图就没有必要开了,Ui的都显示在最上层,每次都只会用最大尺寸的贴图保证质量,白白增加无谓的内存占用。

(4)read&write

默认关掉,开启会使内存占用增大一倍。

2.网格

(1)Normal、Color和Tangent

这些数据的存在将大幅度增加Mesh资源的文件体积和内存占用。其中,Color数据和Normal数据主要为3DMax、Maya等建模软件导出时设置所生成,而Tangent一般为导入引擎时生成。

更为麻烦的是如果项目对Mesh使用了批处理操作的话,会加大内存占用(100个Mesh拼合,99个没有这三个东西,有一个有,那合并后的CombinedMesh会为每个Mesh添加上这三个

这三个东西是Shader所用,来生成较为酷炫的效果,优化策略是:不需要用到这个三个的Shader去掉相关逻辑,合批的时候考虑下这个因素,尽量避免上面的情况。

2.引擎模块自身占用

引擎自身中存在内存开销的部分纷繁复杂,可以说是由巨量的“微小”内存所累积起来的,比如GameObject及其各种Component(最大量的Component应该算是Transform了)、ParticleSystem、MonoScript以及各种各样的模块Manager(SceneManager、CanvasManager、PersistentManager等)…

一般情况下,上面这些的内存开销还比较小,真正的大头是这两处:WebStream 和 SerializedFile。简单说就是bundle的未正确卸载导致的

其绝大部分的内存分配则是由AssetBundle加载资源所致。简单言之,当您使用new WWW或CreateFromMemory来加载AssetBundle时,Unity引擎会加载原始数据到内存中并对其进行解压,而WebStream的大小则是AssetBundle原始文件大小 + 解压后的数据大小 + DecompressionBuffer(0.5MB)。因此,当项目中存在通过new WWW加载多个AssetBundle文件,且AssetBundle又无法及时释放时,WebStream的内存可能会很大,这是研发团队需要时刻关注的。

3.托管堆内存占用

目前Unity所使用的Mono版本存在一个很严重的问题,即:Mono的堆内存一旦分配,就不会返还给系统。这意味着Mono的堆内存是只升不降的。所以关注那些堆内存分配Top10的函数是十分有必要的。

要避免地情况:

1.研发团队切记不要在Update、FixUpdate或比较高调用频率的函数中开辟堆内存,这会对你的项目内存和性能造成非常大的伤害。

2.对项目中的Log进行控制,有关闭按钮(只保留关键Log)

4.内存泄漏

1.检查资源的使用情况,特别是纹理、网格等资源的使用

资源泄漏是内存泄露的主要表现形式,其具体原因是用户对加载后的资源进行了储存(比如放到Container(容器)中),但在场景切换时并没有将其Remove或Clear,从而无论是引擎本身还是手动调用Resources.UnloadUnusedAssets等相关API均无法对其进行卸载,进而造成了资源泄露。

2.通过Profiler来检测(WebStream或SerializedFile)assetbundle的使用情况

AssetBundle的管理不当也会造成一定的内存泄露,即上一场景中使用的AssetBundle在场景切换时没有被卸载掉,而被带入到了下一场场景中。对于这种情况,建议直接通过Profiler Memory中的Take Sample来对其进行检测,通过直接查看WebStream或SerializedFile中的AssetBundle名称,即可判断是否存在“泄露”情况。

3.通过Android/IOS Instrument反馈的APP线程内存来看

“Unity Profiler中内存回落正常,但Android的PSS数值并没有完全回落”是有可能的,这是因为Unity Profiler反馈的是引擎的真实分配的物理内存,而PSS中记录的则包括系统的部分缓存。

这里建议在两个场景之间疯狂切换,如果出现上述情况,可能是因为:

1.unity引擎自身的内存泄漏问题,概率很小

2.第三方插件在使用时出现了内存泄漏,这种概率较大,这种概率较大,因为Profiler仅能对Unity自身的内存进行监控,而无法检测到第三方库的内存分配情况。因此,在出现上述内存问题时,建议大家先对自身使用的第三方库进行排查。

5.无效的Mono堆内存开销

 一个是申请的内存大小,一个是已使用的内存大小,差值就是无效的内存。

如何避免无效内存过多:
1.避免一次性堆内存的过大分配。Mono的堆内存也是按需逐步进行分配的。但如果一次性开辟过大堆内存,比如New一个较大的Container(容器)、加载一个过大的配置文件等,则势必会造成Mono堆内存直接冲高。

2.避免不必要的堆内存开销。对堆内存分配的Top10函数进行分析。

6,资源冗余

所谓“资源冗余”,是指在某一时刻内存中存在两份甚至多份同样的资源。导致这种情况的出现主要有两种原因:

1.AssetBundle打包机制出现问题

同一份资源被打入到多分AssetBundle中。举个例子:同一张纹理被不同的NPC所使用,同时每个NPC被制作成独立的AssetBundle文件,那么在没有针对纹理进行依赖打包的前提下,就会出现该张纹理出现在不同的NPC AssetBundle文件中。

2.资源重复实例化

更改Mesh或者Material的参数会导致重新实例化一份出来,比如更改Material的变量,会创建一个新的Material,虽然Material的内存占用不大,但是过多的冗余资源却为Resources.UnloadUnusedAssets API的调用效率增加了相当大的压力。这种情况可以使用多个Material的方法来规避。