GC优化策略-官篇3

原文:http://www.cnblogs.com/zblade/编程

英文:英文地址c#

 

形成没必要要的堆内存分配的因素数组

  咱们已经知道值类型变量在堆栈上分配,其余的变量在堆内存上分配,可是任然有一些状况下的堆内存分配会让咱们感到吃惊。下面让咱们分析一些常见的没必要要的堆内存分配行为并对其进行优化。缓存

  字符串  ide

   在c#中,字符串是引用类型变量而不是值类型变量,即便看起来它是存储字符串的值的。这就意味着字符串会形成必定的内存垃圾,因为代码中常用字符串,因此咱们须要对其格外当心。函数

  c#中的字符串是不可变动的,也就是说其内部的值在建立后是不可被变动的。每次在对字符串进行操做的时候(例如运用字符串的“加”操做),unity会新建一个字符串用来存储新的字符串,使得旧的字符串被废弃,这样就会形成内存垃圾。性能

  咱们能够采用如下的一些方法来最小化字符串的影响:测试

  1)减小没必要要的字符串的建立,若是一个字符串被屡次利用,咱们能够建立并缓存该字符串。优化

  2)减小没必要要的字符串操做,例如若是在Text组件中,有一部分字符串须要常常改变,可是其余部分不会,则咱们能够将其分为两个部分的组件,对于不变的部分就设置为相似常量字符串便可,见下面的例子。ui

  3)若是咱们须要实时的建立字符串,咱们能够采用StringBuilderClass来代替,StringBuilder专为不须要进行内存分配而设计,从而减小字符串产生的内存垃圾。

  4)移除游戏中的Debug.Log()函数的代码,尽管该函数可能输出为空,对该函数的调用依然会执行,该函数会建立至少一个字符(空字符)的字符串。若是游戏中有大量的该函数的调用,这会形成内存垃圾的增长。

  在下面的代码中,在Update函数中会进行一个string的操做,这样的操做就会形成没必要要的内存垃圾:

1
2
3
4
5
6
7
public  Text timerText;
private  float  timer;
void  Update()
{
     timer += Time.deltaTime;
     timerText.text =  "Time:" + timer.ToString();
}

  经过将字符串进行分隔,咱们能够剔除字符串的加操做,从而减小没必要要的内存垃圾:

1
2
3
4
5
6
7
8
9
10
11
12
public  Text timerHeaderText;
public  Text timerValueText;
private  float  timer;
void  Start()
{
     timerHeaderText.text =  "TIME:" ;
}
 
void  Update()
{
    timerValueText.text = timer.ToString();
}

  Unity函数调用

  在代码编程中,当咱们调用不是咱们本身编写的代码,不管是Unity自带的仍是插件中的,咱们均可能会产生内存垃圾。Unity的某些函数调用会产生内存垃圾,咱们在使用的时候须要注意它的使用。

  这儿没有明确的列表指出哪些函数须要注意,每一个函数在不一样的状况下有不一样的使用,因此最好仔细地分析游戏,定位内存垃圾的产生缘由以及如何解决问题。有时候缓存是一种有效的办法,有时候尽可能下降函数的调用频率是一种办法,有时候用其余函数来重构代码是一种办法。如今来分析unity中常见的形成堆内存分配的函数调用。

  在Unity中若是函数须要返回一个数组,则一个新的数组会被分配出来用做结果返回,这不容易被注意到,特别是若是该函数含有迭代器,下面的代码中对于每一个迭代器都会产生一个新的数组:

void ExampleFunction()
{
    for(int i=0; i < myMesh.normals.Length;i++)
    {
        Vector3 normal = myMesh.normals[i];
    }
}

  对于这样的问题,咱们能够缓存一个数组的引用,这样只须要分配一个数组就能够实现相同的功能,从而减小内存垃圾的产生:

1
2
3
4
5
6
7
8
void  ExampleFunction()
{
     Vector3[] meshNormals = myMesh.normals;
     for ( int  i=0; i < meshNormals.Length;i++)
     {
         Vector3 normal = meshNormals[i];
     }
}

  此外另外的一个函数调用GameObject.name 或者 GameObject.tag也会形成预想不到的堆内存分配,这两个函数都会将结果存为新的字符串返回,这就会形成没必要要的内存垃圾,对结果进行缓存是一种有效的办法,可是在Unity中都对应的有相关的函数来替代。对于比较gameObject的tag,能够采用GameObject.CompareTag()来替代。

  在下面的代码中,调用gameobject.tag就会产生内存垃圾:

1
2
3
4
5
private  string  playerTag= "Player" ;
void  OnTriggerEnter(Collider other)
{
     bool  isPlayer = other.gameObject.tag == playerTag;
}

  采用GameObject.CompareTag()能够避免内存垃圾的产生:

1
2
3
4
5
private  string  playerTag =  "Player" ;
void  OnTriggerEnter(Collider other)
{
     bool  isPlayer = other.gameObject.CompareTag(playerTag);
}

  不仅是GameObject.CompareTag,unity中许多其余的函数也能够避免内存垃圾的生成。好比咱们能够用Input.GetTouch()和Input.touchCount()来代替Input.touches,或者用Physics.SphereCastNonAlloc()来代替Physics.SphereCastAll()。

  装箱操做

  装箱操做是指一个值类型变量被用做引用类型变量时候的内部变换过程,若是咱们向带有对象类型参数的函数传入值类型,这就会触发装箱操做。好比String.Format()函数须要传入字符串和对象类型参数,若是传入字符串和int类型数据,就会触发装箱操做。以下面代码所示:

1
2
3
4
5
void  ExampleFunction()
{
     int  cost = 5;
     string  displayString = String.Format( "Price:{0} gold" ,cost);
}

  在Unity的装箱操做中,对于值类型会在堆内存上分配一个System.Object类型的引用来封装该值类型变量,其对应的缓存就会产生内存垃圾。装箱操做是很是广泛的一种产生内存垃圾的行为,即便代码中没有直接的对变量进行装箱操做,在插件或者其余的函数中也有可能会产生。最好的解决办法是尽量的避免或者移除形成装箱操做的代码。

  协程

  调用 StartCoroutine()会产生少许的内存垃圾,由于unity会生成实体来管理协程。因此在游戏的关键时刻应该限制该函数的调用。基于此,任何在游戏关键时刻调用的协程都须要特别的注意,特别是包含延迟回调的协程。

  yield在协程中不会产生堆内存分配,可是若是yield带有参数返回,则会形成没必要要的内存垃圾,例如:

1
yield  return  0;

  因为须要返回0,引起了装箱操做,因此会产生内存垃圾。这种状况下,为了不内存垃圾,咱们能够这样返回:

1
yield  return  null ;

  另一种对协程的错误使用是每次返回的时候都new同一个变量,例如:

1
2
3
4
while (!isComplete)
{
     yield  return  new  WaitForSeconds(1f);
}

  咱们能够采用缓存来避免这样的内存垃圾产生:

1
2
3
4
5
WaitForSeconds delay =  new  WaiForSeconds(1f);
while (!isComplete)
{
     yield  return  delay;
}

  若是游戏中的协程产生了内存垃圾,咱们能够考虑用其余的方式来替代协程。重构代码对于游戏而言十分复杂,可是对于协程而言咱们也能够注意一些常见的操做,好比若是用协程来管理时间,最好在update函数中保持对时间的记录。若是用协程来控制游戏中事件的发生顺序,最好对于不一样事件之间有必定的信息通讯的方式。对于协程而言没有适合各类状况的方法,只有根据具体的代码来选择最好的解决办法。

  foreach 循环

  在unity5.5之前的版本中,在foreach的迭代中都会生成内存垃圾,主要来自于其后的装箱操做。每次在foreach迭代的时候,都会在堆内存上生产一个System.Object用来实现迭代循环操做。在unity5.5中解决了这个问题,好比,在unity5.5之前的版本中,用foreach实现循环:

1
2
3
4
5
6
7
void  ExampleFunction(List listOfInts)
{
     foreach ( int  currentInt  in  listOfInts)
     {
         DoSomething(currentInt);
     }
}

  若是游戏工程不能升级到5.5以上,则能够用for或者while循环来解决这个问题,因此能够改成:

1
2
3
4
5
6
7
8
void  ExampleFunction(List listOfInts)
{
     for ( int  i=0; i < listOfInts.Count; i++)
     {
         int  currentInt = listOfInts[i];
         DoSomething(currentInt);
     }
}

  函数引用

   函数的引用,不管是指向匿名函数仍是显式函数,在unity中都是引用类型变量,这都会在堆内存上进行分配。匿名函数的调用完成后都会增长内存的使用和堆内存的分配。具体函数的引用和终止都取决于操做平台和编译器设置,可是若是想减小GC最好减小函数的引用。

  LINQ和常量表达式

  因为LINQ和常量表达式以装箱的方式实现,因此在使用的时候最好进行性能测试。

重构代码来减少GC的影响

  即便咱们减少了代码在堆内存上的分配操做,代码也会增长GC的工做量。最多见的增长GC工做量的方式是让其检查它没必要检查的对象。struct是值类型的变量,可是若是struct中包含有引用类型的变量,那么GC就必须检测整个struct。若是这样的操做不少,那么GC的工做量就大大增长。在下面的例子中struct包含一个string,那么整个struct都必须在GC中被检查:

1
2
3
4
5
6
7
public  struct  ItemData
{
     public  string  name;
     public  int  cost;
     public  Vector3 position;
}
private  ItemData[] itemData;

  咱们能够将该struct拆分为多个数组的形式,从而减少GC的工做量:

1
2
3
private  string [] itemNames;
private  int [] itemCosts;
private  Vector3[] itemPositions;

  另一种在代码中增长GC工做量的方式是保存没必要要的Object引用,在进行GC操做的时候会对堆内存上的object引用进行检查,越少的引用就意味着越少的检查工做量。在下面的例子中,当前的对话框中包含一个对下一个对话框引用,这就使得GC的时候会去检查下一个对象框:

1
2
3
4
5
6
7
8
9
public  class  DialogData
{
      private  DialogData nextDialog;
      public  DialogData GetNextDialog()
      {
            return  nextDialog;
                     
      }
}

  经过重构代码,咱们能够返回下一个对话框实体的标记,而不是对话框实体自己,这样就没有多余的object引用,从而减小GC的工做量:

1
2
3
4
5
6
7
8
public  class  DialogData
{
     private  int  nextDialogID;
     public  int  GetNextDialogID()
     {
        return  nextDialogID;
     }
}

  固然这个例子自己并不重要,可是若是咱们的游戏中包含大量的含有对其余Object引用的object,咱们能够考虑经过重构代码来减小GC的工做量。 

定时执行GC操做

  主动调用GC操做

   若是咱们知道堆内存在被分配后并无被使用,咱们但愿能够主动地调用GC操做,或者在GC操做并不影响游戏体验的时候(例如场景切换的时候),咱们能够主动的调用GC操做:

1
System.GC.Collect()

  经过主动的调用,咱们能够主动驱使GC操做来回收堆内存。

总结

  经过本文对于unity中的GC有了必定的了解,对于GC对于游戏性能的影响以及如何解决都有必定的了解。经过定位形成GC问题的代码以及代码重构咱们能够更有效的管理游戏的内存。

  接着我会继续写一些Unity相关的文章。翻译的工做,在后面有机会继续进行。

相关文章
相关标签/搜索