游戏性能优化——内存管理

项目的性能优化主要围绕CPU、GPU和内存三大方面进行

内存管理

内存的开销无外乎以下三大部分:1.资源内存占用;2.引擎模块自身内存占用;3.托管堆内存占用。

内存占用

资源内存占用

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

一、纹理

(1) 纹理格式

ETC1 不支持透明通道问题

  在Android平台上,对于使用OpenGL ES 2.0的设备,其纹理格式仅能支持ETC1格式,该格式有个较为严重的问题,即不支持Alpha透明通道,使得透明贴图无法直接通过ETC1格式来进行储存。对此,我们建议研发团队将透明贴图尽可能分拆成两张,即一张RGB24位纹理记录原始纹理的颜色部分和一张Alpha8纹理记录原始纹理的透明通道部分。然后,将这两张贴图分别转化为ETC1格式的纹理,并通过特定的Shader来进行渲染,从而来达到支持透明贴图的效果。该种方法不仅可以极大程度上逼近RGBA透明贴图的渲染效果,同时还可以降低纹理的内存占用,是我们非常推荐的使用方式。

  当然,目前已经有越来越多的设备支持了OpenGL ES 3.0,这样Android平台上你可以进一步使用ETC2甚至ASTC,这些纹理格式均为支持透明通道且压缩比更为理想的纹理格式。

(2)纹理尺寸

  一般来说,纹理尺寸越大,则内存占用越大。

(3) Mipmap功能

Mipmap旨在有效降低渲染带宽的压力,提升游戏的渲染效率。但是,开启Mipmap会将纹理内存提升1.33倍。对于具有较大纵深感的3D游戏来说,3D场景模型和角色我们一般是建议开启Mipmap功能的,但是在我们的测评项目中,经常会发现部分UI纹理也开启了Mipmap功能。这其实就没有必要的,绝大多数UI均是渲染在屏幕最上层,开启Mipmap并不会提升渲染效率,反倒会增加无谓的内存占用。

(4) Read & Write

因为开启该选项将会使纹理内存增大一倍

引擎模块自身占用

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

  一般情况下,上面所指出的引擎各组成部分的内存开销均比较小,真正占据较大内存开销的是这两处:WebStream 和 SerializedFile。其绝大部分的内存分配则是由AssetBundle加载资源所致。简单言之,当您使用new WWW或CreateFromMemory来加载AssetBundle时,Unity引擎会加载原始数据到内存中并对其进行解压,而WebStream的大小则是AssetBundle原始文件大小 + 解压后的数据大小 + DecompressionBuffer(0.5MB)。同时,由于Unity 5.3版本之前的AssetBundle文件为LZMA压缩,其压缩比类似于Zip(20%-25%),所以对于一个1MB的原始AssetBundle文件,其加载后WebStream的大小则可能是5~6MB,因此,当项目中存在通过new WWW加载多个AssetBundle文件,且AssetBundle又无法及时释放时,WebStream的内存可能会很大

托管堆内存占用

  对于目前绝大多数基于Unity引擎开发的项目而言,其托管堆内存是由Mono分配和管理的。“托管” 的本意是Mono可以自动地改变堆的大小来适应你所需要的内存,并且适时地调用垃圾回收(Garbage Collection)操作来释放已经不需要的内存,从而降低开发人员在代码内存管理方面的门槛。

  但是这并不意味着研发团队可以在代码中肆无忌惮地开辟托管堆内存,因为目前Unity所使用的Mono版本存在一个很严重的问题,即:Mono的堆内存一旦分配,就不会返还给系统。这意味着Mono的堆内存是只升不降的。举个例子,项目运行时,在场景A中开辟了60MB的托管堆内存,而到下一场景B时,只需要使用20MB的托管堆内存,那么Mono中将会存在40MB空闲的堆内存,且不会返还给系统。这是我们非常不愿意看到的现象,因为对于游戏(特别是移动游戏)来说,内存的占用可谓是寸土寸金的,让Mono毫无必要地锁住大量的内存,是一件非常浪费的事情。所以,我们在UWA测评报告中,为研发团队统计了测试过程中累积的函数堆内存分配量,大家只需要通过查看堆内存分配Top10的函数,即可快速对其底层代码实现进行查看,定位是否有分配不必要堆内存的代码存在。

  • 高频率地 New Class/Container/Array等。研发团队切记不要在Update、FixUpdate或较高调用频率的函数中开辟堆内存,这会对你的项目内存和性能均造成非常大的伤害

  • Log输出。对自身Log的输出进行严格的控制,仅保留关键Log,以避免不必要的堆内存分配。

  • UIPanel.LateUpdate。这是NGUI中CPU和堆内存开销最大的函数。它本身只是一个函数,但NGUI的大量使用使它逐渐成为了一个不可忽视规则。该函数的堆内存分配和自身CPU开销,其根源上是一致的,即是由UI网格的重建造成

内存泄露

一、检查资源的使用情况,特别是纹理、网格等资源的使用在我们进行过的项目深度优化过程中,资源泄漏是内存泄露的主要表现形式,其具体原因是用户对加载后的资源进行了储存(比如放到Container中),但在场景切换时并没有将其Remove或Clear,从而无论是引擎本身还是手动调用Resources.UnloadUnusedAssets等相关API均无法对其进行卸载,进而造成了资源泄露。

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

推荐的测试方式是在两个场景之间来回不停切换,比如主城和战斗副本间。理论上来说,多次切换同样的场景,如果Profiler中显示的Unity内存回落正常,那么其PSS/Instrument的内存数值波动范围也是趋于稳定的

资源冗余

  在内存管理方面,还有一个大家必须关注的话题——资源冗余。

一、AssetBundle打包机制出现问题

  同一份资源被打入到多份AssetBundle文件中。举个例子,同一张纹理被不同的NPC所使用,同时每个NPC被制作成独立的AssetBundle文件,那么在没有针对纹理进行依赖打包的前提下,就会出现该张纹理出现在不同的NPC AssetBundle文件中。当这些AssetBundle先后被加载到内存后,内存中即会出现纹理资源冗余的情况。

二、资源的实例化所致

  在Unity引擎中,当我们修改了一些特定GameObject的资源属性时,引擎会为该GameObject自动实例化一份资源供其使用,比如Material、Mesh等。以Material为例,我们在研发时经常会有这样的做法:在角色被攻击时,改变其Material中的属性来得到特定的受击效果。这种做法则会导致引擎为特定的GameObject重新实例化一个Material,后缀会加上(instance)字样。其本身没有特别大的问题,但是当有改变Material属性需求的GameObject越来越多时(比如ARPG、MMORPG、MOBA等游戏类型),其内存中的冗余数量则会大量增长。