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相關的文章。翻譯的工做,在後面有機會繼續進行。

相關文章
相關標籤/搜索