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

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程语言 > C# >内容正文

C#

Unity开发者的C#内存管理

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


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






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


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


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


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


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


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


自己做內存管理者


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


當談到釋放的時候你的力量是一樣的有限。其實,可以釋放堆對象的唯一過程是GC,而它的工作原理是不可見的。你可以影響的是對任何一個對象的最后一個引用在堆中超出范圍的時機,因為在此之前,GC都不能碰他們。這種限制有巨大的實際意義,因為周期性的垃圾收集(你無法抑制)往往在沒有什么釋放的時候是非常快的。這一事實為構建對象池的各種方法提供了基礎,我在第三篇帖子討論。


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


你應該避免foreach循環嗎?


在Unity 論壇和其他一些地方我經常碰到的常見建議是避免foreach循環,并用for或者while代替。乍一看理由似乎很充分。Foreach真的只是語法糖,因為編譯器會這樣把代碼做預處理:


復制代碼
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(); ? ?
}}
復制代碼
換句話說,每次使用foreach都會在后臺創建一個enumerator對象-一個System.Collections.IEnumerator接口的實例。但是是創建在堆上的還是在堆棧上的?這是一個好問題,因為兩種都有可能!最重要的是,在System.Collections.Generic 命名空間里幾乎所有的集合類型(List<T>, Dictionary<K, V>, LinkedList<T>, 等等)都會根據GetEnumerator()的實現聰明地返回一個struct。這包括伴隨著Mono2.6.5的所有集合版本。(Unity所使用)


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


所以你覺得你該避免使用foreach循環嗎?


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


你應該避免閉包和LINQ嗎?


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


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


復制代碼
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 }}
復制代碼
正如你所看到的,這段代碼似乎每幀創建了myFunc委托 100次,每次都會用它執行一個計算。但是Mono僅僅在Update()函數第一次調用的時候分配內存(我的系統上是52字節),并且在后續的幀里不會再做任何堆的分配。怎么回事?使用代碼反射器(我會在下一篇帖子里解釋)就會發現C#編譯器只是簡單的把myFunc替換為System.Func<int, int>類的一個靜態域。


我們來對這個委托的定義做一點點改變:


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


當然,在C#3.0引入閉包和其他一些語言特性的主要原因是LINQ。如果閉包會導致內存泄露,那在游戲里使用LINQ是安全的嗎?也許我不適合問這個問題,因為我總是像躲瘟疫一樣避免使用LINQ。LINQ的一部分顯然不會在不支持實時編譯(jit)的系統上工作,比如IOS。但是從內存角度考慮,LINQ也不是好的選擇。一個像這樣基礎到難以置信的表達式:


?


復制代碼
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; ? ?...}
復制代碼
在我的系統上每幀需分配68字節(Enumerable.OrderByDescending()分配28,Enumerable.Where()40)!這里的元兇甚至不是閉包而是IEnumerable的擴展方法:LINQ必須得創建中間數組以得到最終結果,并且之后沒有適當的系統來回收。雖然這么說,但我也不是LINQ方面的專家,我也不知道是否部分可以再實際中可以使用。


協程


如果你通過StartCoroutine()來啟動一個協程,你就隱式創建了一個UnityCoroutine類(21字節)和一個Enumerator 類(16字節)的實例。重要的是,當協程 yield和resume的時候不會再分配內存,所以你只需要在游戲運行的時候限制StartCoroutine() 的調用就能避免內存泄露。


字符串


對C#和Unity內存問題的概論不提及字符串是不完整的。從內存角度考慮,字符串是奇怪的,因為它們既是堆分配的又是不可變的。當你這樣連接兩個字符串的時候:


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


裝箱


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


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


?


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


對 String.Format()來說這個行為也許不是一個問題,因為你怎樣都希望它分配堆內存(為新的字符串)。但是裝箱也會在意想不到的地方發生。最著名的一個例子是發生在當你想要為你自己的值類型實現等于操作符“==”的時候(例如,代表復數的結構)。閱讀關于如果避免隱式裝箱的例子點這里here。


庫函數


為了結束這篇帖子,我想說許多庫函數也包含隱式內存分配。發現它們最好的方法就是通過分析。最近遇到的兩個有趣的例子是:


之前我提到foreach循環通過大部分的標準泛集合類型并不會導致堆分配。這對Dictionary<K, V>也成立。然而,神奇的是,Dictionary<K, V>集合和Dictionary<K, V>.Value集合是類類型,而不是結構。意味著 “(K key in myDict.Keys)..."需要占用16字節。真惡心!
List<T>.Reverse()使用標準的原地數組翻轉算法。如果你像我一樣,你會認為這意味著不會分配堆內存。又錯了,至少在Mono2.6里。有一個擴展方法你能使用,但是不像.NET/Mono版本那樣優化過,但是避免了堆分配。和使用List<T>.Reverse()一樣使用它:
復制代碼
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里內存管理的基礎,并且提供了一些避免不必要的堆分配的建議。第三篇會深入到對象池。所有的都主要是面向中級的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上。


復制代碼
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); ? ?}}
復制代碼
它所做的就是通過一組整數用一種繞的方法創建了一個字符串("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的一部分)。讓我們從一個加兩個整數的函數開始。


1 int AddTwoInts(int first, int second){ ? ?
2 int result = first + second; ? ? ? ? ??
3 ?return result;
4 }
如果你愿意,你可以將這段代碼粘貼到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.


然我們來看一個人為的執行這三種內存分配的方法。


復制代碼
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 }
復制代碼
有這幾行代碼產生的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#内存管理的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。