【TDR】 Unity3D UI性能优化

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/sinat_33442459/article/details/86639581

1】基础知识分享:

为何NGUI加载资源速度慢?

 

-->>Resources.Load

    Resources.Load是一个“同步”耗时操作,Unity内部维护了资源的内存池,但是调用Load的时候Unity会自动的把Prefab上所引用的资源再加入内存池,它不会重复加载资源。即当你加载相同的UIAtlas的时候,只会第一次比较卡。所以Unity的很多游戏一般第一次打开某界面的时候要比以后打开此界面时间长一些。

 

-->>GameObject.Instantiate

    事实上Instantiate()时间是非常快的。

 

-->>第一次添加脚本

    添加脚本会有两种形式。第一种是通过AddComponent<Script>的形式吧脚本添加给游戏对象,还有一种就是你的Prefab天生就带着这个脚本。无论哪种加载时间都是一样的。第一次加载脚本要比以后的慢,这个和Resource缓存池的原理一样。

 

-->>第二次以后的GameObject.InstantiateAddComponent<Script>

    GameObject.Instantiate的加载时间很快,主要详细讨论Script

    导致你界面打开慢的原因就是prefab上绑定的脚本,罪魁祸首就是脚本。

    AddComponent<Script>以后或者Prefab上预先绑定的脚本。当你GameObject.Instantiate()同步方法执行的时候,并不是把脚本挂上去就结束了,而它要等脚本里面的一些方法执行完毕才算结束。脚本中有两个典型的方法Awake和OnEnable。当Prefab用Instantiate()方法载入的时候,它的脚本必须执行完Awake和OnEnable两个方法以后才算完整载入。如果你的脚本这里面有一些耗时的操作,载入必然就会变慢。

    如果你在Awake()或者OnEnable()方法里面继续去实例化对象,继续绑定脚本,那么依然需要把新实例化对象的Awake()和OnEnable()方法执行完毕才会结束。

    这里还没有完,还有一个地方会引起打开界面慢。代码中用Public声明的对象,然后是在编辑器拖拽赋值。拖拽赋值,如果是资源很大的话Unity需要Load,然而Load就是一个同步耗时的操作,它也会影响打开界面的时间。

   

    如上图所示,NGUI里面UITexture ,UISprite,UILabel,这三个脚本上面都有public绑定的对象。NGUI打开界面慢的罪魁祸首就在这里,当尝试把public绑定的代码全部取消,发现20ms就载入完成了。。。知道原因,但是我们也毫无办法,我们无法随意更改其源码。

注意:

    一定要把一个界面的所有GameObject做成一个Prefab,有些人不想用Unity的Prefab,想通过一种规则程序运行时利用GameObject.Instantiate()和AddComponent<Script>来生成界面的树状结构。经过测试如果单纯加载一个Prefab和代码动态生成对应树状结构,前者要比后者快30%左右。所以如果做UI编辑器的话,一定要先把Prefab生成出来,一定要只加载一个Prefab。

    Unity的Sprite载入是要比NGUI的Sprite载入快,唯一可能的解释就是Unity底层用的是C语言,而NGUI用的是纯C#,所以执行效率上C语言会快很多。

 

2{拓展}关于Unity3D占用内存太大的解决方法【可跳过阅读】:

AssetBundle运行时加载:

    来自文件就用CreateFromFile(注意这种方法只能用于standalone程序),这是最快的加载方法。

    来自Memory,用CreateFromMemory(Byte[]),这个byte[]可以来自文件读取的缓冲,www的下载或者其他可能的方式。其实WWW的assetBundle就是内部数据读取完后自动创建了一个assetBundle而已。

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

 

Assets加载:

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

 

AssetBundle的释放:

    AssetBundle.Unload(false)是释放AssetBundle文件的内存镜像,不包含Load创建的内存对象。

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

 

Load资源过程:

    一个PrefabassetBundleLoad出来 里面可能包括:Gameobject transform mesh texture material shader script和各种其他Asset。你 Instaniate一个Prefab,是一个对Assets进行Clone(复制)+引用结合的过程,GameObject transform Clone是新生成的。其他mesh / texture / material / shader 等,这其中些是纯引用的关系的,包括:TextureTerrainData,还有引用和复制同时存在的包括:Mesh/material /PhysicMaterial。引用的Asset对象不会被复制,只是一个简单的指针指向已经LoadAsset对象。

 

特殊资源Script Asset

    看起来很奇怪,Unity里每个Script都是一个封闭的Class定义而已,并没有写调用代码,光Class的定义脚本是不会工作的。其 实Unity引擎就是那个调用代码,Clone一个script asset等于new一个class实例,实例才会完成工作。把他挂到Unity主线程的调用链里去,Class实例里的OnUpdate OnStart等才会被执行。多个物体挂同一个脚本,其实就是在多个物体上挂了那个脚本类的多个实例而已,这样就好理解了。在new class这个过程中,数据区是复制的,代码区是共享的,算是一种特殊的复制+引用关系。

 

Load资源总结: 

    你可以再Instaniate一个同样的Prefab,还是这套mesh/texture/material/shader...,这时候会有新的GameObject等,但是不会创建新的引用对象比如Texture.所以你Load出来的Assets其实就是个数据源,用于生成新对象或者被引用,生成的过程可能是复制(clone)也可能是引用(指针)。

注意:

    当你Destroy一个实例时,只是释放那些Clone对象,并不会释放引用对象和Clone的数据源对象,Destroy并不知道是否还有别的object在引用那些对象。等到没有任何 游戏场景物体在用这些Assets以后,这些assets就成了没有引用的游离数据块了,是Unused-Assets了,这时候就可以通过 Resources.UnloadUnusedAssets来释放,Destroy不能完成这个任 务,AssetBundle.Unload(false)也不行,AssetBundle.Unload(true)可以但不安全,除非你很清楚没有任何 对象在用这些Assets了。

 

各种加载和初始化用法总结:

AssetBundle.CreateFrom…:

    创建一个AssetBundle内存镜像,注意同一个assetBundle文件在没有Unload之前不能再次被使用。

WWW.AssetBundle:

    同上,当然要先new一个再yield return 然后才能使用。

AssetBundle.Load(name):

    从AssetBundle读取一个指定名称的Asset并生成Asset内存对象,如果多次Load同名对象,除第一次外都只会返回已经生成的Asset对象。

Resource.Load(path%name):

    同上,只是从默认的位置加载

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

 

各种释放方法总结:

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功能不算好,没把握的时候就强制调用一下。

 

举两个例子帮助理解:

1

一个常见的错误:你从某个AssetBundle里Load了一个prefab并克隆之:

obj = Instaniate(AssetBundle1.Load('MyPrefab”);这个prefab比如是个npc

然后你不需要他的时候你用了:Destroy(obj);你以为就释放干净了。

其实这时候只是释放了Clone对象,通过Load加载的所有引用、非引用Assets对象全都静静静的躺在内存里。

这种情况应该在Destroy以后用:AssetBundle1.Unload(true),彻底释放干净。

如果这个AssetBundle1是要反复读取的 不方便Unload,那可以在Destroy以后用:Resources.UnloadUnusedAssets()把所有和这个npc有关的Asset都销毁。

当然如果这个NPC也是要频繁创建 销毁的 那就应该让那些Assets呆在内存里以加速游戏体验。

由此可以解释另一个之前有人提过的话题:为什么第一次Instaniate 一个Prefab的时候都会卡一下,因为在你第一次Instaniate之前,相应的Asset对象还没有被创建,要加载系统内置的 AssetBundle并创建Assets,第一次以后你虽然Destroy了,但Prefab的Assets对象都还在内存里,所以就很快了。

2

从磁盘读取一个1.unity3d文件到内存并建立一个AssetBundle1对象

AssetBundle AssetBundle1 = AssetBundle.CreateFromFile("1.unity3d");

从AssetBundle1里读取并创建一个Texture Asset,把obj1的主贴图指向它

obj1.renderer.material.mainTexture = AssetBundle1.Load("wall") as Texture;

把obj2的主贴图也指向同一个Texture Asset

obj2.renderer.material.mainTexture =obj1.renderer.material.mainTexture;

Texture是引用对象,永远不会有自动复制的情况出现(除非你真需要,用代码自己实现copy),只会是创建和添加引用

如果继续:

AssetBundle1.Unload(true) 那obj1和obj2都变成黑的了,因为指向的Texture Asset没了

如果:

AssetBundle1.Unload(false) 那obj1和obj2不变,只是AssetBundle1的内存镜像释放了

继续:

Destroy(obj1),//obj1被释放,但并不会释放刚才Load的Texture

如果这时候:Resources.UnloadUnusedAssets();不会有任何内存释放 因为Texture asset还被obj2用着

如果Destroy(obj2),obj2被释放,但也不会释放刚才Load的Texture

继续

Resources.UnloadUnusedAssets();这时候刚才load的Texture Asset释放了,因为没有任何引用了

最后CG.Collect();强制立即释放内存。

 

Unity几种动态加载Prefab方式的差异 :

其实存在3种加载prefab的方式:

一是静态引用,建一个public的变量,在Inspector里把prefab拉上去,用的时候instantiate

二是Resource.Load,Load以后instantiate

三是AssetBundle.Load,Load以后instantiate

三种方式有细节差异,前两种方式,引用对象texture是在instantiate时加载,而assetBundle.Load会把perfab的全部 assets都加载,instantiate时只是生成Clone。所以前两种方式,除非你提前加载相关引用对象,否则第一次instantiate时会 包含加载引用类assets的操作,导致第一次加载的lag。官方论坛有人说Resources.Load和静态引用是会把所有资源都预先加载的,反复测试的结果,静态引用和Resources.Load也是OnDemand的,用到时才会加载

 

几种AssetBundle创建方式的差异:

CreateFromFile:

    这种方式不会把整个硬盘AssetBundle文件都加载到内存来,而是类似建立一个文件操作句柄和缓冲区,需要时才实时Load,所以这种资源加载方式是最节省资源的,基本上AssetBundle本身不占什么内存,只需要Asset对象的内存。可惜只能在PC/Mac Standalone程序中使用。

CreateFromMemory和www.assetBundle:

    这两种方式Assetbundle文件会整个镜像于内存中,理论上文件多大就需要多大的内存,之后Load时还要额外内存去生成Asset对象。

 

什么时候才是UnusedAssets ?

Object obj = Resources.Load("MyPrefab");  

GameObject instance = Instantiate(obj) as GameObject;  

......  

Destroy(instance);  

创建后随后销毁了一个Prefab实例,这个时候MyPrefab已经没有被实际的物体引用了,但如果这时:

Resources.UnloadUnusedAssets();

内存并没有被释放,原因:MyPrefab还被这个变量obj所引用

这时候:

Obj = null ;

Resources.UnloadUnusedAssets();

这样才能真正释放Assets对象

所以:UnusedAssets不但要没有被实际物体引用,也要没有被生命周期内的变量所引用,才可以理解为Unused(引用计数为0)

注意:

    如果你用全局变量保存你Load的Assets,又没有显示的设为NULL,那么在这个变量失效前你无论如何UnloadUnusedAssets也释放不了那些Assets。如果你这些Assets又不是从磁盘加载的,那除了UnloadUnusedAssets或者加载新场景以外没有其他方式可以卸载之。

 

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的文档也有说明。实际上,在游戏开发中绝大多数被剥离的功能使用不上的,因此不管如何,库剥离的优化方法都值得一试。

扩展:【优化代码

Unity3D是用C++写的,而我们的代码是用C#作为脚本来写的,那么问题就来了~脚本和底层的交互开销是否需要考虑呢?也就是说,我们用Unity3D写游戏的“游戏脚本语言”,也就是C#是由mono运行时托管的。而功能是底层引擎的C++实现的,“游戏脚本”中的功能实现都离不开对底层代码的调用。那么这部分的开销,我们应该如何优化呢?

   2.如上所述,最好不要频繁使用GetComponent,尤其是在循环中。

3.善于使用OnBecameVisible()和OnBecameVisible(),来控制物体的update()函数的执行以减少开销。

4.使用内建的数组,比如用Vector3.zero而不是new Vector(0, 0, 0);

5.对于方法的参数的优化:善于使用ref关键字。值类型的参数,是通过将实参的值复制到形参,来实现按值传递到方法,也就是我们通常说的按值传递。复制嘛,总会让人感觉很笨重。比如Matrix4x4这样比较复杂的值类型,如果直接复制一份新的,反而不如将值类型的引用传递给方法作为参数。

 

托管堆优化:

    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()来建议系统进行一次垃圾回收。要注意的是这里的调用真的仅仅只是建议,可能系统会在一段时间后在进行回收,也可能完全不理会这条请求,不过在大部分时间里,这个调用还是靠谱的。

扩展:【处理内存,却让CPU受伤的GC】

虽然GC是用来处理内存的,但的确增加的是CPU的开销。因此它的确能达到释放内存的效果,但代价更加沉重,会加重CPU的负担,因此对于GC的优化目标就是尽量少的触发GC。

首先我们要明确所谓的GC是Mono运行时的机制,而非Unity3D游戏引擎的机制,所以GC也主要是针对Mono的对象来说的,而它管理的也是Mono的托管堆。 搞清楚这一点,你也就明白了GC不是用来处理引擎的assets(纹理啦,音效啦等等)的内存释放的,因为U3D引擎也有自己的内存堆而不是和Mono一起使用所谓的托管堆。

其次我们要搞清楚什么东西会被分配到托管堆上?不错咯,就是引用类型咯。比如类的实例,字符串,数组等等。而作为int,float,包括结构体struct其实都是值类型,它们会被分配在堆栈上而非堆上。所以我们关注的对象无外乎就是类实例,字符串,数组这些了。

那么GC什么时候会触发呢?两种情况:

所以为了达到优化CPU的目的,我们就不能频繁的触发GC。而上文也说了GC处理的是托管堆,而不是Unity3D引擎的那些资源,所以GC的优化说白了也就是代码的优化。那么我觉得有以下几点是需要注意的:

  • 字符串连接的处理。因为将两个字符串连接的过程,其实是生成一个新的字符串的过程。而之前的旧的字符串自然而然就成为了垃圾。而作为引用类型的字符串,其空间是在堆上分配的,被弃置的旧的字符串的空间会被GC当做垃圾回收。
  • 尽量不要使用foreach,而是使用for。foreach其实会涉及到迭代器的使用,而据传说每一次循环所产生的迭代器会带来24 Bytes的垃圾。那么循环10次就是240Bytes。
  • 不要直接访问gameobject的tag属性。比如if (go.tag == “human”)最好换成if (go.CompareTag (“human”))。因为访问物体的tag属性会在堆上额外的分配空间。如果在循环中这么处理,留下的垃圾就可想而知了。
  • 使用“池”,以实现空间的重复利用。
  • 最好不用LINQ的命令,因为它们会分配临时的空间,同样也是GC收集的目标。而且我很讨厌LINQ的一点就是它有可能在某些情况下无法很好的进行AOT编译。比如“OrderBy”会生成内部的泛型类“OrderedEnumerable”。这在AOT编译时是无法进行的,因为它只是在OrderBy的方法中才使用。所以如果你使用了OrderBy,那么在IOS平台上也许会报错。

本机堆的优化:

    当你加载完成一个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内存占用的不二法则~

3NGUI Prefab优化:

优化之前情况和数据:

1】格斗家专属系统

 

由上图可以看出,格斗家专属系统在优化前Prefab大小为1295KB,资源加载时间为430ms左右,初始化结束后时间为470ms左右。加载资源非常耗时。

 

2】格斗家专属详情系统

  由上图可以看出,格斗家专属详情系统在优化前Prefab大小为1639KB,资源加载时间为450ms左右,初始化结束后时间为500ms左右。加载资源非常耗时。

3】格斗家历练系统(特训)

由上图可以看出,格斗家历练系统在优化前Prefab大小为9779KB,Prefab之所以这么大是因为该系统Prefab里面添加了大量的特效,特效比较占内存。资源加载时间为680ms左右,初始化结束后时间为800ms左右。加载资源非常耗时。并且我们可以看出,特效越大,加载时间也相应变长。

 

进行了哪些优化?

    针对特效非常占内存的情况,我决定删除Preafab中的预制特效,采用脚本动态加载Prefab中的特效,这样可以极大的降低Prefab的大小,减少Prefab加载时间。

步骤:

    【1】在Assets/Editor中添加了一个脚本,主要用来检测当前项目中有哪些Prefab的Size是大于500KB的。打开Unity,点击菜单栏Tools/Check Prefab Size

由上图可知,当前项目中有58个系统的Prefab大小超过了500KB。

    2】在Assets\Src\UI\Common目录下添加一个脚本文件“DynamicLoadEffects.cs”

             在Assets\Src\UI\Common\Editor目录下添加一个脚本文件“ChangeEffectLoadType.cs”

这两个脚本主要用来动态加载特效,其中通过ChangeEffectLoadType.cs脚本设置SerializedObject 对象,方便玩家直接在编辑器中修改资源加载类型,加载的特效名称,资源的状态以及异步加载延迟时间。

在你当前需要放置特效的位置添加一个和你特效同名的空的GameObject,给当前空的GameObject挂上脚本文件“DynamicLoadEffects.cs”,选择加载类型,填上特效的名称。Status决定当前特效是否打开,Async Time是异步加载时间。

注意:

    【1】这里选择将该GameObject的名称设置为特效的名称是为了满足某些系统可能存在打开了当前的上层节点,但是关闭了下一层节点的特效。

    2】如果特效本身带了UI Render Queue则无需操作,如果需要给特效添加 UI Render Queue,则直接将该UI Render Queue添加到当前创建的与特效同名的GameObject上。

    当启动游戏后,进入系统界面的时候就可以看到加载的特效了。

优化之后情况和数据:

1】格斗家专属系统

由上图可以看出,格斗家专属系统在优化后Prefab大小为195KB,极大的缩小了Prefab的大小,资源加载时间也比优化前减少了130ms左右,但是优化后加载Prefab仍然比较耗时。

 

2】格斗家专属详情系统

由上图可以看出,格斗家专属系统在优化后Prefab大小为229KB,极大的缩小了Prefab的大小,资源加载时间也减小了很多,但是优化后加载Prefab仍然比较耗时。

 

3】格斗家历练系统(特训)

由上图可以看出,格斗家专属系统在优化后Prefab大小为497KB,极大的缩小了Prefab的大小,资源加载时间也减小了很多,但是优化后加载Prefab仍然比较耗时。

总结

    由优化过后的数据可以得出,不再预制特效确实可以极大的缩减Prefab的大小,也可以缩减资源加载时间,但是加载时间并没有呈现质的飞跃。UI性能仍然待优化。

 

下面对UIAtlas进行测试:

1Prefab中仅有一个Sprite

由上图可知,我只测试了一个Sprite,仅加载了一个UIAtlas,该Atlas 尺寸为2048 x 256,大小为2.7MB,加载时间为25ms。

 

2Prefab中仅有一个SpriteIts different form the first sprite and both their UIAtlas are also different

由上图可知,我测试了另一个Sprite,仅加载了一个UIAtlas,该Atlas 尺寸为2048 x 1025,大小为8.1MB,加载时间为26ms。

 

3Prefab中仅有一个Lable

由上图可知,我测试了一个UILabel,仅加载了一个字体集,加载时间为25ms。

 

4Prefab中有10个相同的Sprite

由上图可知,我测试了10个相同的Sprite,仅加载了一个UIAtlas,该Atlas 尺寸为2048 x 1025,大小为8.1MB,加载时间为49ms。由加载时间我们可以看出,10个相同的UISprite的加载时间并不会成线性翻倍,这是因为Unity会自动的把Prefab上所引用的资源加入内存池,它并不会重复加载资源。

 

5Prefab中有2个不同的Sprite

由上图可知,我测试了2个不同的Sprite,仅加载了2个不同的UIAtlas,加载时间为45ms,几乎等同于单独加载2个不同的Sprite时间之和

 

6Prefab中有3个不同的Sprite

由上图可知,我测试了3个不同的Sprite,仅加载了3个不同的UIAtlas,加载时间为60ms,几乎等同于单独加载3个不同的Sprite时间之和

 

7Prefab中有3个不同的Sprite x2

 

由上图可知,我测试了2倍的3个不同的Sprite,仅加载了3个不同的UIAtlas,加载时间为80ms,加载时间并没有呈现线性叠加,这是因为Unity并不会重复加载资源。

 

8Prefab中有5个不同的Sprite

由上图可知,我测试了5个不同的Sprite,仅加载了5个不同的UIAtlas,加载时间为95ms,几乎等同于单独加载5个不同的Sprite时间之和

 

9Prefab中有5个不同的Sprite x2

由上图可知,我测试了2倍的5个不同的Sprite,仅加载了5个不同的UIAtlas,加载时间为114ms,加载时间并没有呈现线性叠加,这是因为Unity并不会重复加载资源。

 

10Prefab中有5个不同的Sprite x5

由上图可知,我测试了5倍的5个不同的Sprite,仅加载了5个不同的UIAtlas,加载时间为120ms,加载时间并没有呈现线性叠加,这是因为Unity并不会重复加载资源。

 

专属系统资源依赖关系:

 

专属详情系统资源依赖关系:

 

历练特训系统资源依赖关系:

 

 

总结:

    项目中的Sprite应当尽量集中在一个或者几个UIAtlas中,切忌项目中不要出现许多Sprite或Label都在不同的子集中,这会极大的增加资源加载的时间,尽量保持项目的独立性,避免与其他系统产生过多的耦合。

4Unity3D UI性能优化:

图形引擎渲染画面的过程:

Unity(或者说基本所有图形引擎)生成一帧画面的处理过程大致可以这样简化描述:

 

1. 可见性测试

     引擎首先经过简单的可见性测试,确定摄像机可以看到的物体。

 

2. 准备好物体的数据

    然后把这些物体的顶点(包括本地位置、法线、UV等),索引(顶点如何组成三角形),变换(就是物体的位置、旋转、缩放、以及摄像机位置等),相关光源,纹理,渲染方式(由材质/Shader决定)等数据准备好。

 

3. 通知图形API开始绘制

    然后通知图形API——或者就简单地看作是通知GPU——开始绘制,GPU基于这些数据,经过一系列运算,在屏幕上画出成千上万的三角形,最终构成一幅图像。

 

什么是Draw Call

    在Unity中,每次引擎准备数据并通知GPU的过程称为一次Draw Call。

    这一过程是逐个物体进行的,对于每个物体,不止GPU的渲染,引擎重新设置材质/Shader也是一项非常耗时的操作。因此每帧的Draw Call次数是一项非常重要的性能指标,对于iOS来说应尽量控制在20次以内,这个值可以在编辑器的Statistic窗口看到。

 

Draw Call Batching 技术

    Unity内置了Draw Call Batching技术,从名字就可以看出,它的主要目标就是在一次Draw Call中批量处理多个物体。只要物体的变换和材质相同,GPU就可以按完全相同的方式进行处理,即可以把它们放在一个Draw Call中。

 

Draw Call Batching的核心:

    Draw Call Batching技术的核心就是在可见性测试之后,检查所有要绘制的物体的材质,把相同材质的分为一组(一个Batch),然后把它们组合成一个物体(统一变换),这样就可以在一个Draw Call中处理多个物体了(实际上是组合后的一个物体)。

 

Draw Call Batching的缺陷:

    但Draw Call Batching存在一个缺陷,就是它需要把一个Batch中的所有物体组合到一起,相当于创建了一个与这些物体加起来一样大的物体。与此同时就需要分配相应大小的内存。这不仅会消耗更多内存,还需要消耗CPU时间。特别是对于移动的物体,每一帧都得重新进行组合,但对于静止不动的物体来说,只需要进行一次组合,之后就可以一直使用,效率要高得多。这就需要进行一些权衡,否则得不偿失。

 

Dynamic Batching和Static Batching

Unity提供了Dynamic Batching和Static Batching两种方式。

 

Dynamic Batching:

Dynamic Batching是完全自动进行的,不需要也无法进行任何干预。对于顶点数在300以内的可移动物体,只要使用相同的材质,就会组成Batch

 

Static Batching:

Static Batching则需要把静止的物体标记为Static,然后无论大小,都会组成Batch。如前文所说,Static Batching显然比Dynamic Batching要高效得多。

 

高效利用Draw Call Batching:

    要有效利用Draw Call Batching,有以下注意点:

    首先是尽量减少场景中使用的材质数量,即尽量共享材质,对于仅纹理不同的材质可以把纹理组合到一张更大的纹理中(称为Texture Atlasing)。然后是把不会移动的物体标记为Static。此外还可以通过CombineChildren脚本(Standard Assets/Scripts/Unity Scripts/CombineChildren)手动把物体组合在一起,但这个脚本会影响可见性测试,因为组合在一起的物体始终会被看作一个物体,从而会增加GPU要处理的几何体数量,因此要小心使用。对于复杂的静态场景,还可以考虑自行设计遮挡剔除算法,减少可见的物体数量同时也可以减少Draw Call。

    总之,理解Draw Call和Draw Call Batching原理,根据场景特点设计相应的方案来尽量减少Draw Call次数才是王道,其它方面亦然。

 

Tips:

提醒:

1、批处理动态物体需要在每个顶点上进行一定的开销,所以动态批处理仅支持小于900顶点的网格物体。

2、如果你的着色器使用顶点位置,法线和UV值三种属性,那么你只能批处理300顶点以下的物体;如果你的着色器需要使用顶点位置,法线,UV0,UV1和切向量,那你只能批处理180顶点以下的物体。请注意:属性数量的限制可能会在将来进行改变。

3、不要使用缩放尺度(scale)。分别拥有缩放尺度(1,1,1)和(2,2,2)的两个物体将不会进行批处理。 

4、统一缩放尺度的物体不会与非统一缩放尺度的物体进行批处理。使用缩放尺度(1,1,1)和 (1,2,1)的两个物体将不会进行批处理,但是使用缩放尺度(1,2,1)和(1,3,1)的两个物体将可以进行批处理。 

5、使用不同材质的实例化物体(instance)将会导致批处理失败。

6、拥有lightmap的物体含有额外(隐藏)的材质属性,比如:lightmap的偏移和缩放系数等。所以,拥有lightmap的物体将不会进行批处理(除非他们指向lightmap的同一部分)。

7、多通道的shader会妨碍批处理操作。比如,几乎unity中所有的着色器在前向渲染中都支持多个光源,并为它们有效地开辟多个通道。

8 、预设体的实例会自动地使用相同的网格模型和材质。

 

理解DrawCall

    开发游戏时,一定被时时提醒要减少 Draw Call,当然Unity也不例外,打开Game 窗口裡的 Stats,可以看到 Draw Call 与 Batched 的数字。

“一个 Draw Call,等于呼叫一次 DrawIndexedPrimitive (DX) or glDrawElements (OGL),等于一个 Batch”

    摸过 DirectX 或 OpenGL 的人来说,对 DrawIndexedPrimitive 與 glDrawElements 这 API 一定不陌生。当我们准备好资料 (通常为三角面的顶点资料) 要 GPU 划出来时,一定得调用这个函数。

 

举例说明:

    换句话说,如果在画面上有一张 “木" 椅子、一张 “铁" 桌子,那理论上就会有两个 Draw Call。有看到特別指出 “木" 与 “铁" 吗?这代表两物件是使用不同材质球或者不同的 Shader。在 DirectX 或 OpenGL 里,对不同物件指定不同贴图或不同 Shader 的描述,就会需要呼叫两次Draw Call。

 

举例代码:

每次对 Shader 的更改或者贴图的更改,基本上就是对 Rendering Pipeline 的設定做修改,所以需要不同的 Draw Call 來完成物件的绘制。現在了解为什麼 UNITY 官方文件里,老是要你尽量使用同样材质球,以减少 Draw Call 数量了吧!

 

4-1 DrawCall优化:

    DrawCall可以通过点开Stats和Window->Frame Debugger查看具体信息。

在进行drawcall优化的时候,我们可以通过,移动Frame Debug的左侧栏,然后观察drawcall数量的变化,从而找到那部分ui的drawcall数量不正常。

 

1mask的多次使用:

    mask对于drawcall的影响应该是所有组件里面最大的。每存在一个mask,就把mask以内和以外的UI分割成两个“世界”,依次计算两个“世界”的drawcall,然后再相加。原因是mask以内和以外的UI不能通过unity3D一次渲染(batch)

所以在使用mask的时候要仔细思考,能不用就不用,实在要用可以考虑用带通道的图片代替mask的遮罩功能。

 

2】图集整理不规范:

    影响drwacall数量的根本是batch(批处理数),而batch是根据一个一个图集来进行批处理的。简单来说,两张Sprite重叠在一起,当两张Sprite是一个图集里的时候,这两个Sprite就是一个batch;当不是一个图集的时候,两个Sprite就变成了两个batch。所以在处理图集的时候,通常的做法是,常用的图片放在一个共有图集里,然后独立界面的图片放在一个图集,一个UI最好控制在2-3个图集。

 

3】图文交叉:

    Unity3D NGUI的batch规则除了依赖图集以外,还依赖于组件关系。当2张图片(同一图集),1个文字进行重叠时,处理不好会发生一些drawcall的多余。比如:Sprite->Sprite->Label,这样的话,drawcall就是两个,但是当:Sprite->Label->Sprite的时候,就算两张Sprite是一个图集,这样的drawcall都会有3个。所以尽量不要出现Sprite->Label->Sprite,图文交叉的情况。根据第二点,有个小小的Tips,当存在两个图集的三张image的时候,也尽量不要出现Sprite1->Sprite2->Sprite1这样的操作。

 

4UI层级的深度:

    在不必要的情况下,我们应当尽量减少UI层级的深度,在NGUI中Hierarchy面板,节点的深度,表现的就是UI层级的深度,我们UI中有N层,N越大越靠前,会遮住后面的组件。当深度越深,不处在同一层级的UI就越多,drwacall就会越大。

 

4-2 动静分离的优化:

    当我们在制作UI的时候,我们应该考虑到整个UI,哪个部分处于经常变化的部分,哪个部分属于不长变化的部分,把长变化的归到动态区域,把不长变化的归到静态区域。

    以一个游戏的主界面为例:(该图来源于网络,仅做展示用)

 

把界面简单的规划成了4个区域:

    1.上方按钮区域

    2.右方按钮区域

    3.技能区域

    4.人物头像及任务区域

    如果按照简单的动静分离原因:1.2区域是不常变化的,我们放在一个节点以下;3.4区域是常变化的,几乎每一秒都在变化,我们放在一个节点以下。这样就可以达到动静分离的效果。

    动静分离,可以减少UIMesh动态更新,在某些比较复杂,常驻的界面可以这样优化。小的界面就没必要了,因为不必要的节点有可能会造成drawcall的增加的。

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

猜你喜欢

转载自blog.csdn.net/sinat_33442459/article/details/86639581