【前言】

当我们谈及Unity内存管理时,我们更多的是在说手游项目上如何更好的去管理内存,如果是在端游项目上,没有那么多讲究,内存随便用。

【为什么手机上内存不够用】

CPU读写速度远快于内存的速度,大多数时候CPU都在等内存给数据,为了缓解主存速度慢、跟不上CPU读写速度要求的矛盾,进而提高程序运行效率,CPU设计时引入了高速缓冲存储器。在PC中,CPU一般有三级缓存 ,大小共8~16M。而手机没有独立显卡、独立显存,CPU和GPU共用一个缓存,其内存更小,缓存级数更少、大小仅为2M。

PC有内存交换机制,在物理内存不够用时,操作系统会将不用的数据(DeadMemory)交换到硬盘上。而手机不做内存交换,一是因为移动设备的硬盘IO速度比PC慢很多,二是因为移动设备的硬盘可擦写次数更少,频繁擦写会减少设备使用寿命。Android机上没有做内存交换,IOS可以把不活跃的内存进行压缩,使得实际可用的内存更多,这就是为什么同样大的物理内存,IOS的虚拟内存会标的更大。

【Android的内存管理】

Android的内存管理基本单位是Page(页),一般是4k 一个Page。内存的回收和分配都是以 Page为单位进行操作。Android内存分用户态和内核态两个部分,内核态的内存是用户严格不能访问。的。

内存杀手(low memory killer,LMK):

当手机内存不够用时,就会出现LMK,按照优先级,杀掉手机内的各种应用和服务,直到内存够使用为止。其优先级如下:

0、Native:系统内核

1、System:系统服务

2、Persistent:用户服务,比如电话、蓝牙、Wifi等。这一层被杀掉,系统重启。

3、Foreground:前台应用,当前正在使用的Activity。主要是前台应用不断使用内存,造成内存不够,这一层被杀掉,前台应用闪退。

4、Perceptible:辅助应用,音乐、搜索、键盘等;这一层被杀掉,音乐突然没了,键盘没响应。

5、Service:驻后台线程的服务,云同步、垃圾回收等;

6、Home键;这一层被杀掉,桌面重启,APP图标一个个重新出现,壁纸可能没了。

7、Previous:上一个使用的应用;这一层被杀掉,继续切换到上一个应用时,该应用会重启。

8、Cached:后台,之前使用过的各种应用。这一层被杀掉,会发现切换到后台的那个应用时,该应用重启了。

内存指标

RSS,resident set size:当前的APP所应用到的所有内存,包括自己的APP所使用的内存和调用的各种服务、共用库所产生的内存。

PSS,proportional set size:PSS会把公共库所使用的内存平摊到所有调用这个库的APP上,如果调用的某个公共库已经有了很大的内存分配,平摊下来就会导致自己的APP的PSS虚高。

USS,unique set size,指自己的APP使用的内存,实际工作中的优化指优化USS。

【Unity游戏运行时占用的内存】

游戏运行时使用的内存可以分为五类:一是程序代码内存(Code Memory)、二是本地内存(Native Memory)、三是托管内存(Managed Memory)、四是用户管理的内存、五是Lua管理的内存。

Unity的内存管理主要指二和三。注意,在Editor下和在Runtime下Unity的内存分配的时机、方式、大小不同的,有时查看内存使用情况需要连真机查看。

Code Memory

包括了所有的Unity引擎代码,使用的库,第三方插件代码、自己写的代码等。这些代码在打包后会被编译成特定的程序代码文件,这些文件在游戏运行前需要一次性加载到内存中,在退出游戏后再从内存中卸载。在整个游戏运行过程中,程序代码一直占用一定的内存,无法在运行时管理或优化,只能在打包前减少程序代码的大小。

Native Memory

Unity底层是用C++写的,除了一些Editor里面的Services可能会用到NodeJS这些网络的语言,Runtime里面用到的每一行Unity底层代码全是C++的,其分为三层:

  • 最底层的Runtime,全是Native C++代码。
  • 最上层用C#,其中Unity的Editor、一些Package也是C#写的,游戏开发的绝大多数逻辑都是C#写的
  • 中间叫Binding,可以看见很多的.bindings.cs文件(基于C#的binding语言,一开始是Unity自定义的一种语言),这些文件的作用就是把C++和C#联系在一起,为C#层提供所有的API

我们平时使用Unity时看见的C# API,都是在Binding层中自定义的。这些文件底层运行的时候还是C++,只是进行了Wrapper(封装)。

早期用户代码是运行在C#上,是MonoRuntime。但是现在可以通过IL2CPP将其转成C++代码,所有现在几乎没有纯正的C#在运行了。

Unity的VM(虚拟机:Virtual Machine)依旧还是存在,主要用于跨平台,有了一层VM抽象后,跨平台的工作会容易很多,IL2CPP本身也是个VM。

Unity C++层重载了所有分配内存的操作符,分配内存的东西叫Allocator。重载后分配内存是需要传入一个额外的参数memory label(可以在Profiler-shaderlab-object-memory-detail-snapshot看到),Allocator 会根据memory label在不同类型池里分配内存。每个类型池里做单独的跟踪,可以根据memory label在runtime获取类型池的大小等信息。

Allocator 在 NewAsRoot 中生成,生成一个所谓的Memory island。它下面会有很多的子内存。例如一个Shader,当我们加载一个shader进内存的时候,首先会生成一个shader的Root,也就是Memory Island。Shader底下的数据,例如Subshader,Pass,Properties等,会作为该Root底下的成员,依次的分配。最后统计Runtime的内存时,统计这些Root即可,而不会统计成员,因为太多了没法统计。

和托管内存堆不同的是,当我们去delete或free一个内存的时候,会立刻返回给系统。

贴图、音效、Mesh、AssetBundle等资源的管理和加载都与Native Memory有关。

Managed Memory

这里的Managed Memory主要指Mono VM的内存池,其内存以Block的形式管理,当一个Block连续6次GC没有被访问到,这块内存会被返回给系统。条件苛刻,比较难触发。

Unity的GC机制是Boehm内存回收,是不分代的,非压缩式的。(C#的GC是分代、压缩的)

分代是指:大块内存、小内存、超小内存是分在不同内存区域来进行管理的。还有长久内存,当有一个内存很久没动的时候会移到长久内存区域中,从而省出内存给更频繁分配的内存。

压缩是指:当有内存被回收的时候,把已经用到的内存里的内容移动到空出来的内存上,已用到的内存紧密排列。非压缩就是空着不管,下次要用了下次要用了再填进去。

IL2CPP 的 GC 机制是 Unity 自己重新写的,是升级版的 Boehm

 GC不分代、不压缩会造成Memory fragmentation 内存碎片化的问题。如下图所示,当进行内存分配时发现的第一块空闲内存不够时,那就要找一块更大的内存,如果找不到更大的内存,那就要像操作系统申请内存了。如果存在大量的小内存,会造成统计出的空闲内存总量足够多,但仍需要像系统申请内存的现象,也即某一刻游戏占用的内存突然增大,但统计发现有很多内存没用到。

为了防止内存碎片化(Memory Fragmentation),在做加载的时候,应先加载大内存的资源,再加载小内存的资源(因为Bohem没有内存压缩),这样可以保证最大限度地利用内存。

另外,也可能造成僵尸内存Zombie Memory,其不会被用到,但也不会被释放。

---下一代GC——Incremental GC(渐进式 GC)

进行GC必须暂停主线程,如果GC时间过长,会造成主线程卡顿。Incremental GC把暂停主线程的事分帧做了,这样主线程不会单次暂停过长时间。用Incremental GC和不用,主线程总体暂停时间(GC回收时间)基本差不多。

User Memory

用户自己管理的内存主要包括Native插件内存、Unsafe代码内存等,这些代码用的是非托管内存靠用户自己管理,要像C/C++一样记得分配和释放内存。

Native 插件大多用C++编写, Unity 无法分析已经编译过的 C++ 是如何去分配和使用内存的Unity无法统计其内部的内存使用情况。因此,Unity的Profile工具无法检测用户自己管理的内存。

Lua Memory

Lua有自己的内存管理方式,需要针对Lua做优化,这个要求比较高。

【Code Memory优化】

整个游戏的代码可以分为四类:引擎代码、库代码、插件代码、业务代码,优化的优先级为库代码>业务代码>插件代码>引擎代码

---库代码:默认引用的一些基础库(例如.Net)包含的东西过于全面,有些在游戏中用不到,可以剥离,Unity中有如何剥离代码的说明。

---业务代码:

对Debug相关的代码要剥离,可以用宏定义Release和Debug,做个工具类统一管理。

如果打包使用IL2CPP,那么在写业务代码时要注意避免模板泛型的滥用。模板泛型的滥用,会影响到代码文件大小以及打包速度。例如一个模板函数有四五个不同的泛型参数(float,int,double等),c++编译的时候我们用的所有的Class,所有的Template最终都会被展开成静态类型(因为是AOT编译,需要事先把所有可能的结果编译出来)。因此当模板函数有很多排列组合时,最后编译会得到所有的排列组合代码,导致文件很大,影响使用IL2CPP时的打包速度。

---插件代码:同库代码一样,插件的有些功能在游戏中用不到,相关代码可以剥离。但剥离的前提是得看懂插件源码知道要剥离哪一部分,要不然剥离多了,很容易打包失败。基本上没谁有耐心把插件源码看懂,性价比低。所以插件代码的优化在于避免引入各种乱七八糟的插件,随意引入各种插件,不仅会使Code Memory增大,还会使得包体增大。(这里认为插件包括:接入的SDK、unity原生插件等)

---引擎代码:这个基本改不了,不用考虑

【 Native Memory优化】

Scene

Scene是导致Native Memory增长的原因最常见原因,在场景中new GameObject会导致Native Memory快速增长。因为是c++引擎,所有的实体最终都会反映在c++上,而不会反映在托管堆上。当我们new GameObject的时候,实际上在Unity的底层会构建一个或多个object来存储这一个GameObject的信息(GameObject可能有很多Component)

Audio

---DSP Buffer,是指一个声音的缓冲,当一个声音要播放的时候,需要向CPU去发送指令。如果声音的数据量非常的小,会造成频繁的向CPU发指令,造成IO压力。在Unity的FMOD声音引擎里面,一般会有一个Buffer,当Buffer填充满了才会去向CPU发送一次播放声音的指令。如果DSP Buffer的值过打,填充满需要很多的声音数据,当我们声音数据不大的时候,就会产生延时。如果DSP Buffer过小,仍可能会频繁向CPU发指令,没起到作用。可以在Project Setting ->Audio中设置DSP Buffer的大小。

---Force To Mono:​这个选项作用是强制单声道,很多音效师为了追求音质会设置成双声道,导致声音在包体和内存中,占用的空间加倍,但是95%以上的声音,两个声道是完全一样的数据。因此对声音不是很敏感的项目建议勾选此项,来降低内存的占用。

---Format:格式设置,不同平台对音频的格式支持不一样,这个纹理格式一个道理,具体在不同平台选择什么格式可以看Unity手册。

AssetBundle

---TypeTree:Unity前后有很多的版本,不同的版本中很多的类型可能会有数据结构的改变,为了做数据结构的兼容,会在对数据类型序列化的时候,生成一个叫TypeTree的东西,以记录当前这个版本用到了哪些变量,它们对应的数据类型是什么,当进行反序列化的时候,根据TypeTree去做反序列化。例如,如果上一个版本的类型在这个版本没有,那在当前版本的TypeTree里就没有它,所以上一个版本的类型不会被序列化。如果有新的类型,但是在当前版本不存在的话,那要用它的默认值来序列化。从而保证了在不同版本之间不会序列化出错。

在Build AssetBundle的时候,有开关可以关掉TypeTree。

BuildAssetBundleOptions.DisableWriteTypeTree

如果AssetBundle和APP都是从相同版本的Unity中Build出来的,就可以关闭TypeTree。这样,一可以减少内存,二AssetBundle包大小会减少,三build和运行时会变快,因为不会去序列化和反序列化TypeTree。所以,我们会经常要求版本一致。

---压缩方式(Lz4和Lzma):现在Unity主推Lz4(也就是ChunkBased,BuildAssetBundleOptions. ChunkBasedCompression),Lz4非常快,大概是Lzma的十倍左右,但是平均压缩比例会比Lzma差30%左右,即包体可能会更大些。Lz4的算法开源。

Lzma基本可以不用了,因为Lzma解压和读取速度都会非常慢,并且占大量的内存,因为不是ChunkBased,而是Stream,也就是一次全解压出来。而ChunkBased可以一块一块解压,每次解压可以重用之前的内存,减少内存的峰值。

---大小和数量:AssetBundle分两部分,一部分是头(用于索引,这部分可以重用),一部分是实际的打包的数据部分。如果每个Asset都打成一个AssetBundle,那么可能头的部分比数据还大。官方建议一个AssetBundle的大小在1-2M,可以根据网络带宽加大。

Resource

Resource文件夹里的内容被打进包的时候会做一个红黑树(R-B Tree)用做索引,即检索资源到底在什么位置。所以Resource越大,红黑树越大,它不可卸载,并在刚刚加载游戏的时候就会被一直加在内存里,极大的拖慢游戏的启动时间,因为红黑树没有分析和加载完,游戏是不会启动的,并造成持续的内存压力。所以建议不要使用Resource,使用AssetBundle。

Texture

---Upload Buffer:在Unity 的 Quality 里设置如图,和声音的Buffer类似,填满后向GPU push 一次。

---Read/Write:没必要的话就关闭,正常情况,Texture读进内存解析完了放到Upload Buffer里之后,内存里那部分就会delete掉。除非开了Read/Write,那就不会delete了,会在显存和内存里各一份。手机内存显存通用的,所以内存里会有两份。

---​Mip Maps:例如UI元素这类相对于相机Z轴的值不会有任何变化的纹理,关闭该选项

---Format:选择合适的Format,可减少占用的空间

​​---alpha:对于不透明纹理,关闭其alpha通道

---POT:纹理的大小尽量为2的幂次方(POT),因为有些压缩格式可能不支持非2的幂次方的

---压缩:[2018.1]Unity贴图压缩格式设置 - 知乎

---合并:打图集

Mesh 

---Read/Write:同Texture,若开启,Unity会存储两份Mesh,导致运行时的内存用量变成两倍

---Compression:Mesh Compression是使用压缩算法,将Mesh数据进行压缩,结果是会减少占用硬盘的空间,但是在Runtime的时候会被解压为原始精度的数据,因此内存占用并不会减少

​​--Rig:如果没有使用动画可以,例如房子,石头这些

--​​-Blendshapes:如果没有用到Blendshapes,也关闭

【Managed Memory优化】

参考 :unity GC优化

【优化重点及方向】

Managed Memory>第三方库(主要是lua Memory)>Native Memory>User Memory>Code Mamory

【参考】

Unity 3D中的内存管理 | OneV's Den

[Unity 活动]-浅谈Unity内存管理_哔哩哔哩_bilibili

Unity的内存管理与性能优化 - 知乎

Unity内存管理的原理相关推荐

  1. Unity内存管理你应该知道的底层原理

    本文首发公众号洪流学堂.洪流学堂,让你快人几步. 本文主要是Unity官方川哥的视频<浅谈Unity内存管理>的笔记及相关知识点补充,如果有时间强烈建议学习原视频: https://www ...

  2. 【Unity】Unity内存管理与优化(三)

    文章目录 Unity内存管理 大对象堆LOH(Large Object Heap) 方法调用 内存细分 分析内存 - 分析内存消耗 - 分析内存效率 内存管理性能增强 垃圾回收策略 手动JIT编译 值 ...

  3. 利用图文和代码深度解析操作系统OS的内存管理实现原理机制和算法

    利用图文和代码深度解析操作系统OS的内存管理实现原理机制和算法. 内存作为计算机系统的组成部分,跟开发人员的日常开发活动有着密切的联系,我们平时遇到的Segment Fault.OutOfMemory ...

  4. 演示unity内存管理机制的缺陷

    概述 这是最近做项目时发现的一个内存管理机制上的一个缺陷,但是我并不知道这究竟是不是一个bug,因为他可以造成内存泄漏,但是却能避开野指针. 详细 代码下载:http://www.demodashi. ...

  5. 深入研究glibc内存管理器原理及优缺点

    最近查清了线上内存占用过大和swap使用频繁的原因:由于linux使用的glibc使用内存池技术导致的堆外内存暴增,基于这个过程中学习和了解了glibc的内存管理原理,和大家分享,如有错误请及时指出. ...

  6. 【Unity】Unity内存管理与优化(一)内存域、堆栈、垃圾回收、内存泄漏、内存碎片

    文章目录 Unity内存 内存域 - 托管域 - 本地域 - 外部库 - 跨桥操作 堆和栈 - 栈 - 堆 - 堆栈的使用 垃圾回收 - Mono内存分配过程 - 内存泄漏 - 内存碎片 - 运行时垃 ...

  7. unity 内存管理

    复制 博客园一位大神的杰作 Unity3D在内存占用上一直被人诟病,特别是对于面向移动设备的游戏开发,动辄内存占用飙上一两百兆,导致内存资源耗尽,从而被系统强退造成极差的体验.类似这种情况并不少见,但 ...

  8. 内存管理<原理篇>(五、页表和快表)

    文章目录 5.1 页表 5.1.1 页表介绍 5.1.2 页表项 5.2 页表结构 5.2.1 多级页表 5.2.2 哈希页表 5.2.3 倒置页表 5.3 快表 5.3.1 局限性原理 5.3.2 ...

  9. ARM64的MMU内存管理工作原理

    文章目录 1.背景介绍 2.MMU地址空间 3.MMU的映射粒度 3.1 4KB映射粒度 3.2 16KB映射粒度 3.3 64KB映射粒度 3.64KB的MMU映射分析 4. 参考资料 1.背景介绍 ...

最新文章

  1. javascript优化_如何通过使用服务人员来优化JavaScript应用
  2. Linux调优方案,sysctl.conf的设置
  3. docker privileged作用_Docker环境下秒建Redis集群,连SpringBoot也整上了!
  4. 创建基于Web的实时系统
  5. 11座城市,58个.NET最新岗位速览,内推直通面试官!
  6. Linux samba的配置和使用
  7. 如何正确的更换网站服务器?
  8. WinXP利用无线网卡做AP共享上网
  9. 7-4 超速判断 (10 分)
  10. 走近夜间灯光——教你平均灯光指数(ANLI)如何得到(超详细)
  11. OMC IT监控运维管理平台建设方案
  12. tp3.2中前台模板中日期时间的转换
  13. POJ - 1179
  14. Python爬虫练习:爬取蜂鸟网图片数据
  15. OPPO手机怎么找到快应用入口
  16. Jetson-Xavier-NX刷机+pytorch环境配置+yolov5运行
  17. 魅族便签导出,实践中
  18. matlab 28m35,使用F28M35添加自己写的头文件时,出现了“XXX”has already been declared in the current scope的问题...
  19. Java 堆内存分析
  20. matlab 画偏振态,MATLAB:绘制三维偏振光动画

热门文章

  1. java原生开发是什么意思,深入剖析
  2. USB接口鼠标接触不良的简单处理
  3. SpringBoot整合redis使用setnx完成分布式锁
  4. IIC/I2C总线实验
  5. Opencv Mat数据类型操作
  6. Buu Crypto
  7. Html5版音乐游戏制作及分享(H5音乐游戏)
  8. AtCoder Beginner Contest 217 A B C D E G 题解
  9. Logstash同步数据
  10. VMware-workstation-full-10.0.2中英文切换