# 什么是内存?

# 物理内存

# CPU 访问内存:

  • CPU 访问内存是一个慢速的过程
  • CPU 想要访问一块内存时,并不是立即走系统总线访问内存,而是先向 L1、L2、L3 Cache 查询。当三个 Cache 全部 Miss 以后,才会去主内存中拿一段连续的指令回到 Cache。当下一次需要的时候,依然是先向 Cache 查询。
  • 这样如果我们的内存、指令不是连续的,就会导致大量内存到 Cache 的交换,浪费大量时间在内存读取上
  • (CPU 上 Cahce 的核心面积一般占用都是很大的)
  • (Unity ECS DOTS 就是为了优化这一点,将数据组建为连续内存块,以期减少 Cache Miss)

# PC 和 移动设备内存架构差异:

  • 移动设备没有独立显卡
  • 移动设备没有独立显存
    • 所以移动设备上我们关注的 内存 ,也即是 显存
  • CPU 核心更小,缓存级数和大小也更小

# 虚拟内存

  • PC:
    交换内存,当我们内存不足时,它会尝试将不使用的内存交换到硬盘上,以节省出更多的物理内存给当前系统比较活跃的进程使用

  • IOS:
    IOS 可以进行压缩,将不活跃的内存压缩起来移动到内存中特定空间,节省出物理内存给活跃的应用使用

  • Android:一般是没有的 (虽然有部分手机支持交换内存)

# 内存寻址范围

  • 32 位 CPU 和 64 位 CPU
  • 控制其寻址范围的是 MCU,一般与 CPU 位数对应

# 安卓内存管理:

内存基本单位:Page,没有经过设置一般为 4K,分配和回收以 Page 为单位 (并不意味所有数据都是 4K 对齐)

  • 分用户态和内核态,用户访问内核态 (如 Native 分配) 会错误

内存杀手:low memery killer

  • 杀顺序:缓存 -> 前一个应用 ->Home->Service->Perceptible->Foreground->Persistent (前台驻留内存,杀掉前台还在的)->System (重启)

内存指标:

  • Resident Set Size (RSS):当前应用所用掉的所有内存 (包括调用公共库导致的内存分配)
  • Proportional Set Size (PSS):当前应用内存 + 均分公共库内存 (公共库被调用者均分 -- 内存公摊)
  • Unique Set Size (USS):只有应用自己分配的内存,我们能做到的就是优化这一块内存,并避免在 PSS 上造成更多压力
    • procrank 命令查看内存分配

# Unity 内存

  • 类型:
    • Native Memery
      • 重载了所有内存分配符,每一个操作符 (Allocator) 被使用时要求一个额外参数:MemeryLable,指当前这一块内存要分配到哪个内存池,Profiler 查看时那些名字就是这个统计
    • Managed Memery
    • 引擎管理内存
    • 用户管理内存
  • Unity 检测不到用户分配的 Native 内存、Lua 内存也是无法被 Unity 直接统计到的

# Native 内存

# Scene

  • 场景中的对象,GameObject
  • 当我们创建一个 GameObject 时,Unity 底层会构建一个或多个 Object 存储其信息 (因为其内部还可能存在多个 Component)
    • 也因此当场景有过多 GameObject 时,Native 内存会有显著增长
    • 通过 Profiler 发现 Native 内存大量上升时,可以检查该项

# Audio

  • DSP Buffer:填充满了才会向 CPU 发送指令,过大会导致声音延迟,太小会导致 CPU 负担上升
  • Force To Mono
  • Format:是否硬解支持,IOS 对 MP3 有硬解支持
    • MP3 比 Vorbis 更大,但是比 ADPCM 更小
  • Compression Format

# Code Size

  • 模板泛型滥用,IL2CPP 会被展开编译成静态代码,不同的泛型参数会导致排列组合形成代码膨胀

# AssetBundle

  • Type Tree:当前版本序列化字段形成一张对应表,如果换了一个版本反序列化,没有的字段可以直接采用默认值,避免出错。
    • 确认不会对兼容性造成影响 (跨版本打热更?),就可以关掉。关掉可以 (1) 减小内存、(1) 减小包体大小,(3) Build 和运行时会变快。
    • 若存在 Type Tree,会进行两次序列化 (反序列化),第一步先反序列化出 Type Tree,第二步再反序列化出实际内容
  • LZ4:推荐,但是压缩率会比 LZMA 平均差 30%,速度快 10 倍以上 (官方称),基于 ChunkBase ,可以一块一块解压
    • 例如,一个文件是从第 5 块~10 块,会从第五块开始,5 块、6 块、分别解压,并重用之前内存,可以减少内存峰值
  • LZMA:官方很不推荐,因为解压和读取速度都很慢,另外会占大量内存,因为不是 ChunkBase 而是 Stream,需要一次性读取全解压。
  • Size & Count:每个 Bundle 包含资源数量,没有定论,需要平衡

# Resources 目录

  • 这个目录在打进包的时候,会生成一个红黑树,用于帮助检索资源位置,这棵树在刚开始游戏就会加载进内存且不可卸载,造成持续内存压力。并且会拖慢游戏启动速度:没有分析生成完毕,游戏不会正式启动。
  • 官方极不推荐:建议最多 Debug 环境使用,正式环境直接删掉。

# Texture

  • upload buffer:也可以设置,与声音的 DSP Buffer 有点像,就是填满多大向 GPU Push 一次
  • Read/Write:正常情况下,一张图读进内存,然后提交到 upload buffer 后就会直接 Delete 掉。检测到开启该选项就不会 Delete,显存内存各一份。手游显存和内存通用,就会导致 Unique Memery 存在两份。
  • Mipmap:UI 之类都不用开,如 3D 模型的贴图,涉及 3D 相机变化,才可能需要开启 (需要一个平衡,会增加大概 30% 内存占用,减轻渲染消耗)。

# Mesh

  • Read/Write:与图片一样
  • Compression:压缩,减少文件大小,对内存没帮助,使用时还是解压 (而且有可能会导致内存占用更多)

# Assets

  • 资源管理方式

# Unity Managed Memery

  • VM 内存池
    • 以 Block 进行管理,当一个 Block 连续 6 次未被 GC 访问,会返还系统 (所以这种情况基本上看不到)
    • 不会频繁分配 reserved 内存,一次性分配一大块,每次当接近一个阈值会按照一个比例乘出来
  • GC 机制
    • Unity Mono 使用 Boehm
      • 不分带的
      • 非压缩,不整理内存
      • 为何没升级 Mono GC:要交版税,以及后来转向 IL2CPP,自己实现升级了渐进式 GC
    • 下一代 GC
      • Incremental GC (渐进试 GC)、
        • 正常 GC 会暂停主线程,进行 GC 操作,会造成主线程卡顿
        • 该项将暂停主线程操作分帧做,GC 总体时间不会变,减少的峰值消耗
        • IL2CPP 为 Unity 自己实现的 GC 机制,升级版 Boehm
  • 为什么内存下降了,总体内存池上升?
    • Memery Fragmentation (内存碎片化):分配的新内存已经插不进去了 (虽然碎片加起来可能远远满足需求),造成严重浪费 —— 这些碎片可能再也用不了了。
    • 高密度加载释放,先操作大内存,再操作小内存
    • Zombie Memery (僵尸内存)
      • 并非内存泄露 (无人可以访问和管理)
      • 无用内存、没有释放
      • 通过代码管理和性能工具分析
  • 推荐做法
    • Class (长生命周期) 和 Struct (短生命周期)
    • 内存池,高频使用的小对象
    • 闭包和匿名函数:闭包和匿名函数全被创建为 Class
    • 协程:轮询模式,即使是局部变量,在协程未结束之前也会一直占用,以 Class 形式。所以推荐用的时候生产,用完释放
    • 配置表:是否有庞大配置表,不要全部一次性扔内存
    • 单例:慎用
  • 问题
    • GameObject.SetActive:内部有大量操作,特别是 UI 会产生 额外 GC,激活一个 UI 时还会递归子 UI 初始化。建议这一块比较影响的话,将其移动至屏幕外

内存最佳实践:
https://learn.unity.com/tutorial/memory-management-in-unity#