文章

浅谈Unity内存管理

这是根据2019年官方分享所做的笔记【[Unity 活动]-浅谈Unity内存管理】

Linux的内存占用指标

image.png

  • RSS :自己使用的内存+调用公共服务使用的全部内存
  • PSS : 自己使用的内存+所有调用公共服务的app平均后的内存
  • USS : 自己使用的内存

内存分类

按照分配方式分

  • Native Memory 本地内存:本地内存,指那些不被托管的内存,需要手动申请与释放
  • Managed Memory 托管内存: 指受GC托管的内存
  • Tips : Unity 中 Editor 模式与 Run Time 模式内存统计与分配是不一样的。

按照管理者分

  • Unity管理
  • 用户管理

Unity无法监测到的内存

  • 用户的Native Memory
  • Lua也是用户自管理的内存

Unity Native Memory 最佳实践

Scene

构建Gameobject时底层会使用C++构建多个Object来存储信息,当检查到Unity Native Memory大量上升时,检查场景中是否存在大量物体

Audio

AudioManagerUnity文档

  • DSP Buffer 声音缓存,当满了才会向CPU发送一次请求,设置过大容易导致声音延迟,过小容易造成多次CPU请求导致消耗增大
  • Force to mono 强制单声道,对于声音不敏感的场合可以大量减少内存使用
  • Format 格式要求,各平台要求不一
  • Compression 压缩方式
  • Code Size 代码所占用的内存,这里模板函数与泛型,同时也会影响打包函数,这里应该深入了解C++的底层编译泛型

Asset Bundle

Type Tree

细致的这里挖个坑,等我后续学习完AssetBundle后回来补充

Unity 的每一种类型都有很多数据结构的改变,为了对此做兼容,Unity 会在生成数据类型序列化的时候,顺便会生成 TypeTree:当前我这一个版本里用到了哪些变量,对应的数据类型是什么。在反序列化的时候,会根据 TypeTree 来进行反序列化。

  • 如果上一个版本的类型在这个版本中没有,TypeTree 就没有它,因此不会碰到它。
  • 如果要用一个新的类型,但在这个版本中不存在,会用一个默认值来序列化,从而保证了不会在不同的版本序列化中出错,这就是 Type Tree 的作用。

关于关闭Type Tree:当版本与Unity一致时可关闭,可以减小内存

关闭的好处:

  • 内存减少,本身Type Tree就会占用一定内存
  • 包大小减少,type tree 会被序列化到包内
  • build 和 runtime会变快,因为序列化会先type tree序列化再将实际的东西序列化,反序列化也同理。

两种压缩方式

  • Lz4 建议使用,速度更快虽然占用会多30%左右,并且后续有加密支持
  • Lzma

Size&Count

分包大小建议1M-2M,现在可以适当增大。如果每个Asset都打一个包,会导致最终包头比实际数据还大。

Resources 文件夹

现在不推荐使用,建议使用Asset Bundle。

打包时会生成一颗红黑树,在游戏启动时会一直存在内存中,不可卸载。

Texture

  • upload buffer 与上述音频的DSP Buffer基本一样。
  • r/w 可读写性,若开启,当材质载入buffer后,内存中的那份不会被删除,导致显存与内存同时存在两份
  • mesh -r/w -compression 有些版本可能开了反而更占用内存。

Asset

Asset官方建议文档

Unity Managed Memory

即unity 托管内存部分

  • VM :内存池 mono的内存池 VM的内存会返还给操作系统,但是条件苛刻:连续6次GC没有访问到,才会返还。mono runtime基本不可能,L2cpp runtime可能才会有一点。
  • GC 机制: GC不会返还内存给系统

GC评价指标以及分类:

  1. Throughput 回收能力:一次回收可以回收多少内存
  2. Pause Times 暂停时长:回收时对主线程的影响
  3. Fragmentation 碎片化:回收内存后,会对整体回收内存池的贡献有多大
  4. Mutator Overhead 额外消耗:回收本身有 overhead,要做很多统计、标记的工作
  5. Scalability 可扩展性:多核多线程会有问题吗
  6. Portability 可移植性:其他平台

现在使用的GC是boehm 非分代式GC、非压缩式 GC(26年注:现在也已经淘汰)

  • 分代式:指大内存、小内存、超小内存分布在不同内存区域管理,甚至还有长久内存
  • 压缩式:当一个内存回收后,会重新排布再将各部分使用的内存紧密排布

仍然使用原因:一部分历史原因是与Mono合作的问题,另一原因是目前转向下一代GC:Incremental GC

Incremental GC 解决问题(26年注:现在使用的GC)

  • 进行一次 GC,主线程被迫要停下来,遍历所有 GC Memory elements,来决定哪些可以被 GC 回收。
  • Incremental GC 把暂停主线程的事分帧做了。一点一点分析,主线程不会有峰值。总体 GC 时间不变,但会改善 GC 对主线程的卡顿影响。

Zombie Memory 僵尸内存

由于内存过量碎片化,导致加载一些大内存时无法放入,这并不是内存泄露,内存泄露指的是一块内存无法被管理或访问。

几个优化建议:

  • Don't Null it, but Destroy it. 销毁而不是赋值为空
  • Class vs Struct Class是引用类型而Struct是值类型
  • Pool In Pool 建议对高频使用小部件自建对象池,比如UI 粒子系统 子弹等等
  • Closures and anonymous metheds 闭包和匿名函数
  • Coroutines 协程,这是闭包和匿名函数的一个特例 这里挖个坑后续多研究
  • Configurations 配置表 Singleton 单例 慎用,看里面放的变量,这些从始至终都会一直占用内存

定量调试工具:UPR

后续学习后补充......

许可协议:  CC BY 4.0