前言
之前看的一个Unity官方的视频:浅谈Unity内存管理(https://www.bilibili.com/video/BV1aJ411t7N6);觉得干货满满,但是一时又不能完全记下来,当时就想记个笔记。但是后面要么忘了,要么没时间,一直没有实现(主要还是懒)。
这一次重新看一遍视频,再结合我自己的一些理解,一边学习一边总结。
正文
1、什么是内存
内存分为物理内存、虚拟内存。
关于物理内存:
关于物理内存需要记住:CPU访问内存是一个慢速过程。CPU在需要访问内存时,先是访问自己的缓存(L1Cache、L2Cache……),当全部Miss之后,然后CPU回去主内存拿一段完整的指令到CPU的缓存中。因此,我们需要尽可能保证CPU的指令是连续的,防止CPU过多地与主内存之间的内存交换产生IO。Unity为了处理上述问题,减少Cache Miss ,使用了ECS和DOTS,把分散的内存数据变成整块、连续的数据。
(PS:ECS和DOTS虽然已经实装,但我个人的使用体验来讲还不够成熟,而且API还有可能再改。而且在ECS的部分,似乎热更也是个问题,所以要谨慎使用。)
关于虚拟内存:
电脑在物理内存不够的时候,操作系统会把一些不用的数据(DeadMemory)交换到硬盘上,称之为内存交换。但是手机是不做内存交换的,一是因为移动设备的硬盘IO速度比PC慢很多,而是因为移动设备的硬盘可擦写次数更少;因此手机如果做内存交换一是慢,而是减少设备寿命看,所以Android机上没有做内存交换。IOS可以把不活跃的内存进行压缩,使得实际可用的内存更多,而安卓没有这个能力。
关于移动设备和PC:
移动设备(手机)与PC的区别在于,手机没有独立显卡、独立显存。手机上无论是CPU还是GPU都是共用一个缓存,而且手机的内存更小、缓存级数更少、大小更小。台式机的三级缓存大约8~16M,而手机只有2M。
综上,手机上的内存,不论从哪个角度看,都是比PC要小很多的。所以,手机上更容易出现内存不够的问题。
2、Android 内存管理
Android是基于Linux开发的,所以Android的内存管理和Linux很相似。
Android的内存管理基本单位是Page(页),一般是4k 一个Page。内存的回收和分配都是以 Page为单位进行操作,也就是4k。Android内存分用户态和内核态两个部分,内核态的内存是用户严格不能访问的。
关于内存杀手:Low Memory Killer (LMK)
当手机的内存使用量过多时,就会出现LMK,对当前手机的各种App、服务进行关停。安卓的各种应用、服务分为以下一些类别:
0、Native:系统内核
1、System:系统服务
2、Persistent: 用户服务,比如电话、蓝牙、Wifi等。
3、Foreground:前台应用,当前正在使用的Activity
4、Perceptible:辅助应用,音乐、搜索、键盘等;
5、Service:驻后台线程的服务,云同步、垃圾回收等;
6、Home键;
7、Previous:上一个使用的应用;
8、Cached:后台,之前使用过的各种应用。
这个也是Android系统的应用优先度排序,编号越小优先级越高。当LMK开始工作的时候,会从优先度最低的应用开始Kill。即最先中断各种Cached,最后才会到Native。
例如当Cached被杀掉之后,现象就是当你切换到后台的那些应用时,你会发现那些应用重启了。
当Home被杀死的时候,你发现当你回到桌面时,桌面会重启,你的桌面图标会重建,或者壁纸没了。
到Perceptible的时候,可能你的音乐、键盘不见了。
再往上进行,到Foreground时,当前前台应用就会被杀死,这个时候就会出现应用闪退。
在往上手机就开始重启了。
3、Android内存指标
RSS:Resident Set Size
你当前的APP所应用到的所有内存。除了你自己的APP所使用的内存之外,你调用的各种服务、共用库所产生的内存都会统计到RSS之中。
PSS:Proportional Set Size
与RSS不同的是,PSS会把公共库所使用的内存平摊到所有调用这个库的APP上。(可能你自己的应用没有申请很多内存,但是你的调用的某个公共库已经有了很大的内存分配,平摊下来就会导致你自己的APP的PSS虚高。)
USS:Unique Set Size
只有此APP所使用的内存,剔除掉公共库的内存分配。
我们在实际工作中更多要做的是对USS的优化,有时也会注意一下PSS。
4、关于Unity内存
Unity内存的分类
Unity内存分为 Native Memory和 Managed Memory (托管内存)。值得注意的是,在Editor下和在Runtime下Unity的内存分配是完全不同的。不但分配内存的大小会有不同,甚至分配的时机、方式都会有所不同。
比如一个AssetBundle,在编辑器下是你一打开Unity就开始加载,而在Runtime下则是你使用时才会加载。(Unity2019之后做了一些Asset导入优化,不使用的资源就不会导入)。
Unity的内存还可以分为引擎管理的内存和用户管理器的内存两类。引擎管理的内存一般开发者是访问不到的,而用户管理的内存才是使用者需要关系和优先考虑的。
Unity监测不到的内存
用户分配的 Native 内存内存是Unity的Profile工具监测不到。例如用户写的C++Native插件、以及Lua分配的内存。
5、关于Unity Native内存的控制:
Scene
当一个Scene中有过多的GameObject存在的时候,Unity Native 内存就会显著上升。
Audio(音频):
DSP Buffer :相当于音频的缓冲。当DSP Buffer过大时,会导致声音延迟(需要更多的音频数据来填充DSP Buffer)。如果DSP Buffer过小,会导致CPU负担上升。
Foce to Mono : 强制单声道(当两个声道完全相同时可以Force To Mono),可以节省一半的内存。
Format:例如IOS对MP3有硬解支持的,所以MP3的解析会快很多(Android 没有)。
Compressiont Format:声音文件在内存的存在形态(解压的、压缩的等)。
Code Size:
代码也是需要加载进内存的,使用时要注意减少模板泛型的滥用。因为模板泛型在编译成C++时,会把同样的代码排列组合都编译一边,导致Code Size 大幅上升。
AssetBundle:
TypeTree : 当前版本所用到的各种版本数据序列化类型。这个东西其实是Unity在做版本兼容的时候有用,保证Unity能向下兼容。当你确定你当前的AssetBundle和你的Unity是同一个版本的时候,就可以关掉TypeTree。关掉TypeTree之后可以减少内存大小、包大小、加快运行速度。
压缩方式:使用Lz4,而不是Lzma;
Size & Count:AssetBundle包的大小、数量。一般合适的Asb包大小大概是1M~2M的大小,可适量加大。
Resources文件夹:
Resources会在打包时生成一个红黑树,当资源过多时会相应地导致红黑树也变大,还会极大增加游戏的启动时间。另外Resources文件夹下的资源是不会卸载的,会存在于整个应用的生命周期。
尽量使用AssetBundle而不是Resources。
Texture:
Upload Buffer,和声音的DSP Buffer,设置填充满多大之后再推向CPU/GPU。
Read/Write : 不使用就关闭它。正常情况下,当Texture被推给 CPU/GPU之后,内存就会把他删除,除非你打开了 Read&Write。
Mip Map : 像UI这些不需要的就关闭它。
Mesh:
Read/Write :同上
Compression:虽然写的是压缩,但实际效果并不一定有用,甚至还有副作用,建议不开。
6、Unity Managed Memory (托管内存):
VM内存池:
Mono虚拟机的内存池,实际上VM是会返回给操作系统。返回条件是当一块内存(Block)连续6次没有被访问到时,就会被返回给操作系统。当然这个返回基本看不到,尤其是在Mono Runtime的时候。
GC机制:
Unity的GC机制是Boehm内存回收,是不分代的,非压缩式的。(之所以是使用Boehm是因为Unity和Mono的一些历史原因,以及目前Unity主要精力放在IL2CPP上面)
Incremental GC(渐进式GC),现已实装。Incremental GC可以改善GC时的主线程卡顿,他把GC的工作分多帧进行。
IL2CPP的GC机制是Unity重写的Boehm。
为了防止内存碎片化(Memory Fragmentation),在做加载的时候,应先加载大内存的资源,再加载小内存的资源(因为Bohem没有内存压缩),这样可以保证最大限度地利用内存。
Zombie Memory(僵尸内存):无用内存或者没有释放的内存。通过代码管理和性能工具分析,查看各个资源的引用,避免僵尸内存的出现。
管理技巧:
1、用Destory而不是NULL 。
2、多使用Struct。
3、使用内存池(UI、粒子系统等)
4、闭包和匿名函数:减少使用。所有的闭包和匿名函数最后都会变成一个Class。
5、协程:只要不被释放,里面所有引用的所有内存都会存在。(用的时候生产一个,不用的时候扔掉)。
6、配置表:减少一次性使用的配置表数量;
7、单例:慎用,会长期占用内存。
后记:
建议各位Unity开发者去听听原视频,会收获更多更详细~