# 什么是内存?
# 物理内存
# 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
- 引擎管理内存
- 用户管理内存
- Native 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
- Incremental GC (渐进试 GC)、
- Unity Mono 使用 Boehm
- 为什么内存下降了,总体内存池上升?
- Memery Fragmentation (内存碎片化):分配的新内存已经插不进去了 (虽然碎片加起来可能远远满足需求),造成严重浪费 —— 这些碎片可能再也用不了了。
- 高密度加载释放,先操作大内存,再操作小内存
- Zombie Memery (僵尸内存)
- 并非内存泄露 (无人可以访问和管理)
- 无用内存、没有释放
- 通过代码管理和性能工具分析
- 推荐做法
- Class (长生命周期) 和 Struct (短生命周期)
- 内存池,高频使用的小对象
- 闭包和匿名函数:闭包和匿名函数全被创建为 Class
- 协程:轮询模式,即使是局部变量,在协程未结束之前也会一直占用,以 Class 形式。所以推荐用的时候生产,用完释放
- 配置表:是否有庞大配置表,不要全部一次性扔内存
- 单例:慎用
- 问题
- GameObject.SetActive:内部有大量操作,特别是 UI 会产生 额外 GC,激活一个 UI 时还会递归子 UI 初始化。建议这一块比较影响的话,将其移动至屏幕外
内存最佳实践:
https://learn.unity.com/tutorial/memory-management-in-unity#