unity 内存优化

          找工作之余又开始扯淡了,讲内存优化其实跟unity好像并没有半毛钱的关系,纯粹是一些.net框架的事,先将几个概念有助于我们理解unity和.net.Monos就像是一个神奇的酱汁,,混合成Unity平台这个食谱,并且赋予了它跨平台的能力。 Mono是一个开源的项目,基于API(应用程序编程接口),说明书和来自微软.NET框架的通用库工具建立起了它自己(Mono)的框架和库.但是却几乎不能对源代码进行访问。注意,尽管Mono库建立在开源的微软娱乐基础NET类库上,但是它(Mono)完全的兼容了基础的微软库。Mono项目的目标是提供使用NET框架作为通用层,提供一个允许跨平台兼容的框架.它将允许使用一门普通编程语言编写的应用程序,却能运行在许多不同的硬件平台上,包括Linux,OS X,Windows,ARM,个人电脑甚至更多不同设备.Mono也支持许多不同的编程语言,不仅仅是C#,Boo和UnityScripts,甚至是更多我们所熟悉的编程语言都可以支持。 DotNET框架的纯通用中间语言(CLR-稍后描述)也能充分的整合在Mono平台上,这包括C#,但是也包括类似于F#,Java,VB.NET,PythonNet和IronPython。大家可能觉得unity引擎是建立在mono上的,这是一个普遍的错误。mono在一方面并不参与处理某些比如导入游戏资源的任务,比如导入audio,rendering,physics等。所以得出的结论就是unity技术是建立在纯在的c++背后,目的是为了提高运行速度,并且允许用户使用mono作为脚本交口来控制unit引擎。同样mono只不过是unity引擎的一部分而已。一些渲染,动画,资源管理等重要任务当然还是c++来实现的,而mono只是提供一种脚本语言来实现游戏逻辑。本机代码仅仅意味着直接编译目标操作系统的代码,并执行运行时环境的复杂性中不包括额外的层。这使得管理成本较低,但是牺牲了以更直接的方式需要管理内存和其他任务中的代码。脚本语言通常抽象的以自动收集内存垃圾来管理复杂的内存,并且提供各种安全特性,这简化了编程的运行时的开销,有些脚本语言也能动态的运行时解析,这意味着他们不需要被编译之前执行。原始的指令转化为动态的机器码和执行指令的那一刻,他们是在运行时读取的(这里说的可不是编译期)。讲了这么多概念我都觉的自己在扯淡了。给大家补充一下运行环境java运行环境是jre c# vb.net,c++.net运行环境是clr,linux上运行环境是mono,这些就当是个基础知识了解就行。Unity引擎的内存空间在本质上可以分割成三个不同的内存域.每个内存域存储着不同的数据类型和托管着不同的任务。

第一个区域是本地域,这是Unity引擎中基础的基础,这个域是用C++语言编写并且根据目标平台编译成对应的机器码。这个区域负责分配内存空间,比如Asset数据,贴图材质,网格信息等.内存空间分为各种子系统,比如渲染系统,物理系统,输入系统等等.最后,它还包括了本机最重要的代表:游戏对象,比如Gameobject和Component组件. 这区域是很多基础组件保存他们的数据的地方,比如说Transform组件,Rigidbody组件等。
第二个内存区域叫做托管域,这个域是monop平台工作的地方,也是垃圾回收器工作的内存域。任何脚本对象和自定义容器都存储在这片区域。另外它还包括包装为存储在本地域内的同一个对象,这就是mono代码和机器嗲吗之间沟通的桥梁。任何域对于相同的一个实体对象都有属于该域的代表来表示这个实体,但是2个域之间的桥梁过多会造成一些伤害我们游戏的显著的性能。当一个新的游戏对象或者组件被实例化,这个实例化过程将需要内存分配在托管域和本地域。这允许如物理系统和渲染系统等子系统,通过获取本地域的transform数据来控制和渲染对象,而transform组件从我们的脚本代码中获取,仅仅是一种通过桥梁来进入本地内存空间和改变对象位置信息的引用,来回穿越桥梁应该越少越好,就是在改变位置和旋转信息的时候,先用临时变量存储,等发生变化后再对其赋值操作。
最后一个内存域是那些在本地或者外部引用的DLL(动态链接库),比如DirectX,OpenGL,和其他我们导入到项目中的DLL. 从Mono 中的C#代码引用其他DLL将会造成类似于内存空间中本地域和托管域之间的桥梁通信过渡。我们可以通过性能分析器看到所有的内存分配情况。

下面就逐个讲解:
1 我们总是容易忘记数据组织在内存中的重要性,但是恰当的组织好数据,可以得到相当大的性能提升。缓存未命中应该尽可能的避免,这意味着在大多数情况下,数组的数据在内存中是连续的迭代顺序,而不是其他任何迭代的风格。这意味着数据布局对垃圾收集也很重要,因为我们可以节省大量的迭代时间。它以迭代的方式完成,如果我们可以找到垃圾收集器跳过有问题的地方,那么我们可以节省大量的迭代时间。 在本质上,我们希望保持大量的引用类型和大量的值类型直接进行分离,哪怕是在值类型中只夹杂着一个引用类型,比如一个结构中,那么垃圾回收器会遍历整个对象和所有的数据成员,间接可引用对象. 当它到了标记-清除的时候,在此(mark - and - sweep)之前它必须验证对象的所有字段.  但是,如果我们用不同的数组分离了各种不同的类型,那么可以使垃圾收集器跳过大部分数据,  例如,如果我们有一个包含数据的结构体类型数组,那么垃圾回收器需要迭代结构体中的每一个成员变量,这造成了相当多的时间浪费。

public struct MyStruct {
int myInt;
float myFloat;
bool myBool;
string myString;
}
MyStruct[] arrayOfStructs = new MyStruct[1000];
  但是,如果我们使用这些数据的单一数组来代替,那么垃圾回收器将忽略所有的原始数据类型,只是检查字符串类型就可以了.这将使得垃圾回收标记过程变得更加快速.
int[] myInts = new int[1000];
float[] myFloats = new float[1000];
bool[] myBools = new bool[1000];
string[] myStrings = new string[1000];
这样做的原因是我们给垃圾收集器减少间接引用检查。当数据被分成单独的数组(引用类型),它发现三个数组的值类型,标志数组以后然后立即跳过,因为不需要mark标记值类型。但是它仍然必须遍历字符串数组内的所有字符串,因为每个是字符串都是引用类型,它需要确认有没有间接引用。从技术上讲,字符串不能包含间接引用,但垃圾收集器工作的水平,只知道如果对象是一个引用类型或值类型。最终,我们减少了垃圾收集器需要遍历一个额外的3000块的数据的工作量(1000个整数、浮点数、和布尔值)。
2  我们应该注意到有几个Unity API指令能够影响到堆内存的分配. 这些方法本质上都返回一个数组的数据。 例如,以下方法将在堆上分配内存
GetComponents<T>(); // (T[])
Mesh.vertices; // (Vector3[])
Camera.allCameras; // (Camera[])
这种方法应该尽可能避免调用,至多调用一次缓存,这样我们不会导致更多不必要的内存分配了。注意,Unity技术已经暗示在Unity5中这些方法可能推出内存分配更少的版本的。据推测,它可能是粒子系统这种允许访问粒子数据的方式,包括提供一个粒子数组来指向所需引用的数据。这避免了因为我们在调用相同重复的数据时重复分配缓冲区。
3  foreach循环关键之在Unity开发圈子中是一个具有争议的问题. 在UnityC#代码中调用foreach循环实现会导致不必要的堆内存分配,它在堆里分配了一个枚举器对象,就像一个类,而不是像结构体分配在栈里.这一切都取决于给定的集合执行了 GetEnumerator()方法。原来每一个集合都在Unity中的Mono版本(Mono2.6.5)都会创建一个类来替代结构体,这导致了分配在堆上的结果. 这包括但是不限制于 List<T>,LinkedList<T>,Dictionary<K,V>,ArrayList等.但是,注意实际上在典型数组上使用Foreach循环还算是安全的. Mono编译器隐式地转换Foreach循环为简单的For循环。 小规模的数量迭代造成的只是微不足道的堆分配消耗, 哪怕是当一个Enumerator对象被分配,并且一次又一次的重复使用,也只是会造成几个字节的内存开销而已。  除非,我们在每个Update函数中调用Foreach循环,不然在小项目中Foreach也只不过会造成几乎可以忽略的消耗而已. 把所有的循环都写写for循环貌似也没有多大的必要,我们只需要在下一个项目中记住不要这么写就行了(就是善用Foreach和for循环)。 注意,在Transform组件中执行Foreach循环是一个非常典型的捷径,可以迭代全部Transform的孩子,比如.
foreach (Transform child in transform) {
// do stuff with 'child'
    然而,就如上面提到的,结果在同一个堆分配.编写代码的时候应该避免如下这样写法:
for (int i = 0; i < transform.childCount; ++i) {
Transform child = transform.GetChild(i);
// do stuff with 'child'
}
4   刚开始启动一个协程的时候需要花费小数量的内存,但注意当yields方法调用的时候不会再造成进一步产生内存成本花费. 如果内存消耗和垃圾收集是重要的问题,我们应该尽量避免出现过多的短暂的的协同程序,还有避免在运行时过多的调用StartCoroutine()。说到临时缓冲区,对象池是一个非常好的方法来进行最小化和控制内存使用,进而避免了内存的回收和再分配。这个想法是为了制定我们创建对象的系统, 隐藏的对象包括我们最近被回收的新对象或者之前更早回收分配的对象. 典型的术语来描述这个过程就是"生成"和"消失"(注:而不是销毁),来替代创建和删除. 在任何时间一个对象消失,只是意味着这个对象仅仅是从视图中隐藏,直到当我们重新需要它的时候才出现,这时候就是这个对象的重生和重用。让我们来展示下一个对象池的快速实现方式:
        第一个需求是允许池内对象当需要的时候如何回收本身,下列接口实现了需求:
public interface IPoolableObject
{
void New();
void Respawn();
}
  这个接口定义了2个方法:New()和Respawn(),这应该称之为对象的第一次创建和对象的再次重现. 第二个需求是提供一个属于这个接口的基类实现,基类允许任何类型的任何对象处理该对象的初始创建和重新生成。关于对象池我就不多讲了,前面有2节讲到了对象池。
5 .net性能优化,
1 在非托管的资源的清理上,主要有有终止化操作和Dispose模式两种,其中Finalize方式在执行时间不确定,运行顺序不确定,同时对垃圾回收的性能有极大的损伤。因此强烈建议以Dispose模式来代替Finalize方式,在代来性能提升的同时,实现了更加灵活的控制权。
2 在适当的情况下对对象实现弱引用,为对象实现弱引用,是有效提高性能的手段之一。弱引用是对象引用的一种“中间态”,实现了对象既可以通过GC回收其内存,又可被应用程序访问的机制。在.net中,weakreference类用于表示弱引用,通过其target属性来表示要追踪的对象,通过赋值给变量来创建目标对象的强应用,
public void WeakRef()
{
    MyClass mc=new MyClass();
    WeakReference wr=new WeakReference(mc); 
    mc=null;
    if(wr.IsAlive)
    {
      mc=wr.Target as MyClass;
    }
    else
    {
      mc=new MyClass();
    }
}
3 尽可能以using来执行资源清理。以using语句来执行实现了Dispose模式的对象,是较好的资源清理选择,简洁优雅的代码实现,同时能够保证自动执行Dispose方法来销毁非托管资源,当我们在处理IO操作的时候用using会特别方便。
4 初始化是最好为集合对象指定大下。
ArraryList a1=new ArrayList(2);
a1.add("One");
a1.add("Two");

              //容量动态增加一倍

a1.add("Three");

5 尽量在子类中重写方法

ToString()是System.Object提供的一个公有的虚方法,.net中任何类型都可继承System.Object类型提供的实现方法,默认为访问类型全路径名称。在自定义类或结构中重写ToString方法,除了可以有效控制输出结果,还能有效减少装箱操作的发生。

6 以is/as模式进行类型兼容性检查。以is和as操作符可以用于判断对象类型的兼容性,以is来实现类型判断,以as实现安全的类型转换是值得推荐的方法。这样能避免不必要的异常抛出。从而实现一种安全,灵活的转换控制。

const是编译时常量,readonly是运行时常量,所以const高效,readonly灵活。在实际的应用中,推荐一static readonly来代替const,及解决const可能引起的程序集引用不一致的问题,还有代来更多的灵活性控制。

淡扯完了,接下给大家分享一本c#的书籍吧,或许对大家有用吧。http://pan.baidu.com/s/1boVkwxx。如果有朋友有问题的话,大家可以加qq讨论,qq:1850761495

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值