Unity内存管理的原理

Posted 永恒星

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Unity内存管理的原理相关的知识,希望对你有一定的参考价值。

【前言】 

当我们谈及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 3D中的内存管理

 

技术分享

本文欢迎转载,但烦请保留此行出处信息:http://www.onevcat.com/2012/11/memory-in-unity3d/

Unity3D在内存占用上一直被人诟病,特别是对于面向移动设备的游戏开发,动辄内存占用飙上一两百兆,导致内存资源耗尽,从而被系统强退造成极差的体验。类似这种情况并不少见,但是绝大部分都是可以避免的。虽然理论上Unity的内存管理系统应当为开发者分忧解难,让大家投身到更有意义的事情中去,但是对于Unity对内存的管理方式,官方文档中并没有太多的说明,基本需要依靠自己摸索。最近在接手的项目中存在严重的内存问题,在参照文档和Unity Answer众多猜测和证实之后,稍微总结了下Unity中的内存的分配和管理的基本方式,在此共享。

虽然Unity标榜自己的内存使用全都是“Managed Memory”,但是事实上你必须正确地使用内存,以保证回收机制正确运行。如果没有做应当做的事情,那么场景和代码很有可能造成很多非必要内存的占用,这也是很多Unity开发者抱怨内存占用太大的原因。接下来我会介绍Unity使用内存的种类,以及相应每个种类的优化和使用的技巧。遵循使用原则,可以让非必要资源尽快得到释放,从而降低内存占用。


Unity中的内存种类

实际上Unity游戏使用的内存一共有三种:程序代码、托管堆(Managed Heap)以及本机堆(Native Heap)。

程序代码包括了所有的Unity引擎,使用的库,以及你所写的所有的游戏代码。在编译后,得到的运行文件将会被加载到设备中执行,并占用一定内存。这部分内存实际上是没有办法去“管理”的,它们将在内存中从一开始到最后一直存在。一个空的Unity默认场景,什么代码都不放,在iOS设备上占用内存应该在17MB左右,而加上一些自己的代码很容易就飙到20MB左右。想要减少这部分内存的使用,能做的就是减少使用的库,稍后再说。

托管堆是被Mono使用的一部分内存。Mono项目一个开源的.net框架的一种实现,对于Unity开发,其实充当了基本类库的角色。托管堆用来存放类的实例(比如用new生成的列表,实例中的各种声明的变量等)。“托管”的意思是Mono“应该”自动地改变堆的大小来适应你所需要的内存,并且定时地使用垃圾回收(Garbage Collect)来释放已经不需要的内存。关键在于,有时候你会忘记清除对已经不需要再使用的内存的引用,从而导致Mono认为这块内存一直有用,而无法回收。

最后,本机堆是Unity引擎进行申请和操作的地方,比如贴图,音效,关卡数据等。Unity使用了自己的一套内存管理机制来使这块内存具有和托管堆类似的功能。基本理念是,如果在这个关卡里需要某个资源,那么在需要时就加载,之后在没有任何引用时进行卸载。听起来很美好也和托管堆一样,但是由于Unity有一套自动加载和卸载资源的机制,让两者变得差别很大。自动加载资源可以为开发者省不少事儿,但是同时也意味着开发者失去了手动管理所有加载资源的权力,这非常容易导致大量的内存占用(贴图什么的你懂的),也是Unity给人留下“吃内存”印象的罪魁祸首。


优化程序代码的内存占用

这部分的优化相对简单,因为能做的事情并不多:主要就是减少打包时的引用库,改一改build设置即可。对于一个新项目来说不会有太大问题,但是如果是已经存在的项目,可能改变会导致原来所需要的库的缺失(虽说一般来说这种可能性不大),因此有可能无法做到最优。

技术分享

当使用Unity开发时,默认的Mono包含库可以说大部分用不上,在Player Setting(Edit->Project Setting->;Player或者Shift+Ctrl(Command)+B里的Player Setting按钮)面板里,将最下方的Optimization栏目中“Api Compatibility Level”选为.NET 2.0 Subset,表示你只会使用到部分的.NET 2.0 Subset,不需要Unity将全部.NET的Api包含进去。接下来的“Stripping Level”表示从build的库中剥离的力度,每一个剥离选项都将从打包好的库中去掉一部分内容。你需要保证你的代码没有用到这部分被剥离的功能,选为“Use micro mscorlib”的话将使用最小的库(一般来说也没啥问题,不行的话可以试试之前的两个)。库剥离可以极大地降低打包后的程序的尺寸以及程序代码的内存占用,唯一的缺点是这个功能只支持Pro版的Unity。

这部分优化的力度需要根据代码所用到的.NET的功能来进行调整,有可能不能使用Subset或者最大的剥离力度。如果超出了限度,很可能会在需要该功能时因为找不到相应的库而crash掉(iOS的话很可能在Xcode编译时就报错了)。比较好地解决方案是仍然用最强的剥离,并辅以较小的第三方的类库来完成所需功能。一个最常见问题是最大剥离时Sysytem.Xml是不被Subset和micro支持的,如果只是为了xml,完全可以导入一个轻量级的xml库来解决依赖(Unity官方推荐这个)。

关于每个设定对应支持的库的详细列表,可以在这里找到。关于每个剥离级别到底做了什么,Unity的文档也有说明。实际上,在游戏开发中绝大多数被剥离的功能使用不上的,因此不管如何,库剥离的优化方法都值得一试。


托管堆优化

Unity有一篇不错的关于托管堆代码如何写比较好的说明,在此基础上我个人有一些补充。

首先需要明确,托管堆中存储的是你在你的代码中申请的内存(不论是用js,C#还是Boo写的)。一般来说,无非是new或者Instantiate两种生成object的方法(事实上Instantiate中也是调用了new)。在接收到alloc请求后,托管堆在其上为要新生成的对象实例以及其实例变量分配内存,如果可用空间不足,则向系统申请更多空间。

当你使用完一个实例对象之后,通常来说在脚本中就不会再有对该对象的引用了(这包括将变量设置为null或其他引用,超出了变量的作用域,或者对Unity对象发送Destory())。在每隔一段时间,Mono的垃圾回收机制将检测内存,将没有再被引用的内存释放回收。总的来说,你要做的就是在尽可能早的时间将不需要的引用去除掉,这样回收机制才能正确地把不需要的内存清理出来。但是需要注意在内存清理时有可能造成游戏的短时间卡顿,这将会很影响游戏体验,因此如果有大量的内存回收工作要进行的话,需要尽量选择合适的时间。

如果在你的游戏里,有特别多的类似实例,并需要对它们经常发送Destroy()的话,游戏性能上会相当难看。比如小熊推金币中的金币实例,按理说每枚金币落下台子后都需要对其Destory(),然后新的金币进入台子时又需要Instantiate,这对性能是极大的浪费。一种通常的做法是在不需要时,不摧毁这个GameObject,而只是隐藏它,并将其放入一个重用数组中。之后需要时,再从重用数组中找到可用的实例并显示。这将极大地改善游戏的性能,相应的代价是消耗部分内存,一般来说这是可以接受的。关于对象重用,可以参考Unity关于内存方面的文档中Reusable Object Pools部分,或者Prime31有一个是用Linq来建立重用池的视频教程(Youtube,需要FQ,上半部分下半部分)。

如果不是必要,应该在游戏进行的过程中尽量减少对GameObject的Instantiate()和Destroy()调用,因为对计算资源会有很大消耗。在便携设备上短时间大量生成和摧毁物体的话,很容易造成瞬时卡顿。如果内存没有问题的话,尽量选择先将他们收集起来,然后在合适的时候(比如按暂停键或者是关卡切换),将它们批量地销毁并且回收内存。Mono的内存回收会在后台自动进行,系统会选择合适的时间进行垃圾回收。在合适的时候,也可以手动地调用System.GC.Collect()来建议系统进行一次垃圾回收。要注意的是这里的调用真的仅仅只是建议,可能系统会在一段时间后在进行回收,也可能完全不理会这条请求,不过在大部分时间里,这个调用还是靠谱的。


本机堆的优化

当你加载完成一个Unity的scene的时候,scene中的所有用到的asset(包括Hierarchy中所有GameObject上以及脚本中赋值了的的材质,贴图,动画,声音等素材),都会被自动加载(这正是Unity的智能之处)。也就是说,当关卡呈现在用户面前的时候,所有Unity编辑器能认识的本关卡的资源都已经被预先加入内存了,这样在本关卡中,用户将有良好的体验,不论是更换贴图,声音,还是播放动画时,都不会有额外的加载,这样的代价是内存占用将变多。Unity最初的设计目的还是面向台式机,几乎无限的内存和虚拟内存使得这样的占用似乎不是问题,但是这样的内存策略在之后移动平台的兴起和大量移动设备游戏的制作中出现了弊端,因为移动设备能使用的资源始终非常有限。因此在面向移动设备游戏的制作时,尽量减少在Hierarchy对资源的直接引用,而是使用Resource.Load的方法,在需要的时候从硬盘中读取资源,在使用后用Resource.UnloadAsset()和Resources.UnloadUnusedAssets()尽快将其卸载掉。总之,这里是一个处理时间和占用内存空间的trade off,如何达到最好的效果没有标准答案,需要自己权衡。

在关卡结束的时候,这个关卡中所使用的所有资源将会被卸载掉(除非被标记了DontDestroyOnLoad)的资源。注意不仅是DontDestroyOnLoad的资源本身,其相关的所有资源在关卡切换时都不会被卸载。DontDestroyOnLoad一般被用来在关卡之间保存一些玩家的状态,比如分数,级别等偏向文本的信息。如果DontDestroyOnLoad了一个包含很多资源(比如大量贴图或者声音等大内存占用的东西)的话,这部分资源在场景切换时无法卸载,将一直占用内存,这种情况应该尽量避免。

另外一种需要注意的情况是脚本中对资源的引用。大部分脚本将在场景转换时随之失效并被回收,但是,在场景之间被保持的脚本不在此列(通常情况是被附着在DontDestroyOnLoad的GameObject上了)。而这些脚本很可能含有对其他物体的Component或者资源的引用,这样相关的资源就都得不到释放,这绝对是不想要的情况。另外,static的单例(singleton)在场景切换时也不会被摧毁,同样地,如果这种单例含有大量的对资源的引用,也会成为大问题。因此,尽量减少代码的耦合和对其他脚本的依赖是十分有必要的。如果确实无法避免这种情况,那应当手动地对这些不再使用的引用对象调用Destroy()或者将其设置为null。这样在垃圾回收的时候,这些内存将被认为已经无用而被回收。

需要注意的是,Unity在一个场景开始时,根据场景构成和引用关系所自动读取的资源,只有在读取一个新的场景或者reset当前场景时,才会得到清理。因此这部分内存占用是不可避免的。在小内存环境中,这部分初始内存的占用十分重要,因为它决定了你的关卡是否能够被正常加载。因此在计算资源充足或是关卡开始之后还有机会进行加载时,尽量减少Hierarchy中的引用,变为手动用Resource.Load,将大大减少内存占用。在Resource.UnloadAsset()和Resources.UnloadUnusedAssets()时,只有那些真正没有任何引用指向的资源会被回收,因此请确保在资源不再使用时,将所有对该资源的引用设置为null或者Destroy。同样需要注意,这两个Unload方法仅仅对Resource.Load拿到的资源有效,而不能回收任何场景开始时自动加载的资源。与此类似的还有AssetBundle的Load和Unload方法,灵活使用这些手动自愿加载和卸载的方法,是优化Unity内存占用的不二法则~

以上是关于Unity内存管理的原理的主要内容,如果未能解决你的问题,请参考以下文章

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

Unity内存管理的原理

转Unity资源管理原理知识(干货不嫌长)

Unity - Android, C# - C++ 内存泄漏

Unity 3D中的内存管理

Unity编译Android的原理解析和apk打包分析