日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁 > 编程语言 > C# >内容正文

C#

Unity开发者的C#内存管理

發(fā)布時間:2024/1/8 C# 28 豆豆
生活随笔 收集整理的這篇文章主要介紹了 Unity开发者的C#内存管理 小編覺得挺不錯的,現(xiàn)在分享給大家,幫大家做個參考.
很多游戲時常崩潰,大多數(shù)情況下都是內(nèi)存泄露導(dǎo)致的。這系列文章詳細(xì)講解了內(nèi)存泄露的原因,如何找到泄露,又如何規(guī)避。


我要在開始這個帖子之前懺悔一下。雖然一直作為一個C / C++開發(fā)者,但是很長一段時間我都是微軟的C#語言和.NET框架的秘密粉絲。大約三年前,當(dāng)我決定離開狂野的基于C / C++的圖形庫,進(jìn)入現(xiàn)代游戲引擎的文明世界,Unity 帶著一個讓我毫不猶豫選擇它的特性脫穎而出。Unity 并不需要你用一種語言(如Lua或UnrealScript)‘寫腳本’卻用另外一種語言'編程'。相反,它對Mono有深度的支持,這意味著所有的編程可以使用任何.NET語言。哦,真開心!我終于有一個正當(dāng)?shù)睦碛珊虲 ++說再見,而且通過自動內(nèi)存管理我所有的問題都得到了解決。此功能已經(jīng)內(nèi)置在C#語言,是其哲學(xué)的一個組成部分。沒有更多的內(nèi)存泄漏,沒有更多的考慮內(nèi)存管理!我的生活會變得容易得多。






如果你有哪怕是最基本的使用Unity或游戲編程的經(jīng)驗(yàn),你就知道我是多么的錯誤了。我費(fèi)勁艱辛才了解到在游戲開發(fā)中,你不能依賴于自動內(nèi)存管理。如果你的游戲或中間件足夠復(fù)雜并且對資源要求很高,用C#做Unity開發(fā)就有點(diǎn)像往C ++方向倒退了。每一個新的Unity開發(fā)者很快學(xué)會了內(nèi)存管理是很麻煩的,不能簡單地托付給公共語言運(yùn)行庫(CLR)。Unity論壇和許多Unity相關(guān)的博客包含一些內(nèi)存方面的技巧集合和最佳實(shí)不規(guī)范踐。不幸的是,并非所有這些都是基于堅實(shí)的事實(shí),盡我所知,沒有一個是全面的。此外,在Stackoverflow這樣的網(wǎng)站上的C#專家似乎經(jīng)常對Unity開發(fā)者面對的古怪的、非標(biāo)準(zhǔn)的問題沒有一點(diǎn)耐心。由于這些原因,在這一篇和下面的兩篇帖子,我試著給出關(guān)于Unity特有的C#的內(nèi)存管理問題的概述,并希望能介紹一些深入的知識。


第一篇文章討論了在.NET和Mono的垃圾收集世界中的內(nèi)存管理基礎(chǔ)知識。我也討論了內(nèi)存泄漏的一些常見的來源。
第二篇著眼于發(fā)現(xiàn)內(nèi)存泄漏的工具。Unity的Profiler是一個強(qiáng)大的工具,但它也是昂貴的(似乎在中國不是)。因此,我將討論.NET反匯編和公共中間語言(CIL),以顯示你如何只用免費(fèi)的工具發(fā)現(xiàn)內(nèi)存泄漏。
第三篇討論C#對象池。再次申明,重點(diǎn)只針對出現(xiàn)在Unity/ C#開發(fā)中的具體需要。


垃圾收集的限制
大多數(shù)現(xiàn)代操作系統(tǒng)劃分動態(tài)內(nèi)存為棧和堆(1, 2),許多CPU架構(gòu)(包括你的PC / Mac和智能手機(jī)/平板電腦)在他們的指令集支持這個區(qū)分。 C#通過區(qū)分值類型支持它(簡單的內(nèi)置類型以及被聲明為枚舉或結(jié)構(gòu)的用戶自定義類型)和引用類型(類,接口和委托)。值類型在堆中,引用類型分配在棧上。堆具有固定大小,在一個新的線程開始時被設(shè)定。它通常很小 - 例如,NET線程在Windows默認(rèn)為一個1MB的堆棧大小。這段內(nèi)存是用來加載線程的主函數(shù)和局部變量,并且隨后加載和卸載被主函數(shù)調(diào)用的函數(shù)(與他們的本地變量)。一些內(nèi)存可能會被映射到CPU的緩存,以加快速度。只要調(diào)用深度不過高或局部變量不過大,你不必?fù)?dān)心堆棧溢出。這種棧的用法很好地契合結(jié)構(gòu)化編程的概念(structured programming)。


如果對象太大不適合放在棧上,或者如果他們要比創(chuàng)造了他們的函數(shù)活得長,堆這個時候就該出場了。堆是“其他的一切“- 是一段可以隨著每個OS請求增長的內(nèi)存,and over which the program rules as it wishes(這句不會……)。不過,雖然棧幾乎是不能管理(只使用一個指針記住free section開始的地方),堆碎片很快會從分配對象的順序到你釋放的順序打亂。把堆想成瑞士奶酪,你必須記住所有的孔!根本沒有樂趣可言。進(jìn)入自動內(nèi)存管理。自動分配的任務(wù) - 主要是為你跟蹤奶酪上所有的孔 - 是容易的,而且?guī)缀醣凰械默F(xiàn)代編程語言支持。更難的是自動釋放,尤其是決定釋放的時機(jī),這樣你就不必去管了。


后者任務(wù)被稱為垃圾收集(GC)。不是你告訴你的運(yùn)行時環(huán)境什么時候可以釋放對象的內(nèi)存,是運(yùn)行時跟蹤所有的對象引用,從而能夠確定——在特定的時間間隔里,一個對象不可能被你的代碼引用到了。這樣一個對象就可以被銷毀,它的內(nèi)存會被釋放。GC仍被學(xué)者積極地研究著,這也解釋了為什么GC的架構(gòu)自.net框架1.0版以來改變?nèi)绱酥?。然?#xff0c;Unity不使用.net而是其開源的表親,Mono,而它一直落后于它的商業(yè)化對手(.net)。此外,Unity不默認(rèn)使用Mono的最新版本(2.11/3.0),而是使用版本2.6(準(zhǔn)確地說,2.6.5,在我的Windows4.2.2安裝版上(編輯:這同樣適用于Unity4.3])。如果你不確定如何自己驗(yàn)證這一點(diǎn),我將在接下來的帖子里討論。


在Mono2.6版本之后引入了有關(guān)GC的重大修改。新版本使用分代垃圾收集(generational GC),而2.6仍采用不太復(fù)雜的貝姆垃圾收集器(Boehm garbage collector)?,F(xiàn)代分代GC執(zhí)行得非常好,甚至可以在實(shí)時應(yīng)用中使用(在一定限度內(nèi)),如游戲。另一方面,勃姆式GC的工作原理是在堆上做窮舉搜索垃圾。以一種相對“罕見”的時間間隔(即,通常的頻率大大低于一次每幀)。因此,它極有可能以一定的時間間隔造成幀率下降,因而干擾玩家。Unity的文檔建議您調(diào)用System.GC.Collect(),只要您的游戲進(jìn)入幀率不那么重要的階段(例如,加載一個新的場景,或顯示菜單)。然而,對于許多類型的游戲,出現(xiàn)這樣的機(jī)會也極少,這意味著,在GC可能會在你不想要它的時候闖進(jìn)來。如果是這樣的話,你唯一的選擇是自己硬著頭皮管理內(nèi)存。而這正是在這個帖子的其余部分,也是以下兩個帖子的內(nèi)容!


自己做內(nèi)存管理者


讓我們申明在Unity/.NET的世界里“自己管理內(nèi)存”意味著什么。你來影響內(nèi)存是如何分配的的力量是(幸運(yùn)的)非常有限的。你可以選擇自定義的數(shù)據(jù)結(jié)構(gòu)是類(總是在堆上分配的)或結(jié)構(gòu)(在棧中分配,除非它們被包含在一個類中),并且僅此而已。如果你想要更多的神通,必須使用C#的不安全關(guān)鍵字。但是,不安全的代碼只是無法驗(yàn)證的代碼,這意味著它不會在Unity Web Player中運(yùn)行,還可能包括一些其他平臺。由于這個問題和其他原因,不要使用不安全的關(guān)鍵字。因?yàn)槎褩5纳鲜鱿拗?#xff0c;還因?yàn)镃#數(shù)組是只是System.Array(這是一個類)的語法糖,你不能也不應(yīng)該回避自動堆分配。你應(yīng)該避免的是不必要的堆分配,我們會在這個帖子下一個(也是最后一個)部分講到這個。


當(dāng)談到釋放的時候你的力量是一樣的有限。其實(shí),可以釋放堆對象的唯一過程是GC,而它的工作原理是不可見的。你可以影響的是對任何一個對象的最后一個引用在堆中超出范圍的時機(jī),因?yàn)樵诖酥?#xff0c;GC都不能碰他們。這種限制有巨大的實(shí)際意義,因?yàn)橹芷谛缘睦占?#xff08;你無法抑制)往往在沒有什么釋放的時候是非??斓?。這一事實(shí)為構(gòu)建對象池的各種方法提供了基礎(chǔ),我在第三篇帖子討論。


不必要的堆分配的常見原因


你應(yīng)該避免foreach循環(huán)嗎?


在Unity 論壇和其他一些地方我經(jīng)常碰到的常見建議是避免foreach循環(huán),并用for或者while代替。乍一看理由似乎很充分。Foreach真的只是語法糖,因?yàn)榫幾g器會這樣把代碼做預(yù)處理:


復(fù)制代碼
foreach (SomeType s in someList) ??
s.DoSomething();
...into something like the the following:
using (SomeType.Enumerator enumerator = this.someList.GetEnumerator()){ ??
?while (enumerator.MoveNext()) ? ?{ ? ? ??
? ? ? ?SomeType s = (SomeType)enumerator.Current;
? ? ? ?s.DoSomething(); ? ?
}}
復(fù)制代碼
換句話說,每次使用foreach都會在后臺創(chuàng)建一個enumerator對象-一個System.Collections.IEnumerator接口的實(shí)例。但是是創(chuàng)建在堆上的還是在堆棧上的?這是一個好問題,因?yàn)閮煞N都有可能!最重要的是,在System.Collections.Generic 命名空間里幾乎所有的集合類型(List<T>, Dictionary<K, V>, LinkedList<T>, 等等)都會根據(jù)GetEnumerator()的實(shí)現(xiàn)聰明地返回一個struct。這包括伴隨著Mono2.6.5的所有集合版本。(Unity所使用)


Matthew Hanlon指出微軟現(xiàn)在的C#編譯器和Unity正在使用編譯你的腳本的老的Mono/c#編譯器之間一個不幸的差異。你也許知道你可以使用Microsoft Visual Studio來開發(fā)甚至編譯 Unity/Mono 兼容的代碼。你只需要將相應(yīng)的程序集放到‘Assets’目錄下。所有代碼就會在Unity/Mono運(yùn)行時環(huán)境中執(zhí)行。但是,執(zhí)行結(jié)果還是會根據(jù)誰編譯了代碼不一樣。Foreach循環(huán)就是這樣一個例子,這是我才發(fā)現(xiàn)的。盡管兩個編譯器都會識別一個集合的GetEnumerator()返回struct還是class,但是Mono/C#有一個會把struct-enumerator裝箱從而創(chuàng)建一個引用類型的BUG。


所以你覺得你該避免使用foreach循環(huán)嗎?


不要在Unity替你編譯的時候使用
在用最新的編譯器的時候可以使用用來遍歷standard generic collections (List<T> etc.)Visual Studio或者免費(fèi)的 .NET Framework SDK 都可以,而且我猜測最新版的Mono 和 MonoDevelop也可以。
當(dāng)你在用外部編譯器的時候用foreach循環(huán)來遍歷其他類型的集合會怎么樣?很不幸,沒有統(tǒng)一的答案。用在第二篇帖子里提到的技術(shù)自己去發(fā)現(xiàn)哪些集合是可以安全使用foreach的。


你應(yīng)該避免閉包和LINQ嗎?


你可能知道C#提供匿名函數(shù)和lambda表達(dá)式(這兩個幾乎差不多但是不太一樣)。你能分別用delegate 關(guān)鍵字和=>操作符創(chuàng)建他們。他們通常都是很有用的工具,并且你在使用特定的庫函數(shù)的時候很難避免(例如List<T>.Sort()) 或者LINQ。


匿名方法和lambda會造成內(nèi)存泄露嗎?答案是:看情況。C#編譯器實(shí)際上有兩種完全不一樣的方法來處理他們。來看下面小段代碼來理解他們的差異:


復(fù)制代碼
1 int result = 0; ??
2 void Update(){ ??
3 for (int i = 0; i < 100; i++) ? ?{ ? ? ? ?
4 ? ? System.Func<int, int> myFunc = (p) => p * p; ? ? ??
5 ? ? ?result += myFunc(i); ? ?
6 }}
復(fù)制代碼
正如你所看到的,這段代碼似乎每幀創(chuàng)建了myFunc委托 100次,每次都會用它執(zhí)行一個計算。但是Mono僅僅在Update()函數(shù)第一次調(diào)用的時候分配內(nèi)存(我的系統(tǒng)上是52字節(jié)),并且在后續(xù)的幀里不會再做任何堆的分配。怎么回事?使用代碼反射器(我會在下一篇帖子里解釋)就會發(fā)現(xiàn)C#編譯器只是簡單的把myFunc替換為System.Func<int, int>類的一個靜態(tài)域。


我們來對這個委托的定義做一點(diǎn)點(diǎn)改變:


? System.Func<int, int> myFunc = (p) => p * i++;
通過把‘p’替換成’i++’,我們把可以稱為’本地定義的函數(shù)’變成了一個真正的閉包。閉包是函數(shù)式編程的核心。它們把函數(shù)和數(shù)據(jù)綁定在一起-更準(zhǔn)確的說,是和在函數(shù)外定義的非本地變量綁定。在myFunc這個例子里,’p’是一個本地變量但是’i’不是,它屬于Update()函數(shù)的作用域。C#編譯器現(xiàn)在得把myFunc轉(zhuǎn)換成可以訪問甚至改變非本地變量的函數(shù)。它通過聲明(后臺)一個新類來代表myFunc創(chuàng)造時的引用環(huán)境來達(dá)到這個目的。這個類的對象會在我們每次經(jīng)歷for循環(huán)的時候創(chuàng)建,這樣我們就突然有了一個巨大的內(nèi)存泄露(在我的電腦上2.6kb每幀)。


當(dāng)然,在C#3.0引入閉包和其他一些語言特性的主要原因是LINQ。如果閉包會導(dǎo)致內(nèi)存泄露,那在游戲里使用LINQ是安全的嗎?也許我不適合問這個問題,因?yàn)槲铱偸窍穸阄烈咭粯颖苊馐褂肔INQ。LINQ的一部分顯然不會在不支持實(shí)時編譯(jit)的系統(tǒng)上工作,比如IOS。但是從內(nèi)存角度考慮,LINQ也不是好的選擇。一個像這樣基礎(chǔ)到難以置信的表達(dá)式:


?


復(fù)制代碼
1 int[] array = { 1, 2, 3, 6, 7, 8 };
2 void Update(){ ??
3 ?IEnumerable<int> elements = from element in array ? ? ? ? ? ? ? ? ? ?
4 orderby element descending ? ? ? ? ? ? ? ? ??
5 ?where element > 2 ? ? ? ? ? ? ? ? ? ?
6 select element; ? ?...}
復(fù)制代碼
在我的系統(tǒng)上每幀需分配68字節(jié)(Enumerable.OrderByDescending()分配28,Enumerable.Where()40)!這里的元兇甚至不是閉包而是IEnumerable的擴(kuò)展方法:LINQ必須得創(chuàng)建中間數(shù)組以得到最終結(jié)果,并且之后沒有適當(dāng)?shù)南到y(tǒng)來回收。雖然這么說,但我也不是LINQ方面的專家,我也不知道是否部分可以再實(shí)際中可以使用。


協(xié)程


如果你通過StartCoroutine()來啟動一個協(xié)程,你就隱式創(chuàng)建了一個UnityCoroutine類(21字節(jié))和一個Enumerator 類(16字節(jié))的實(shí)例。重要的是,當(dāng)協(xié)程 yield和resume的時候不會再分配內(nèi)存,所以你只需要在游戲運(yùn)行的時候限制StartCoroutine() 的調(diào)用就能避免內(nèi)存泄露。


字符串


對C#和Unity內(nèi)存問題的概論不提及字符串是不完整的。從內(nèi)存角度考慮,字符串是奇怪的,因?yàn)樗鼈兗仁嵌逊峙涞挠质遣豢勺兊?。?dāng)你這樣連接兩個字符串的時候:


1 void Update(){ ??
2 ?string string1 = "Two"; ??
3 ?string string2 = "One" + string1 + "Three";
4 }
運(yùn)行時必須至少分配一個新的string類型來裝結(jié)果。在String.Concat()里這會通過一個叫FastAllocateString()的外部函數(shù)高效的執(zhí)行,但是沒有辦法繞過堆分配(在我的系統(tǒng)里上述例子占用40字節(jié))。如果你需要動態(tài)改變或者連接字符串,使用System.Text.StringBuilder。


裝箱


有時候,數(shù)據(jù)必須在堆棧和堆之間移動。例如當(dāng)你格式化這樣的一個字符串:


string result = string.Format("{0} = {1}", 5, 5.0f);
你是在調(diào)用這樣的函數(shù):


?


1 public static string Format( ? ?
2 string format, ? ?
3 params Object[] args)
換句話說,當(dāng)調(diào)用Format()的時候整數(shù)5和浮點(diǎn)數(shù)’5.0f’必須被轉(zhuǎn)換成System.Object。但是Object是一個引用類型而另外兩個是值類型。C#因此必須在堆上分配內(nèi)存,將值拷貝到堆上去,然后處理Format()到新創(chuàng)建的int和float對象的引用。這個過程就叫裝箱,和它的逆過程拆箱。


對 String.Format()來說這個行為也許不是一個問題,因?yàn)槟阍鯓佣枷M峙涠褍?nèi)存(為新的字符串)。但是裝箱也會在意想不到的地方發(fā)生。最著名的一個例子是發(fā)生在當(dāng)你想要為你自己的值類型實(shí)現(xiàn)等于操作符“==”的時候(例如,代表復(fù)數(shù)的結(jié)構(gòu))。閱讀關(guān)于如果避免隱式裝箱的例子點(diǎn)這里here。


庫函數(shù)


為了結(jié)束這篇帖子,我想說許多庫函數(shù)也包含隱式內(nèi)存分配。發(fā)現(xiàn)它們最好的方法就是通過分析。最近遇到的兩個有趣的例子是:


之前我提到foreach循環(huán)通過大部分的標(biāo)準(zhǔn)泛集合類型并不會導(dǎo)致堆分配。這對Dictionary<K, V>也成立。然而,神奇的是,Dictionary<K, V>集合和Dictionary<K, V>.Value集合是類類型,而不是結(jié)構(gòu)。意味著 “(K key in myDict.Keys)..."需要占用16字節(jié)。真惡心!
List<T>.Reverse()使用標(biāo)準(zhǔn)的原地數(shù)組翻轉(zhuǎn)算法。如果你像我一樣,你會認(rèn)為這意味著不會分配堆內(nèi)存。又錯了,至少在Mono2.6里。有一個擴(kuò)展方法你能使用,但是不像.NET/Mono版本那樣優(yōu)化過,但是避免了堆分配。和使用List<T>.Reverse()一樣使用它:
復(fù)制代碼
public static class ListExtensions{ ? ?
public static void Reverse_NoHeapAlloc<T>(this List<T> list) ? ?{ ? ? ??
? ? ?int count = list.Count; ? ? ??
? ? ?for (int i = 0; i < count / 2; i++) ? ? ? ?{?
? ? ? ? ? ? ? T tmp = list[i]; ? ? ? ? ?
? ? ? ? list[i] = list[count - i - 1]; ? ? ? ? ? ?
? ? ?list[count - i - 1] = tmp; ? ? ? ?
} ? ?
}} ? ? ? ? ? ? ? ? ? ?




在 .NET/Mono 和Unity里內(nèi)存管理的基礎(chǔ),并且提供了一些避免不必要的堆分配的建議。第三篇會深入到對象池。所有的都主要是面向中級的C#開發(fā)者。


我們現(xiàn)在來看看兩種發(fā)現(xiàn)項目中不想要的堆分配的方法。第一種-Unity profiler-實(shí)在是太簡單了,但是卻相當(dāng)費(fèi)錢,得買’pro‘版的。第二種是講你的.NET/Mono程序集反匯編成中間語言(CIL)然后再檢查。如果你從沒見過反匯編的.NET代碼,繼續(xù)看下去,不難,而且免費(fèi)還很有啟發(fā)意義。


容易的方法:使用Unity profiler


Unity優(yōu)秀的分析器主要被用來分析游戲中各種資源需要的性能和資源:著色器,紋理,音頻,游戲?qū)ο蟮鹊取H欢治銎髟诎l(fā)掘內(nèi)存上也一樣有用-跟你的C#代碼的行為有關(guān)-甚至是外部的 沒引用UnityEngine.dll的.NET/Mono程序集!在當(dāng)前Unity版本中(4.3),這個功能不是來自內(nèi)存分析器,而是CPU分析器。到C#代碼的時候,內(nèi)存分析器只是展示Mono堆的總大小和已使用的量。






這樣讓你看你的C#代碼是否有嫩村泄露實(shí)在太粗糙了。即使不適用任何腳本,已使用的堆大小也會持續(xù)增長和縮減。只要你使用腳本,你需要一個看哪里分配了內(nèi)存的途徑,然后CPU分析器剛好給你提供這個。


讓我們來看看一些實(shí)例代碼。假設(shè)下面的腳本綁定到了一個GameObject上。


復(fù)制代碼
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); ? ?}}
復(fù)制代碼
它所做的就是通過一組整數(shù)用一種繞的方法創(chuàng)建了一個字符串("Hello world!"),一路上造成了不必要的內(nèi)存分配。多少呢?很高興你問了,但是我很懶,就讓我們看看CPU分析器吧。選中窗口頂部的”Deep Profiler“,可以跟蹤到每幀的調(diào)用樹。






正如你所見,堆內(nèi)存在Update()函數(shù)過程中的5個不同位置被分配。這個列表的初始化,foreach循環(huán)里到數(shù)組的轉(zhuǎn)換是多余的,每一個數(shù)字到字符的轉(zhuǎn)換以及連接都需要分配內(nèi)存。有趣的是,僅僅是調(diào)用Debug.Log()也會分配一大塊內(nèi)存-這點(diǎn)值得記下來,即使在生產(chǎn)環(huán)境中這段代碼會被剔除。


如果你沒有Unity Pro,但是恰巧有Microsoft Visual Studio,那就有替代Unity Profiler的方法來發(fā)掘調(diào)用堆棧。Telerik 告訴我他們的 JustTrace Memory profiler 有相似的功能 (see here). 然而, 我不知道它模仿Unity每幀記錄調(diào)用樹到了什么程度。更進(jìn)一步,盡管對Unity項目的遠(yuǎn)程調(diào)試(通過UnityVS) 是可以的,我還是沒有成功的把JustTrace用來分析被Unity調(diào)用的程序集。


只是稍微難一點(diǎn)點(diǎn)的方法:反匯編你的代碼


CIL的背景知識


如果你已經(jīng)有了一個.NET/Mono的反匯編器,開始用吧,不然我推薦ILSpy. 這個工具不僅是免費(fèi)的,它還非常干凈簡單,但是剛好包含下面我們會用到的一個特殊功能。


你也許知道C#編譯器不會將你的代碼編譯成機(jī)器語言,而是公共中間語言。這種語言是被原.NET團(tuán)隊作為一種包含兩種來自高級語言特性的低級語言開發(fā)出來的。一方面,它與硬件無關(guān),另一方面,它包含最適合被稱為’面向?qū)ο蟆奶匦?比如可以引用其他模塊或者類的能力。


沒有經(jīng)過代碼模糊處理( code obfuscator )的CIL代碼是異常容易反向工程的。 許多情況下,結(jié)果幾乎和原始的C#(VB)代碼一樣。ILSpy 可以替你做這件事,但是我們僅僅反匯編代碼就可以了(ILSpy通過調(diào)用ildasm.exe來實(shí)現(xiàn),.它是NET/Mono的一部分)。讓我們從一個加兩個整數(shù)的函數(shù)開始。


1 int AddTwoInts(int first, int second){ ? ?
2 int result = first + second; ? ? ? ? ??
3 ?return result;
4 }
如果你愿意,你可以將這段代碼粘貼到MemoryAllocatingScript.cs文件里。然后確保Unity編譯了它,再用ILSpy打開編譯了的庫Assembly-Csharp.dll。如果你選擇AddTwoInts() 方法,你會看到下面的:






除了藍(lán)色的關(guān)鍵字 hidebysig,我們可以忽略掉,方法簽名應(yīng)該看起來差不多。要了解到方法里主要發(fā)生了什么,你需要知道CIL把CPU看成一個堆棧式機(jī)器stack machine 而不是寄存器機(jī)器register machine。CIL假設(shè)CPU可以處理非?;A(chǔ),非常算法的指令,例如”將兩個整數(shù)相加“,而且它可以處理任何內(nèi)存地址的隨機(jī)訪問。CIL還假設(shè)CPU不直接在RAM上進(jìn)行算術(shù)操作,而是首先需要將數(shù)據(jù)裝載進(jìn)概念上的計算堆棧。(注意計算堆棧和你你知道的C#堆棧沒有任何關(guān)系。CIL計算堆棧只是一個抽象的,并且預(yù)設(shè)很小。)在行IL_0000到IL_0005發(fā)生了:


兩個整型參數(shù)被推進(jìn)堆棧。
加法被調(diào)用然后從堆棧里彈出開始位置的兩個對象,自動將記過壓進(jìn)堆棧。
第3和4行可以忽略,因?yàn)樵诎l(fā)行版本里會被優(yōu)化掉。
這個方法返回堆棧的第一個值。
找到CIL里面的內(nèi)存分配


CIL代碼美在它不會隱藏任何堆分配。而且,堆分配會嚴(yán)格按照以下三個順序分配,在你的反匯編代碼里能看到。


newobj <constructor>:這創(chuàng)建了一個由constructor指定類型的未初始化的對象。如果這個對象是值類型,它就在堆棧上被創(chuàng)建。如果它是一個引用類型,就在堆上。你總是能從CIL代碼知道類型,所以你可以容易的知道內(nèi)存分配產(chǎn)生的地方。
newarr <element type>:這條指令在堆上創(chuàng)建了一個新的數(shù)組。Element的類型由參數(shù)指定。
box <value type token>:這條特殊的指令執(zhí)行裝箱操作,我們已經(jīng)在第一篇帖子里說過。
Let's look at a rather contrived method that performs all three types of allocations.


然我們來看一個人為的執(zhí)行這三種內(nèi)存分配的方法。


復(fù)制代碼
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 }
復(fù)制代碼
有這幾行代碼產(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()


正如我們懷疑過的,對象的數(shù)組(SomeMethod()里的第一行)導(dǎo)致newarr指令。整數(shù)5被賦給數(shù)組的第一個元素需要裝箱。Dictionary<int, int>是被newobj指令分配的。


但是還有第四個堆分配!正如我在第一篇帖子里提到的,Dictionary<K, V>. KeyCollection被聲明為一個類,不是結(jié)構(gòu)。這個類的一個實(shí)例會被創(chuàng)建,這樣foreach蓄奴換才有迭代的對象。不幸的是,分配發(fā)生在Keys屬性的getter方法里。正如你在CIL代碼里看到,這個方法的名字是get_Keys(),而且它的返回值是一個類。


作為一個查找內(nèi)存泄露的通用方法,你可以生成一個對你的整個程序集反匯編的CIL文件,只要在ILSpy按下Ctrl+S。然后用你喜歡的文本編輯器打開這個文件,搜索上面提到的三種指令。查出其他程序集里的內(nèi)存泄露是有難度。我唯一知道的辦法就是仔細(xì)檢查你的C#代碼,確認(rèn)所有的外部方法調(diào)用,并且一個個地查看它們的CIL代碼。你怎么知道什么時候就完成了?很簡單:你的游戲可以流暢的運(yùn)行好幾個小時,不因?yàn)槔占斐扇魏蔚男阅芷款i。


PS:在之前的帖子里,我答應(yīng)要向你們展示如何確認(rèn)你們系統(tǒng)上的Mono版本。只要裝了ILSpy,沒有比這更簡單的了。在ILSpy里,點(diǎn)擊打開然后找到Unity根目錄。找到Data/Mono/lib/mono/2.0然后打開mscorlib.dll。在層級視圖里,找到mscorlib/-/Consts,然后那兒你能找到MonoVersion作為一個字符串常量。



總結(jié)

以上是生活随笔為你收集整理的Unity开发者的C#内存管理的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網(wǎng)站內(nèi)容還不錯,歡迎將生活随笔推薦給好友。