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