unity内存优化总结



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

  • 误区一
    我的项目进出场景前后内存回落不一致,比如进入场景后,内存增加40MB,出来后下降30MB,仍有10MB内存没有返回给系统,即说明内存存在泄露情况。
  • 误区二
    我的项目在进出场景前后,Unity Profiler中内存回落正常,但Android的PSS数值并没有完全回落(出场景后的PSS值高于进场景前的PSS值),即说明内存存在泄露情况。
  • 推荐的判断内存是否泄漏的方法如下:
  • 1. 检查资源的使用情况,特别是纹理、网格等资源的使用

  • Unity所使用的Mono版本中存在一个较大的问题,即内存一旦分配,则不会再返回给系统。这就衍生出另外一个问题—— 无效的Mono堆内存

  • 避免一次性堆内存的过大分配。Mono的堆内存也是“按需”逐步进行分配的。但如果一次性开辟过大堆内存,比如New一个较大Container、加载一个过大配置文件等,则势必会造成Mono的堆内存直接冲高,所以研发团队对堆内存的分配需要时刻注意;

  • 避免不必要的堆内存开销。UWA测评报告中将项目运行过程中堆内存分配Top10函数进行罗列,限于篇幅,我们不再此处进行一一赘述,研发团队可以直接查看之前一篇的内存优化相关文章。

  • 三、资源冗余

  • 1. AssetBundle打包机制出现问题

    同一份资源被打入到多份AssetBundle文件中。举个例子,同一张纹理被不同的NPC所使用,同时每个NPC被制作成独立的AssetBundle文件,那么在没有针对纹理进行依赖打包的前提下,就会出现该张纹理出现在不同的NPC AssetBundle文件中。当这些AssetBundle先后被加载到内存后,内存中即会出现纹理资源冗余的情况。对此,我们建议研发团队在发现资源冗余问题后,对相关AssetBundle的制作流程一定要进行检查。

    2. 资源的实例化所致

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

  • 一般情况下,资源属性的改变情况都是固定的,并非随机出现。比如,假设GameObject受到攻击时,其Material属性改变随攻击类型的不同而有三种不同的参数设置。那么,对于这种需求,我们建议你直接制作三种不同的Material,在Runtime情况下通过代码直接替换对应GameObject的Material,而非改变其Material的属性。这样,你会发现,成百上千的instance Material在内存中消失了,取而代之的,则是这三个不同的Material资源。

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

  • 资源内存占用

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

建议开发团队尽可能根据硬件的种类选择硬件支持的纹理格式,比如Android平台的ETC、iOS平台的PVRTC、Windows PC上的DXT

色阶问题
由于ETC、PVRTC等格式均为有损压缩,因此,当纹理色差范围跨度较大时,均不可避免地造成不同程度的“阶梯”状的色阶问题。因此,很多研发团队使用RGBA32/ARGB32格式来实现更好的效果。但是,这种做法将造成很大的内存占用。比如,同样一张1024x1024的纹理,如果不开启Mipmap,并且为PVRTC格式,则其内存占用为512KB,而如果转换为RGBA32位,则很可能占用达到4MB。所以,研发团队在使用RGBA32或ARGB32格式的纹理时,一定要慎重考虑,更为明智的选择是尽量减少纹理的色差范围,使其尽可能使用硬件支持的压缩格式进行储存。

  • ETC1 不支持透明通道问题
    在Android平台上,对于使用OpenGL ES 2.0的设备,其纹理格式仅能支持ETC1格式,该格式有个较为严重的问题,即不支持Alpha透明通道,使得透明贴图无法直接通过ETC1格式来进行储存。对此,我们建议研发团队将透明贴图尽可能分拆成两张,即一张RGB24位纹理记录原始纹理的颜色部分和一张Alpha8纹理记录原始纹理的透明通道部分。然后,将这两张贴图分别转化为ETC1格式的纹理,并通过特定的Shader来进行渲染,从而来达到支持透明贴图的效果。该种方法不仅可以极大程度上逼近RGBA透明贴图的渲染效果,同时还可以降低纹理的内存占用,是我们非常推荐的使用方式。
  • 目前已经有越来越多的设备支持了OpenGL ES 3.0,这样Android平台上你可以进一步使用ETC2甚至ASTC,这些纹理格式均为支持透明通道且压缩比更为理想的纹理格式。如果你的游戏适合人群为中高端设备用户,那么不妨直接使用这两种格式来作为纹理的主要存储格式。
  • Mipmap会将纹理内存提升1.33倍。对于具有较大纵深感的3D游戏来说,3D场景模型和角色我们一般是建议开启Mipmap功能的
  • 纹理资源的“Read & Write”功能在Unity引擎中是默认关闭的 开启该选项将会使纹理内存增大一倍
  • 网格资源在较为复杂的游戏中,往往占据较高的内存。
  • Mesh资源的数据中经常会含有大量的Color数据、Normal数据和Tangent数据。这些数据的存在将大幅度增加Mesh资源的文件体积和内存占用。其中,Color数据和Normal数据主要为3DMax、Maya等建模软件导出时设置所生成,而Tangent一般为导入引擎时生成。
  • 如果项目对Mesh进行Draw Call Batching操作的话,那么将很有可能进一步增大总体内存的占用。比如,100个Mesh进行拼合,其中99个Mesh均没有Color、Tangent等属性,剩下一个则包含有Color、Normal和Tangent属性,那么Mesh拼合后,CombinedMesh中将为每个Mesh来添加上此三个顶点属性,进而造成很大的内存开销。


    引擎模块自身占用


    真正占据较大内存开销的是这两处: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的内存可能会很大,这是研发团队需要时刻关注的。

    对于SerializedFile,则是当你使用LoadFromCacheOrDownload、CreateFromFile或new WWW本地AssetBundle文件时产生的序列化文件。



    对于WebStream和SerializedFile,你需要关注以下两点:

    • 是否存在AssetBundle没有被清理干净的情况。开发团队可以通过Unity Profiler直接查看其使用具体的使用情况,并确定Take Sample时AssetBundle的存在是否合理;
    • 对于占用WebStream较大的AssetBundle文件(如UI Atlas相关的AssetBundle文件等),建议使用LoadFromCacheOrDownLoad或CreateFromFile来进行替换,即将解压后的AssetBundle数据存储于本地Cache中进行使用。这种做法非常适合于内存特别吃紧的项目,即通过本地的磁盘空间来换取内存空间。

    托管堆内存占用

    目前Unity所使用的Mono版本存在一个很严重的问题,即:Mono的堆内存一旦分配,就不会返还给系统。这意味着Mono的堆内存是只升不降的。举个例子,项目运行时,在场景A中开辟了60MB的托管堆内存,而到下一场景B时,只需要使用20MB的托管堆内存,那么Mono中将会存在40MB空闲的堆内存,且不会返还给系统。




    我们发现用户不必要的堆内存分配主要来自于以下几个方面:

    • 高频率地 New Class/Container/Array等。研发团队切记不要在Update、FixUpdate或较高调用频率的函数中开辟堆内存,这会对你的项目内存和性能均造成非常大的伤害。做个简单的计算,假设你的项目中某一函数每一帧只分配100B的堆内存,帧率是1秒30帧,那么1秒钟游戏的堆内存分配则是3KB,1分钟的堆内存分配就是180KB,10分钟后就已经分配了1.8MB。如果你有10个这样的函数,那么10分钟后,堆内存的分配就是18MB,这期间,它可能会造成Mono的堆内存峰值升高,同时又可能引起了多次GC的调用。在我们的测评项目中,一个函数在10分钟内分配上百MB的情况比比皆是,有时候甚至会分配上GB的堆内存。
    • Log输出。我们发现在大量的项目中,仍然存在大量Log输出的情况。建议研发团队对自身Log的输出进行严格的控制,仅保留关键Log,以避免不必要的堆内存分配
    • 代码堆内存分配的注意点还有很多,比如String连接、部分引擎API(GetComponent)的使用等等


















  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值