Unity性能优化_内存

Why

  1. 当超出终端设备的可用内存大小时,将导致应用程序的Crash。比如:1G内存的iPhone6,可供程序使用的内存尽量不要超过600M
  2. 过多的内存操作,将导致碎片化和GC的工作更重,可能使应用程序运行帧率不稳定
  3. 过大的内存请求,将导致内存分配效率的降低
  4. 过大的内存使用量,触发更大的显存带宽消耗,将提升功耗,影响用户设备使用

常用策略

  1. 从源头考虑,减少载入内存的资源的大小及复杂度。比如:精简Mesh,更低的纹理复杂度,动画复杂度,音频视频质量等
  2. 减少运行时代码逻辑申请的内存快,比如:使用合适的数据结构
  3. 使用缓存,减少开辟新的内存块,减少碎片

一些方案

  1. Shader Variants
  2. Remapper
  3. AssetBundle Cache
  4. Managed Mem
    1. Allocation/Pool
    2. DataStructor
    3. GC
  5. Native Asset Load

在分析具体的处理方案之前,先回顾以下应用程序的内存机制。

内存机制

说到程序内存性能优化,老方法论了,须要建立起对进程内存机制的系统认识。主要分为六个部分(从高地址开始):命令行操作与环境变量,栈,堆,未初始化数据段,已初始化数据段,代码段。


图源:GeeksforGeeks

其中堆作为内存优化的核心问题,最后介绍,先讲其他部分。

命令行操作与环境变量

略。

代码段

  • 即编译好的代码,一次性被载入的操作指令。
  • 代码段是只读的,以防止程序以外修改指令。
  • 文本段在堆栈的下面,防止堆栈溢出覆盖它。
  • 通常代码段是共享的。

数据段:已初始化

  • 可以理解为编译型静态数据区。
  • 全局的。
  • 已初始化的。
  • 内存在程序编译的时候就已经分配好,这块内存在程序的整个运行期间都存在,例如全局常量(public const)和已初始化全局变量(public static)。

数据段:未初始化

  • 又称为BBS段。
  • 全局的。
  • 可以理解为运行时静态数据区。比如存储全局运行时只读型变量(public static readonly)和未初始化的全局变量(public static)。

空间有限,数据类型简单,生命周期短,存储机制简单(压栈 / 出栈:后进先出FILO(First In Last Out))。

速度慢,频繁使用容易产生碎片,减小命中率。

Unity的堆内存空间分为两部分,一部分是托管堆(Managed Heap),集成了垃圾回收器(GC机制);另一部分是本机堆(Native Heap),内存回收需要显式明确的调用相应接口。

方案

1 Shader Variants

Shader优化相关

2 Remapper

todo…

3 AssetBundle Cache

建议AssetBundle使⽤LZ4压缩⽅式,增加DisableTypeTree的BuildOption。

如果一个项目固定引擎版本,或者升引擎时可以Rebuild所有AB,则建议DisableWriteTypeTree。没有TypeTree,数据量少很多,加载更快。

可以做个简单测试,下图左边是无TypeTree的。

补充1:LZ4采用块压缩方式(chunk-based),块压缩的数据被分为大小相同的块,被分别压缩,读取效率比LZMA高非常多。

补充2:WebGL不支持

Reduce load times with AssetBundles – Unity 手册 (unity3d.com)

WebGL doesn’t support threading. As Http downloads become available only after they’re downloaded, Unity WebGL builds need to decompress AssetBundle data on the main thread after the download is complete, blocking the main thread. To avoid this interruption, LZMA AssetBundle compression isn’t available for AssetBundles on WebGL. AssetBundles are compressed using LZ4 instead, which is de-compressed efficiently on-demand.

4 托管堆

New生成的容器,实例中的各种声明的变量等,有自动GC机制。

一些优化方式

  • List或Dictionary使用Clear(),减少New()
  • 在Update函数里频繁New对象
  • 降低逻辑执行频率
  • 减少装箱拆箱的类型转换操作
  • 使用更简单的数据类型
  • string的本质是字符数组,+操作开销大,可以考虑StringBuilder
  • coroutine的使用注意:yield return 0/null,new WaitForSeconds()
  • 直接把对象作为引用,改为使用对象的索引
  • 在合适的场合使用Struct替代Class
  • for遍历时,提前定义好循环的总次数
  • 缓存/对象池

托管堆的GC机制

触发条件:

  1. 极端情况,如:系统内存耗尽
  2. 主动调用GC函数
  3. 主动申请内存
  4. 被动申请内存

新申请堆内存时,先检测是否有现成可分配的内存块,若没有,再GC,如果GC之后仍没有可分配内存,再向操作系统申请(用户态转向内核态)。

所以尽可能不触发GC机制,目前的GC算法肯定是一种综合策略,其中标记整理算法会由于定义的对象,对象的引用数量过多而消耗太多CPU时间。

GC的步骤:

  1. 检查堆中的每一个对象;
  2. 检索每一个当前对象的引用以判断是否可以删除;
  3. 标记可以删除的对象;
  4. 删除被标记的对象,回收内存。

Incremental GC

一般建议开启,以实际测试为准。

Unity 2019.1新功能: 增量式垃圾回收

5 本机堆

(1) 对于模型,动画,贴图,配置数据,字体等资源合理设计,压缩,R/W设置,非必要的Mipmap等

(2) 降低资源质量是有损优化,一般作为最后的手段

(3) 关于运行时清理

Unity自己的内存管理,模型,贴图,音效等原始资源,主动回收通过Destroy,Resources.UnloadAsset(Obj),Resources.UnloadUnusedAssets和AB.Unload(true/false)实现。


图:Unity AssetBundle的内存管理(PS:现在不要用WWW来下载,通过HttpRequest性能更好)

Destroy
对应上图中Instantiate的那部分Objects。除非缓存和对象池的对象,否则我们一般都会使用这个API进行资源卸载,比如UI。

Resources.UnloadAsset(Obj)
卸载Resources.Load(一般只会是logo和splash一类的资源)和AB.Load的Asset备份

Resources.UnloadUnusedAssets
一般在场景切换的时候调用(Unity会隐式调用),并不能释放AB镜像,AB镜像通过AB.Unload(false)释放。

AB.Unload:主动实现的引用计数机制
因为一些大型的游戏对内存管理的要求比较高,如果不做当前场景内的内存清理,很容易内存占用过大,导致系统性能下降,所以需要使用到API:AB.Unload。

而因为有Unload(true)和Unload(false)两种方式,所以这里就有两种回收粒度的策略。

  1. Unload(true):(慎用!!!)同时清理AB镜像和AB.Load出来的Asset备份,需要对AB和AB.Load出来的Asset备份做引用计数。
  2. Unload(false):只清理AB镜像,只需要对AB做引用计数。

当统计的某个资源对象引用计数为0时,即时处理,如果延时处理复杂度就上去了。

参考