UWA

每天一点UWA:第二周

Posted by 蔡华的博客 on August 7, 2017

内存

概述

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

资源内存

  • 大头在纹理,其次在网格、动画片段和音频。

纹理

从格式解决内存占用问题
  • 选择正确的格式,比如Android上是ETC,ios的PVRTC,PC上DXT。
不同格式可能出现的问题:
  • 因为ETC、PVRTC都是有损压缩因此可能出现色阶问题,如果用RGB32这样的格式虽然能解决问题但是内存占用太大。好的办法是在做纹理的时候减少色差范围,必要做出高对比度的阶梯式的图。

    比如,同样一张1024x1024的纹理,如果不开启Mipmap,并且为PVRTC格式,则其内存占用为512KB,而如果转换为RGBA32位,则很可能占用达到4MB。

  • OpenGL ES2的设备只支持ETC1,但是ETC1不支持alpha通道。解决办法是将透明图分成两张,一个RGB24的保存RGB通道,一个alpha8的保存A通道,然后在使用时使用定制的shader去分别读取两个纹理图。
  • PS:OpenGL ES 3.0支持ETC2甚至ASTC,都是很好的支持透明通道的压缩格式。
选择合适的纹理设置
  • 能512解决的事情别用1024
  • 选择性的使用mipmap,对于UI这样的纹理使用完全没有必要用mipmap。
  • Read & Write选项会使得纹理的内存使用量增加一倍。

网格

  • mesh中顶点信息可以进行相关的优化,比如Normal、Color和Tangent这些数据要按照需要来做,不用的就不要做。而且顶点信息超多900还不能动态批处理。不需要就计算发现的时候就可以不用tangent数据了,有贴图的话color数据也不需要。
  • Color数据和Normal数据主要为3DMax、Maya等建模软件导出时设置所生成,而Tangent一般为导入引擎时生成。
  • 如果项目对Mesh进行Draw Call Batching操作的话,那么将很有可能进一步增大总体内存的占用。比如,100个Mesh进行拼合,其中99个Mesh均没有Color、Tangent等属性,剩下一个则包含有Color、Normal和Tangent属性,那么Mesh拼合后,CombinedMesh中将为每个Mesh来添加上此三个顶点属性,进而造成很大的内存开销。

引擎模块自身占用内存

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

  • 一般情况下,上面所指出的引擎各组成部分的内存开销均比较小,真正占据较大内存开销的是这两处:WebStream 和 SerializedFile。
  • PS:5.4以后没有webStream的概念了,不过还是要考虑WWW和LoadFromMemory中会保存AssetBundle原始数据的问题,参见Asset Bundle Compression

  • AssetBundle所占的内存也需要考虑,尽可能做到按需加载,用完后及时的清理。

托管堆内存占用/无效的Mono堆内存开销

  • Mono的堆内存一旦分配,就不会返还给系统。这意味着Mono的堆内存是只升不降的。
不必要的堆内存分配主要来自于以下几个方面:
  • 高频率地 New Class/Container/Array等。不要再update占用的函数中实例化对象。
  • Log输出,需要适当的减少log,只保留最关键的。
  • UIPanel.LateUpdate。这是NGUI中CPU和堆内存开销最大的函数,是由UI网格的重建造成。
一些推荐的办法
  • 不要一次加载一个过大的资源,比如配置文件、纹理图等。这样会造成一次性申请过多的内存,但是还不回去了。

内存泄漏

  • 首先通过工具发现场景切换开始和结束时内存使用没有一致,这个现象不能说明内存就一定有泄漏。比如资源加载后常驻内存以备后续使用、Mono堆内存的只升不降等等,这些均可造成内存无法完全回落。
检查资源的使用情况,特别是纹理、网格等资源的使用
  • 这段主要介绍了如何使用UWA的工具对纹理和网格进行检查,查看是否出现资源的内存泄漏。
通过Profiler来检测WebStream或SerializedFile的使用情况
  • 直接通过Profiler Memory中的Take Sample来对其进行检测,通过直接查看WebStream或SerializedFile中的AssetBundle名称,即可判断是否存在“泄露”情况。

资源冗余

  • 是指在某一时刻内存中存在两份甚至多份同样的资源。导致这种情况的出现主要有两种原因:
AssetBundle打包机制出现问题
  • 显而易见,对于公用资源需要进行合理的划分打包。
资源的实例化所致

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

  • 上面描述的material的问题是个常见的问题。如果是直接改变还好说,会产生一个material instance,但是如果是在一段时间内线性的改变某个属性,那么后果很难说。

  • 建议

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

粒子

  • Navmesh是不支持动态加载的目前的,办法是将多个场景做成prefab,然后用LoadLevelAdditive的方式加载,去拼接。

AssetBundle

  • 本周第一篇关于AssetBundle的文章可以说有点陈旧了,在5.3以后解决了一些功能,比如AssetBundle.LoadFromFile加载LZMA文件的失败的问题。
  • 还有就是在5.4以后已经没有webstream的概念了。
  • 最后开始使用UnityWebRequest

热更新打包时LightmapSnapshot.asset无法导出,导致场景丢失lightmap

  • LightmapSnapshot.asset是editor模式下的无法打包。解决办法是整个scene打包,lightmap信息会打包进去。或者在运行时调用Lightmapsettings.Lightmaps来设置,但是5.x后lightmap信息不会保存在prefab中,因此需要重设Prefab的Lightmap信息(Lightmapindex和Lightmapscaleoffset)。
  • 还有一种可能是因为打包场景的时候shader会根据当前场景的使用情况来打包,如果打包是在一个空场景中那么bundle中的shader会失去lightmap和fog的支持。这个
  • PS: 这个问题在5.5.2中也存在,而且打包的时候是按照场景打包的,但是是不是shader丢失确实需要测试。

prefab打包

  • 如果有一个Prefab,它的Dependencies都在Resources文件夹中,那么,当我在AssetBundle打包时,只打包这个Prefab(不指定BuildAssetBundleOptions.CompleteAssets和BuildAssetBundleOptionsCollectDependencies)是不能正确实例化的,因为AssetBundle中的资源和Resource文件夹下资源是不会建立依赖关系的(脚本除外,因为开启BuildAssetBundleOptionsCollectDependencies 时,脚本依然不会打包到AssetBundle中)。所以会出现Mesh、Material等的丢失。

卸载依赖AssetBundle的问题

  • 比如prefabA和prefabB依赖于AtlasC,那么分别打包的话首先肯定是要先加载AtlasC的AssetBundle的。
  • 但是如果先从AtlasC的AssetBundle中load了AtlasC,然后unload这个AssetBundle,此后加载或实例化A和B时,引擎将无法自动将C绑定给A和B进行使用。这个需要注意。

纹理格式

  • 目前来讲,并不存在一个所有GPU平台都支持硬件解压的压缩格式。
  • ETC1 和 PVRTC 分别是Android和iOS上我们最推荐的格式。 但对于透明纹理,ETC1不支持,而 PVRTC 则可能有较大失真,因此更推荐使用 RGBA 16。
  • 一般来说建议直接使用 Unity 默认的压缩格式(即选择 Compressed 即可,不需要做特殊设置),Unity 会做如下处理:
    • Android 上不带Alpha通道的图片采用 ETC1,带Alpha通道的图片采用True Color中的RGB16,TrueColor中的 RGBA16 会>比 RGBA32 更节省空间,但图像的显示质量会差一些;
    • iOS 上使用 PVRTC,但PVRTC格式要求纹理的长宽相等,且都是2的幂次(即POT,在ImportSettings中可以将NPOT的纹理自动转换成POT)。
  • 同时,我们不建议直接使用 RGBA32 格式的纹理,因为它占用了很大的内存。一般建议使用 RGBA16 和 ETC 格式的纹理来进行加载。 如果转换到 RGBA16 格式时出现了类似“色阶”的颜色问题,则建议尽可能避免大量的过渡色使用。

工具

  • unity的分析工具中的Reserved Total 和 Used Total为Unity引擎在内存方面的总体分配量和总体使用量。 一般来说,引擎在分配内存时并不是向操作系统 “即拿即用”,而是首先获取一定量的连续内存,然后供自己内部使用,待空余内存不够时,引擎才会向系统再次申请一定量的连续内存进行使用。
  • Overhead:
    • Overhead的计算方法是:Profiler当前帧统计的总耗时时间减去所有已知选项的累加时间,即引擎当前还无法识别模块的耗时时间。
    • Overhead数值理论上是趋向于0的,但是由于目前市面上的硬件设备、系统框架过于丰富,所以引擎可能无法识别所有模块的CPU开销。
  • Unity Profiler反馈的则是引擎的真实物理使用内存,有时内存使用数值比原生的分析工具小是正常的。

  • Profiler中ManagedHeap.UsedSize是项目逻辑代码在运行时申请的堆内存, ManagedHeap.UsedSize过大,一方面可能会影响一次GC的耗时;另一方面也可能反映出脚本中不合理的GC Alloc。该选项只能通过优化代码来进行降低。 优化方法一般如下:
    • 尽可能地复用变量,减少new的次数;
    • 使用StringBuilder代替String连接,使用for代替foreach;
    • 对于局部变量或非常驻变量,尽可能使用Struct来代替Class。
  • 本周读到的所有关于内存监控的部分都提到了一个概念就是Profiler所监控到的数据和Android上的PSS或者ios上检测到的会不一致,这 个是因为Unity Profiler反馈的是引擎的真实分配的物理内存,而PSS中记录的则包括系统的部分缓存。一般情况下,Android或iOS并不会及时将所有App卸载数据进行清理,为了保证下次使用时的流畅性,OS会将部分数据放入到缓存,待自身内存不足时,OS Kernel会启动类似LowMemoryKiller的机制来查询缓存甚至杀死一些进程来释放内存。因此,并不能通过一两次的PSS内存没有完全回落来说明内存泄露问题。

  • Reserved GFX 中的内存,主要是纹理和网格资源。

渲染

Lightmap在PC上显示正常,但是转到Android平台上存在色差,颜色普遍偏暗的问题

  • Unity烘焙的Lightmap是32bit的HDR图,而移动设备通常不支持HDR图(32bit per channel),会按照LDR图(8bit per channel)的形式进行处理,因此会出现色偏问题。因此需要如下的处理:
    • 在移动平台下使用Mobile/Diffuse材质,可载入Standard Assets(Mobile) package获得。
    • 如果要获得更合适的效果,需要自行修改Lightmap的DecodeLightmap函数,该函数可在Unity\Editor\Data\CGIncludes\UnityCG.cginc文件中找到。需要说明的是,这种方法也不能达到与PC端完全一致的效果。
    • 如果需要PC和移动平台的显示效果一致,可以用图像编辑软体修改Lightmap為LDR格式,例如PNG(8bit per channel)。
    • 为了避免类似问题,请不要使用过于强烈的Light进行烘焙,因為Light的强度(Intensity)越高,色偏问题会越严重。若有阴影丢失时,可以尝试检查一下模型的Lightmapindex、Lightmapscaleoffset、UV2等影响Lightmap采样的一些参数。
  • 另一种可能是存在过曝现象,可以尝试将playersettings -> use direct3d 11关闭,看问题是否解决。

lightmap动态加载

  • Lightmap的动态加载,需要通过脚本将烘焙时每个物件的Lightmapindex和Lightmapscaleoffset记录下,并在运行时动态加载后设置回去的方式来实现。因为目前Lightmapindex和Lightmapscaleoffset信息是和场景绑定在一起,储存在Lightmapsnap.assets 中,发布时也是放在场景信息中,因此不会记录在Prefab 上。