C#代碼優化:斬斷伸向堆內存的“黑手”

在上期《C#代碼優化:拯救你的CPU耗時》中,我們依託UWA本地資源檢測,從“時間”的角度對C#代碼檢測中和CPU耗時相關的知識點爲大家進行了簡單的講解。本篇將從“空間”角度入手,爲大家繼續梳理C#代碼檢測的相關規則。

C#是在虛擬機(VM)環境當中運行的(Mono虛擬機或IL2CPP虛擬機),其分配的堆內存是由虛擬機進行管理與回收的,因此,C#代碼被稱爲“託管代碼”,其堆內存又被稱爲“託管內存”。託管內存的釋放依賴於虛擬機的GC(垃圾回收)機制,本文就不展開討論了。

C#程序開發要遵循的一個基本原則就是避免不必要的堆內存分配,而堆內存分配主要會造成以下後果:

  • 程序所佔用的內存總量過大。內存是有限的資源,對於遊戲(特別是移動遊戲)來說,內存的佔用可謂是寸土寸金。過多的無法釋放的內存很可能導致程序崩潰的現象。
  • 過多的分配次數會導致堆碎片變多,從而可能導致無法開闢出所需要的連續的內存,這也會導致程序崩潰。
  • 內存分配會觸發GC(垃圾回收),而GC的代價較高,會造成卡頓。

 

1、該類的方法中存在 .tag的調用

tag是場景中GameObject的標籤,而類GameObject的成員tag是一個屬性,在獲取該屬性時,實質上是調用get_tag()函數,從native層返回一個字符串。字符串屬於引用類型,這個字符串的返回,會造成堆內存的分配。然而,Unity引擎也沒有通過緩存的方式對get_tag進行優化,在每次調用get_tag時,都會重新分配堆內存。

所以當需要對tag進行比較時,我們建議使用函數GameObject.CompareTag(),該函數是在native層實現的,不會造成託管堆內存的分配,也就避免了GC壓力。


2、該類的方法中存在對紋理GetPixels()/GetPixels32()調用

對Texture2D類型的對象調用GetPixels()和GetPixels32(),一般都是爲了獲取指定Mipmap層的全部像素信息,而圖片上的像素數量往往是很龐大的。

從內存分配上講,該函數會在託管堆中分配內存,用以存儲紋理數據的像素信息,但引擎不會對其進行緩存。所以如果在頻繁調用的函數中使用,就會造成持續性的堆內存分配。

從耗時上講,擅長執行大規模並行運算的GPU來處理圖片信息是非常容易的,但CPU在進行逐個像素信息的獲取時,就顯得有些喫力了。並且GetPixels()在實現上是由CPU同步執行的,所以耗時會較高,同時會阻塞調用的線程,從而可能會造成卡頓。因此在非必要的情況下,並不建議使用GetPixels()。


3、該類的方法中存在 GetComponentsInChildren調用/該類的方法中存在 GetComponentsInParent調用

在之前的文章《C#代碼優化:拯救你的CPU耗時》中,我們對GetComponentsInChildren和GetComponentsInParent進行了簡單地講解。在這裏,我們進一步補充說明,這兩者在實際調用中,因爲涉及到對象的遍歷和結果的返回,所以如果使用不當,就會造成持續性的堆內存分配。我們建議開發團隊使用接受List類型的引用作爲參數的版本,這樣就可以避免每次調用都造成堆內存的分配。


4、該類的方法中存在Linq相關函數的調用

Linq相關的函數一般都用於對數據的查詢和處理。功能上簡單來講,就是對一堆數據進行各種if判斷和for循環處理。使用Linq提供的API,我們可以寫出SQL語句風格的代碼來進行集合數據的處理,這能夠明顯提升我們代碼的簡明性、可讀性,維護上也更方便,從而提升編寫效率,但是這些優點是以性能的開銷爲代價的。

Linq在執行過程中會產生一些臨時變量,而且會用到委託(lambda 表達式)。如果使用委託作爲條件的判定方法,時間開銷就會很高,並且會造成一定的堆內存分配。所以在一般的Unity遊戲項目開發中,我們不推薦使用Linq相關的函數。在編輯器功能開發中,我們才常常把Linq和Reflection進行配合使用。


5、該類的方法中存在對Renderer進行sharedMaterials的獲取

同樣,在之前的文章《C#代碼優化:拯救你的CPU耗時》中,我們對.material/materials進行了講解。簡單地說,對.material(s)的調用會產生新的材質球實例;而sharedMaterials則是共享材質,不會生成新的材質實例。然而,對.sharedMaterials的調用,依舊會分配堆內存——每次調用都會分配一個用以存放Material的索引的數組。雖然該數組佔用的內存相對較小,但我們還是建議不要對其進行頻繁地調用。


6、該類的方法中存在 Input.touches調用

移動端項目交互裏,點觸操作可以說是極爲頻繁與常見。在點觸操作的獲取上,Input.touches就是用來獲取當前幀中所有點觸操作的狀態和相應數據。但是查看.touches的實現,我們就會發現:每次在對其調用時,都會new一個數組touches,從而造成一定的堆內存分配。所以開發團隊要避免Input.touches的頻繁使用以防造成堆內存的額外佔用。


7、FindObjectsOfType調用

在之前的文章《C#代碼優化:拯救你的CPU耗時》中,我們對FindObjectsOfType進行了簡單地介紹。它在增大CPU耗時的同時,也會佔用相當一部分的堆內存分配,所以建議通過一次調用,緩存結果的方式減少其帶來的性能影響。


8、該類的方法中存在 TextAsset/WWW.bytes調用

該規則針對的其實是兩條不相關的Unity API。首先是TextAsset的bytes屬性。TextAsset是Unity中的一種文本資源,它支持包括txt、html、bytes和csv等多種格式的文件進行轉換。在獲取bytes屬性時,Unity會從native層獲取字節數組(byte[]),從而分配一定的堆內存。

另一個API指的是WWW這個類的bytes成員,每次對其進行調用都會導致堆內存的分配,需要指出的是Unity已經放棄了WWW這個類的相關接口,所以我們建議大家使用UnityWebRequest來實現相關的功能。


以上我們就目前本地資源檢測中提供的檢測項均做了分析和解讀,希望以上這些知識點能在實際的開發過程中爲大家帶來幫助,同時我們也會基於廣大開發者的需求反饋,不斷增加新的檢測項。

需要說明的是,每一項檢測規則的閾值都可以由開發團隊依據自身項目的實際需求去設置合適的閾值範圍,這也是本地資源檢測的一大特點。同時,也歡迎大家來使用UWA推出的本地資源檢測服務,可幫助大家儘早對項目建立科學的美術規範。

 

萬行代碼屹立不倒,全靠基礎掌握得好!

《場景檢測:面片、光影和物理屬性》
《場景檢測:Audio Listener、RigidBody和Prefab連接》
《場景檢測:霧效、Canvas和碰撞體》
《特效優化2:效果與性能的博弈》
《特效優化:發現絢麗背後的質樸》
《Prefab優化:預製體中的各種細節選擇》




性能黑榜相關閱讀

《那些年給性能埋過的坑,你跳了嗎?》
《那些年給性能埋過的坑,你跳了嗎?(第二彈)》
《掌握了這些規則,你已經戰勝了80%的對手!》

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章