Unity开发者的C#内存管理(中篇)
第一篇介紹了在?.NET/Mono?和Unity里內存管理的基礎,并且提供了一些避免不必要的堆分配的建議。第三篇會深入到對象池。所有的都主要是面向中級的C#開發者。
我們現在來看看兩種發現項目中不想要的堆分配的方法。第一種-Unity?profiler-實在是太簡單了,但是卻相當費錢,得買’pro‘版的。第二種是講你的.NET/Mono程序集反匯編成中間語言(CIL)然后再檢查。如果你從沒見過反匯編的.NET代碼,繼續看下去,不難,而且免費還很有啟發意義。
容易的方法:使用Unity?profiler
Unity優秀的分析器主要被用來分析游戲中各種資源需要的性能和資源:著色器,紋理,音頻,游戲對象等等。然而分析器在發掘內存上也一樣有用-跟你的C#代碼的行為有關-甚至是外部的?沒引用UnityEngine.dll的.NET/Mono程序集!在當前Unity版本中(4.3),這個功能不是來自內存分析器,而是CPU分析器。到C#代碼的時候,內存分析器只是展示Mono堆的總大小和已使用的量。
這樣讓你看你的C#代碼是否有嫩村泄露實在太粗糙了。即使不適用任何腳本,已使用的堆大小也會持續增長和縮減。只要你使用腳本,你需要一個看哪里分配了內存的途徑,然后CPU分析器剛好給你提供這個。
讓我們來看看一些實例代碼。假設下面的腳本綁定到了一個GameObject上。
?
using UnityEngine;using System.Collections.Generic; public class MemoryAllocatingScript : MonoBehaviour{ void Update() { List<int> iList = new List<int>(new int[] { 072, 101, 108, 108, 111, 032, 119, 111, 114, 108, 100, 033 }); string result = ""; foreach (int i in iList.ToArray()) result += ((char)i).ToString(); Debug.Log(result); }}?
它所做的就是通過一組整數用一種繞的方法創建了一個字符串("Hello?world!"),一路上造成了不必要的內存分配。多少呢?很高興你問了,但是我很懶,就讓我們看看CPU分析器吧。選中窗口頂部的”Deep?Profiler“,可以跟蹤到每幀的調用樹。
正如你所見,堆內存在Update()函數過程中的5個不同位置被分配。這個列表的初始化,foreach循環里到數組的轉換是多余的,每一個數字到字符的轉換以及連接都需要分配內存。有趣的是,僅僅是調用Debug.Log()也會分配一大塊內存-這點值得記下來,即使在生產環境中這段代碼會被剔除。
如果你沒有Unity?Pro,但是恰巧有Microsoft?Visual?Studio,那就有替代Unity?Profiler的方法來發掘調用堆棧。Telerik?告訴我他們的?JustTrace?Memory?profiler?有相似的功能?(see?here).?然而,?我不知道它模仿Unity每幀記錄調用樹到了什么程度。更進一步,盡管對Unity項目的遠程調試(通過UnityVS)?是可以的,我還是沒有成功的把JustTrace用來分析被Unity調用的程序集。
只是稍微難一點點的方法:反匯編你的代碼
CIL的背景知識
如果你已經有了一個.NET/Mono的反匯編器,開始用吧,不然我推薦ILSpy.?這個工具不僅是免費的,它還非常干凈簡單,但是剛好包含下面我們會用到的一個特殊功能。
你也許知道C#編譯器不會將你的代碼編譯成機器語言,而是公共中間語言。這種語言是被原.NET團隊作為一種包含兩種來自高級語言特性的低級語言開發出來的。一方面,它與硬件無關,另一方面,它包含最適合被稱為’面向對象’的特性,比如可以引用其他模塊或者類的能力。
沒有經過代碼模糊處理(?code?obfuscator?)的CIL代碼是異常容易反向工程的。?許多情況下,結果幾乎和原始的C#(VB)代碼一樣。ILSpy?可以替你做這件事,但是我們僅僅反匯編代碼就可以了(ILSpy通過調用ildasm.exe來實現,.它是NET/Mono的一部分)。讓我們從一個加兩個整數的函數開始。
int AddTwoInts(int first, int second) { int result = first + second; return result;}如果你愿意,你可以將這段代碼粘貼到MemoryAllocatingScript.cs文件里。然后確保Unity編譯了它,再用ILSpy打開編譯了的庫Assembly-Csharp.dll。如果你選擇AddTwoInts()?方法,你會看到下面的:
除了藍色的關鍵字?hidebysig,我們可以忽略掉,方法簽名應該看起來差不多。要了解到方法里主要發生了什么,你需要知道CIL把CPU看成一個堆棧式機器stack?machine?而不是寄存器機器register?machine。CIL假設CPU可以處理非常基礎,非常算法的指令,例如”將兩個整數相加“,而且它可以處理任何內存地址的隨機訪問。CIL還假設CPU不直接在RAM上進行算術操作,而是首先需要將數據裝載進概念上的計算堆棧。(注意計算堆棧和你你知道的C#堆棧沒有任何關系。CIL計算堆棧只是一個抽象的,并且預設很小。)在行IL_0000到IL_0005發生了:
- 兩個整型參數被推進堆棧。
- 加法被調用然后從堆棧里彈出開始位置的兩個對象,自動將記過壓進堆棧。
- 第3和4行可以忽略,因為在發行版本里會被優化掉。
- 這個方法返回堆棧的第一個值。
找到CIL里面的內存分配
CIL代碼美在它不會隱藏任何堆分配。而且,堆分配會嚴格按照以下三個順序分配,在你的反匯編代碼里能看到。
- newobj?<constructor>:這創建了一個由constructor指定類型的未初始化的對象。如果這個對象是值類型,它就在堆棧上被創建。如果它是一個引用類型,就在堆上。你總是能從CIL代碼知道類型,所以你可以容易的知道內存分配產生的地方。
- newarr?<element?type>:這條指令在堆上創建了一個新的數組。Element的類型由參數指定。
- box?<value?type?token>:這條特殊的指令執行裝箱操作,我們已經在第一篇帖子里說過。
Let's?look?at?a?rather?contrived?method?that?performs?all?three?types?of?allocations.
然我們來看一個人為的執行這三種內存分配的方法。
?
void SomeMethod() { object[] myArray = new object[1]; myArray[0] = 5; Dictionary<int, int> myDict = new Dictionary<int, int>();myDict[4] = 6; foreach (int key in myDict.Keys) Console.WriteLine(key);}?
有這幾行代碼產生的CIL代碼很多,所以這里我們只看關鍵部分:
IL_0001:?newarr?[mscorlib]System.Object...IL_000a:?box?[mscorlib]System.Int32...IL_0010:?newobj?instance?void?class?[mscorlib]System.????Collections.Generic.Dictionary'2<int32,?int32>::.ctor()...IL_001f:?callvirt?instance?class?[mscorlib]System.????Collections.Generic.Dictionary`2/KeyCollection<!0,?!1>????class?[mscorlib]System.Collections.Generic.Dictionary`2<int32,????int32>::get_Keys()
正如我們懷疑過的,對象的數組(SomeMethod()里的第一行)導致newarr指令。整數5被賦給數組的第一個元素需要裝箱。Dictionary<int,?int>是被newobj指令分配的。
但是還有第四個堆分配!正如我在第一篇帖子里提到的,Dictionary<K,?V>.?KeyCollection被聲明為一個類,不是結構。這個類的一個實例會被創建,這樣foreach蓄奴換才有迭代的對象。不幸的是,分配發生在Keys屬性的getter方法里。正如你在CIL代碼里看到,這個方法的名字是get_Keys(),而且它的返回值是一個類。
作為一個查找內存泄露的通用方法,你可以生成一個對你的整個程序集反匯編的CIL文件,只要在ILSpy按下Ctrl+S。然后用你喜歡的文本編輯器打開這個文件,搜索上面提到的三種指令。查出其他程序集里的內存泄露是有難度。我唯一知道的辦法就是仔細檢查你的C#代碼,確認所有的外部方法調用,并且一個個地查看它們的CIL代碼。你怎么知道什么時候就完成了?很簡單:你的游戲可以流暢的運行好幾個小時,不因為垃圾收集造成任何的性能瓶頸。
PS:在之前的帖子里,我答應要向你們展示如何確認你們系統上的Mono版本。只要裝了ILSpy,沒有比這更簡單的了。在ILSpy里,點擊打開然后找到Unity根目錄。找到Data/Mono/lib/mono/2.0然后打開mscorlib.dll。在層級視圖里,找到mscorlib/-/Consts,然后那兒你能找到MonoVersion作為一個字符串常量。
總結
以上是生活随笔為你收集整理的Unity开发者的C#内存管理(中篇)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 标志设计教程
- 下一篇: 计算机毕业设计ssm电影售票管理系统n9