unity 内存管理

转载至以下文章:
https://onevcat.com/2012/11/memory-in-unity3d/ Unity 3D中的内存管理
https://docs.unity3d.com/Manual/iphone-playerSizeOptimization.html iOS stripping level(官网)
https://blog.csdn.net/cbbbc/article/details/49134961 .NET 跨平台开源项目——Mono介绍
http://www.cnblogs.com/88999660/archive/2013/03/15/2961663.html Unity3D占用内存太大的解决方法
https://blog.csdn.net/honey199396/article/details/52386731 Unity3D - Unity游戏Mono内存管理与泄漏
https://blog.csdn.net/sinat_20559947/article/details/72900680 Destroy和DestroyImmediate的区别
https://blog.csdn.net/nsdhy/article/details/73133605 基于Unity5.x版本资源内存管理方案

Unity中的内存种类:
程序代码、托管堆(Managed Heap)以及本机堆(Native Heap)

1,程序代码:
程序代码包括了所有的Unity引擎使用的库,以及你所写的所有的游戏代码。在编译后,得到的运行文件将会被加载到设备中执行,并占用一定内存。这部分内存实际上是没有办法去“管理”的,它们将在内存中从一开始到最后一直存在。想要减少这部分内存的使用,能做的就是减少使用的库。

优化:减少打包时的引用库,改一改build设置即可。对于一个新项目来说不会有太大问题,但是如果是已经存在的项目,可能改变会导致原来所需要的库的缺失(虽说一般来说这种可能性不大),因此有可能无法做到最优。
在Player Setting中的Optimization栏目中的:
Api Compatibility Level:选为.NET 2.0 Subset(子集),表示你只会使用到部分的.NET 2.0 Subset,不需要Unity将全部.NET的Api包含进去。选为.NET 2.0,表示使用全部的.net的api。
Stripping Level:表示从build的库中剥离的力度,每一个剥离选项都将从打包好的库中去掉一部分内容。你需要保证你的代码没有用到这部分被剥离的功能。

Stripping Level有以下几种选项:
Disable:不剥离。
Strip assemblies(程序集) level:分析脚本的字节码,以便可以从DLL中删除未从脚本引用的类和方法,从而将其排除在AOT编译阶段之外。 这种优化减少了主二进制文件和附带的DLL的大小,只要不使用反射就是安全的。
Strip ByteCode(字节码) level:任何.NET DLL(存储在Data文件夹中)仅被剥离为元数据。 这是可能的,因为所有代码都已在AOT阶段预编译并链接到主二进制文件中。
Use micro mscorlib level:使用特殊的较小版本的mscorlib。 一些组件从此库中删除,例如,Security,Reflection.Emit,Remoting,非Gregorian日历等。 此外,内部组件之间的相互依赖性最小化。 此优化减少了主二进制和mscorlib.dll大小,但它与某些System和System.Xml程序集类不兼容,因此请小心使用它。

选为“Use micro mscorlib”的话将使用最小的库(一般来说也没啥问题,不行的话可以试试之前的两个)。如果超出了限度,很可能会在需要该功能时因为找不到相应的库而crash掉(iOS的话很可能在Xcode编译时就报错了)。比较好地解决方案是仍然用最强的剥离,并辅以较小的第三方的类库来完成所需功能。一个最常见问题是最大剥离时Sysytem.Xml是不被Subset和micro支持的,如果只是为了xml,完全可以导入一个轻量级的xml库来解决依赖(Unity官方推荐(该网站))。



2,托管堆(Managed Heap)
托管堆是被Mono使用的一部分内存。Mono项目是一个开源的.net框架的一种实现,对于Unity开发,其实充当了基本类库的角色。托管堆用来存放类的实例,比如用new生成的列表,实例中的各种声明的变量等,(个人观点:从这句话来看,托管堆应该就是管理没有继承自MonoBehaviour的类所产生的内存,因为继承自MonoBehaviour的类必须挂载到GameObject上才会生效,且不使用new,而GameObject是本机堆管理的资源,Instantiate创建的内存被创建在了unity里,不是mono里。)。
“托管”的意思是Mono“应该”自动地改变堆的大小来适应你所需要的内存,并且定时地使用垃圾回收(Garbage Collect)来释放已经不需要的内存。关键在于,有时候你会忘记清除对已经不需要再使用的内存的引用,从而导致Mono认为这块内存一直有用,而无法回收。比如:没有继承自MonoBehaviour的类必须在继承自MonoBehaviour的类中才能使用,当创建类的对象为成员变量时,在初始化之后,由于成员变量一直存在,那这块内存将不会被释放。代码如下:
public class TestMemory : MonoBehaviour {
    
    TestMono testMono;//testMono为TestMemory对象的一部分,即分配到本机堆中,由本机堆控制,如果testMono为一个局部变量,那就由栈来释放。
    void Start () {
        testMono = new TestMono();//testMono如果不为空,这块内存将一直存在。如果为空,内存将会在某个时间被释放。
    }
}

public class TestMono
{
    public int[] arr;
    public TestMono()
    {
        arr = new int[1000000];
    }
}
public class TestMemory : MonoBehaviour {
    public int[] arr;
   
    void TestArr()
    {
        if (bIsCreat)
        {
            print("creat");
            arr = new int[10000000];//本机堆与托管堆同时分别暴增70M和40M
            bIsCreat = false;
        }
        if (bDeleteObj)
        {
            print("delete");
            arr = null;////本机堆与托管堆同时暴减,但是依旧有一部分资源没有释放,分别大概有30M和10M
            bDeleteObj = false;
        }
    }
}

优化:
1,需要注意在内存清理时有可能造成游戏的短时间卡顿,这将会很影响游戏体验,因此如果有大量的内存回收工作要进行的话,需要尽量选择合适的时间。
2,对象池:如果在你的游戏里,有特别多的类似实例,并需要对它们经常发送Destroy()的话,游戏性能上会相当难看。一种通常的做法是在不需要时,不摧毁这个GameObject,而只是隐藏它,并将其放入一个重用数组中。这将极大地改善游戏的性能,相应的代价是消耗部分内存,一般来说这是可以接受的。
3,如果不是必要,应该在游戏进行的过程中尽量减少对GameObject的Instantiate()和Destroy()调用,因为对计算资源会有很大消耗。在便携设备上短时间大量生成和摧毁物体的话,很容易造成瞬时卡顿。如果内存没有问题的话,尽量选择先将他们收集起来,然后在合适的时候(比如按暂停键或者是关卡切换),将它们批量地销毁并且回收内存。Mono的内存回收会在后台自动进行,系统会选择合适的时间进行垃圾回收。在合适的时候,也可以手动地调用System.GC.Collect()来建议系统进行一次垃圾回收。要注意的是这里的调用真的仅仅只是建议,可能系统会在一段时间后在进行回收,也可能完全不理会这条请求,不过在大部分时间里,这个调用还是靠谱的。
 

关于Mono(.NET 跨平台开源项目):
Mono是.NET Framework(开源框架)的一种开源实现。Mono项目将使开发者用各种语言(C#,VB.NET等)开发的.NET应用程序,能在任何Mono支持的平台上运行, 包括Linux, Unix。Mono项目将使大家能开发出各种跨平台的应用程序, 并能极大提高开源领域的开发效率。作为一个有机的.NET整体, 它包括一个C#编译器, 一个公用语言运行时环境, 以及相关的一整套类库,他甚至还包括IDE、调试工具和文档浏览器。

Mono希望实现“一次编写,到处运行”。这不是java的口号吗?但是我们知道java可以跨平台但是他不能跨语言。而.Net可以跨语言但是由于微软的战略他不能跨平台。所以Mono的目标就变为跨平台,跨语言。如果Mono成功的话,语言和平台对开发者的影响将变的很小。Mono未来的处境不是很好,因为当今的两大巨头微软和SUN都不支持他。因为微软的头号敌人是Linux,微软和Linux的斗争其本质是OS的竞争,微软是绝对不会让Linux得到普及的。而SUN的最大砝码是JAVA如果大家的都不用JAVA哪SUN还由活路吗?

Mono内存分为两部分,已用内存(used)和堆内存(heap),已用内存指的是mono实际需要使用的内存,堆内存指的是mono向操作系统申请的内存,两者的差值就是mono的空闲内存。当mono需要分配内存时,会先查看空闲内存是否足够,如果足够的话,直接在空闲内存中分配,否则mono会进行一次GC以释放更多的空闲内存,如果GC之后仍然没有足够的空闲内存,则mono会向操作系统申请内存,并扩充堆内存。

除了空闲内存不足时mono会自动调用GC外,也可以在代码中调用GC.Collect()手动进行GC,但是,GC本身是比较耗时的操作,而且由于GC会暂停那些需要mono内存分配的线程(C#代码创建的线程和主线程),因此无论是否在主线程中调用,GC都会导致游戏一定程度的卡顿,需要谨慎处理。另外,GC释放的内存只会留给mono使用,并不会交还给操作系统,因此mono堆内存是只增不减的。

游戏中大部分mono内存泄漏的情况都是由于静态对象的引用引起的,因此对于静态对象的使用需要特别注意,尽量少用静态对象,对于不再需要的对象将其引用设置为null,使其可以被GC及时回收。非静态对象引用也是一样的。



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

优化:
1,scene加载时,scene中的所有对象都将加载到内存中,包括scene中挂载了的脚本中赋值了的的材质,贴图,动画,声音等素材。这正是Unity的智能之处,不会再有额外的加载,这样的代价是内存占用将变多。在面向移动设备游戏的制作时,尽量减少在Hierarchy对资源的直接引用,而是使用Resource.Load的方法,在需要的时候从硬盘中读取资源,在使用后用Resource.UnloadAsset()和Resources.UnloadUnusedAssets()尽快将其卸载掉。
2,在scene切换时,所有资源将会被卸载掉,除非是被标记了DontDestroyOnLoad的资源,如果DontDestroyOnLoad了一个包含很多资源,比如大量贴图或者声音等大内存占用的东西,这部分资源在场景切换时无法卸载,将一直占用内存,这种情况应该尽量避免。
3,在scene切换时,如果脚本是被标记了DontDestroyOnLoad的资源,这些脚本很可能含有对其他物体的Component或者资源的引用,这样相关的资源就都得不到释放,另外,static的单例(singleton)在场景切换时也不会被摧毁,如果这种单例含有大量的对资源的引用,也会成为大问题。因此,尽量减少代码的耦合和对其他脚本的依赖是十分有必要的。如果确实无法避免这种情况,那应当手动地对这些不再使用的引用对象调用Destroy()或者将其设置为null。这样在垃圾回收的时候,这些内存将被认为已经无用而被回收。
4,在Resource.UnloadAsset()和Resources.UnloadUnusedAssets()时,只有那些真正没有任何引用指向的资源会被回收,因此请确保在资源不再使用时,将所有对该资源的引用设置为null或者Destroy。同样需要注意,这两个Unload方法仅仅对Resource.Load拿到的资源有效,而不能回收任何场景开始时自动加载的资源。与此类似的还有AssetBundle的Load和Unload方法,灵活使用这些手动自愿加载和卸载的方法,是优化Unity内存占用的不二法则。

unity的资源管理:

Resources.Load与AssetBundle:
Unity里有两种动态加载机制:一是Resources.Load,一是通过AssetBundle,其实两者本质上我理解没有什么区别。Resources.Load就是从一个缺省打进程序包里的AssetBundle里加载资源,而一般AssetBundle文件需要你自己创建,运行时动态加载,可以指定路径和来源的。其实场景里所有静态的对象也有这么一个加载过程,只是Unity后台替你自动完成了。
 

新版本unity5.x的资源管理:

Unity对资源的管理API汇总:
以下API是Unity5.x下几乎所有的对内存资源管理的接口,所有的内存管理方案也几乎都是用以上接口实现的。
1,Resources:Load、LoadAll、LoadAsync、UnloadAsset、UnloadUnusedAssets。
2,AssetBundle:LoadFromFile、LoadFromFileAsync、LoadFromMemory、LoadFromMemoryAsync、LoadAllAssets、LoadAllAssetsAsync、LoadAsset、LoadAssetAsync、Unload(true/false)。
3,GameObject:Instantiate、Destroy、DestroyImmediate。

文件的加载方式:
对Resource内文件的加载:Resources.Load、Resources.LoadAll、Resources.LoadAsync
支持包内加载的同步加载方式:AssetBundle.LoadFromeFile
常用的同步加载方式:AssetBundle.LoadFromFile、File.Open
常用的异步方式(都支持包内):AssetBundle.LoadFromFileAsync、new www、www.LoadFromCacheOrDownload
对于new www这种加载资源文件的方式,其优点是不会受到ios平台256文件句柄的限制,而其缺点是这种方式加载资源后所占的内存要比其他方式高。
而对于www.LoadFromCacheOrDownload这种方式,其加载后所占内存会比较低,在内存中是以SerializeFile的格式存在,但其会受到ios平台256文件句柄上限的限制。
不过在unity5.3之后,AssetBundle.LoadFromFileAsync和www.LoadFromCacheOrDownload如果超过句柄上限会自动转成new www的方式加载。

老版本unity的资源管理:

第一步:
创建assertBundle的内存镜像:
1,来自文件就用CreateFromFile(注意这种方法只能用于standalone程序)这是最快的加载方法。
2,也可以来自Memory,用CreateFromMemory(byte[]),这个byte[]可以来自文件读取的缓冲。
3,www的下载或者其他可能的方式。其实WWW的assetBundle就是内部数据读取完后自动创建了一个assetBundle而已。

这3个操作等于把硬盘或者网络的一个文件读到内存一个区域,这时候只是个AssetBundle内存镜像数据块,还没有Assets的概念。

几种AssetBundle创建方式的差异:
CreateFromFile:这种方式不会把整个硬盘AssetBundle文件都加载到内存来,而是类似建立一个文件操作句柄和缓冲区,需要时才实时Load,所以这种加载方式是最节省资源的,基本上AssetBundle本身不占什么内存,只需要Asset对象的内存。可惜只能在PC/Mac Standalone程序中使用。
CreateFromMemory和www.assetBundle:这两种方式AssetBundle文件会整个镜像于内存中,理论上文件多大就需要多大的内存,之后Load时还要占用额外内存去生成Asset对象。

释放:
先建立一个AssetBundle,无论是从www还是文件还是memory用AssetBundle.load加载需要的asset加载完后立即AssetBundle.Unload(false),释放AssetBundle文件本身的内存镜像,但不销毁加载的Asset对象。(这样你不用保存AssetBundle的引用并且可以立即释放一部分内存)。

AssetBundle.Unload(flase)是释放AssetBundle文件的内存镜像,不包含Load创建的Asset内存对象。这部分资源切换场景是不会释放的,只能使用AssetBundle.Unload(flase/true)来释放,用.net的术语,这种数据缓存是非托管的。

第二步:
创建assert:
用AssetBundle.Load这才会从AssetBundle的内存镜像里读取并创建一个Asset对象,创建Asset对象同时也会分配相应内存用于存放(反序列化)。异步读取用AssetBundle.LoadAsync。也可以一次读取多个用AssetBundle.LoadAll。

一个Prefab从assetBundle里Load出来里面可能包括:Gameobject transform mesh texture material shader script和各种其他Assets。你Load出来的Assets其实就是个数据源,用于生成新对象或者被引用,生成的过程可能是复制(clone)也可能是引用(指针)。

AssetBundle.Load(name): 从AssetBundle读取一个指定名称的Asset并生成Asset内存对象,如果多次Load同名对象,除第一次外都只会返回已经生成的Asset 对象,也就是说多次Load一个Asset并不会生成多个副本(singleton)。Resources.Load(path&name)也是一样,只是从默认的位置加载。

释放:
AssetBundle.Unload(true)是释放那个AssetBundle文件内存镜像和并销毁所有用Load创建的Asset内存对象。

如果你用个全局变量保存你Load的Assets,又没有显式的设为null,那在这个变量失效前你无论如何UnloadUnusedAssets也释放不了那些Assets的,因为一直存在引用关系。如果你这些Assets又不是从磁盘加载的(使用Resources.Load()加载的),那除了UnloadUnusedAssets或者加载新场景以外没有其他方式可以卸载之。

第三步:
Instaniate,创建GameObject:
Instaniate(AssetBundle.Load("MyPrefab"));  Destory(obj);
Load出来的Assets其实就是个数据源,用于生成新对象或者被引用,生成的过程可能是复制(clone)也可能是引用(指针)。Instaniate一个Prefab,是一个对Assets进行Clone(复制)+引用结合的过程,引用的Asset对象不会被复制,只是一个简单的指针指向已经Load的Asset对象。你可以再Instaniate一个同样的Prefab,这时候会创建新的GameObject,然后clone:GameObject transform这些资源,继续引用:mesh/texture/material/shader这些资源,或者是引用+复制同时存在。当你Destroy一个实例时,只是释放那些Clone对象,并不会释放引用对象和Clone的数据源对象(也就是Resources.UnloadUnusedAssets来释放的那块内存),Destroy并不知道是否还有别的object在引用那些对象。

Instantiate(object):Clone 一个object的完整结构,包括其所有Component和子物体,浅Copy,并不复制所有引用类型。有个特别用法,虽然很少这样用,其实可以用Instantiate来完整的拷贝一个引用类型的Asset,比如Texture等,要拷贝的Texture必须类型设置为Read/Write able。

Clone(新生成的):GameObject,transform
纯引用:Texture,TerrainData,shader
引用+复制:Mesh,material,PhysicMaterial
特殊的复制+引用:Script Asset,数据区是复制的,代码区是共享的,算是一种特殊的复制+引用关系。

3种加载方式:
一是静态引用,建一个public的变量,在Inspector里把prefab拉上去,用的时候instantiate
二是Resource.Load,Load以后instantiate
三是AssetBundle.Load,Load以后instantiate
三种方式有细节差异,前两种方式,引用对象texture是在instantiate时加载,而assetBundle.Load会把perfab的全部assets都加载,instantiate时只是生成Clone。所以前两种方式,除非你提前加载相关引用对象,否则第一次instantiate时会包含加载引用assets的操作,导致第一次加载的迟延。

释放:
如果有Instantiate的对象,用Destroy进行销毁在合适的地方调用Resources.UnloadUnusedAssets,释放已经没有引用的Asset。如果需要立即释放内存加上GC.Collect(),否则内存未必会立即被释放,有时候可能导致内存占用过多而引发异常。

使用Resources.UnloadUnusedAssets来释放没用的assets,Destroy不能完成这个任务,AssetBundle.Unload(false)也不行,AssetBundle.Unload(true)可以但不安全,除非你很清楚没有任何对象在用这些Assets了。


所有的释放方式汇总:
Destroy: 主要用于销毁克隆对象,也可以用于场景内的静态物体,不会自动释放该对象的所有引用。虽然也可以用于Asset,但是概念不一样要小心,如果用于销毁从文件加载的Asset对象会销毁相应的资源文件!但是如果销毁的Asset是Copy的或者用脚本动态生成的,只会销毁内存对象。
AssetBundle.Unload(false):释放AssetBundle文件内存镜像
AssetBundle.Unload(true):释放AssetBundle文件内存镜像同时销毁所有已经Load的Assets内存对象
Reources.UnloadAsset(Object):显式的释放已加载的Asset对象,只能卸载磁盘文件加载的Asset对象
Resources.UnloadUnusedAssets:用于释放所有没有引用的Asset对象
GC.Collect():强制垃圾收集器立即释放,内存Unity的GC功能不算好,没把握的时候就强制调用一下

系统在加载新场景时,所有的内存对象都会被自动销毁,包括你用AssetBundle.Load加载的对象和Instaniate克隆的。但是不包括AssetBundle文件自身的内存镜像,那个必须要用Unload来释放,用.net的术语,这种数据缓存是非托管的。


 

其他:

1,Destroy与DestroyImmediate:
Destroy():表示移除物体或物体上的组件,代表销毁该物体,实际上该物体的内存并没有立即释放,因为有延迟删除机制在。U3D的Destroy是只销毁当前场景的prefab对象的,而对象调用的mesh和tex仍然驻留在内存并没有被立即销毁,Instantiate后,mesh和tex不会复制出来,还是在原来的内存里并被引用,反复的Destroy和Instantiate会导致有过多的prefab对象的删除和新建,Unity调用的资源是由unity自己的内部回收机制管理。

Destroy是异步销毁,不会影响主线程的运行。被Destroy的游戏对象不会发生类似List中移除对象的情况,销毁了第0个子物体,第1个子物体还在原来的位置,程序不能立即检测到到它被销毁了,实际对象销毁始终延迟到当前Update循环之后,但始终在渲染之前完成。如果想实现List一样的效果,那么需要使用DestroyImmediate这个方法 。list对象被删除时,list的count会立刻自动减一。

DestroyImmediate():立即销毁,立即释放资源,做这个操作的时候,会消耗很多时间的,影响主线程运行 。destroyimadiate是立即将物体从场景hierachy中移除,并标记为 "null",注意 是带引号的null。这是UNITY内部的一个处理技巧。关于这个技巧有很争议。destroy要等到帧末才会将物体从场景层级中移除并标记为"null"。Destroy(gameobject)之后,你的gameobject==null的结果就为true,此时的gameobject是一个null。适用在自定义窗口里面,因为那个里面没有延迟删除机制。
 

2,一些知识:
1,由于Unity不开源,Unity对内存的管理方式,官方文档中并没有太多的说明,基本需要依靠自己摸索。
2,虽然Unity标榜自己的内存使用全都是“Managed Memory”,但是事实上你必须正确地使用内存,以保证回收机制正确运行。比如:没有用的GameObject手动删掉。
3,一个空的Unity默认场景,什么代码都不放,在iOS设备上占用内存应该在17MB左右,而加上一些自己的代码很容易就飙到20MB左右。
4,Unity3D对于面向移动设备的游戏开发,动辄内存占用飙上一两百兆。


 

猜你喜欢

转载自blog.csdn.net/tran119/article/details/81776899
今日推荐