TLDR;
Unity堅定的擁抱.NET標準生態,正全速向CoreCLR遷移。
Mono vs CoreCLR
對于一個C#的初學者,首先要了解的便是.NET和C#的關系。所以這里不再贅述。對于一個Unity的初學者,在使用C#編碼的過程中,一定會遇到一些C#新特性不能在項目中使用的情況,這是因為微軟官方提供的.NET運行時環境(最新版為 .NET 6 的?CoreCLR
)遠比Unity集成的Mono
強大。由于歷史原因,Unity一直未能使用最新的.NET運行時。本文就來細說一下其中的歷史,以及Unity未來的發展。
首先,Mono
是.NET的開源實現,由Xamarin牽頭維護 mono/mono 這個repo。2016年Xamarin被微軟收購,將其license從GPL改成了MIT,同時微軟也參與到Mono的開發中。在微軟的 dotnet/runtime 這個repo中,可以發現mono,但這并不是mono/mono的替代品,而是微軟為了方便其他組件開發,將部分代碼拷貝過來進行魔改,在需要的時候同步回mono/mono。(出處:Announcement: Consolidating .NET)
然后說一下?CoreCLR
。微軟改名部絕非浪得虛名,這些年的一系列改名操作把好端端的.NET技術攪成一灘渾水。非常簡要的說,過去.NET運行時只能用于Windows平臺,名為 .NET Framework,運行時叫CLR
,大名鼎鼎的《CLR via C#》就是基于該運行時。后來微軟決定將.NET技術開源,并徹底地跨平臺(Windows,Linux,MacOS等),順便大幅提高運行效率并拋棄一些舊組件,重寫了一版 .NET Core,運行時叫?CoreCLR
。雙線開發并不是個好主意,因此再后來微軟決定將前者廢棄,以 .NET Core相關技術為核心,將.NET技術大一統(桌面開發、移動開發、游戲開發、IoT開發、云開發等等),史稱 .NET 5。因此,往后.NET的官方運行時就叫CoreCLR
。repo在這里:dotnet/runtime。
大一統的餅是畫出來了,但當下移動平臺的.NET開發的主流還是Mono
。這里結合Wiki,對Mono的重要歷史加以整理。
2010年9月,Mono2.8發布,帶來了新的分代式GC:
SGen
;支持 C#4;支持 .NET 4.0。2013年7月,Mono3.2發布,
SGen
取代Boehm
成為默認的GC。2015年4月,Mono4.0發布,開始集成 .NET Core;支持 C#6。
2017年5月,Mono5.0發布,開啟了concurrent SGen;使用Roslyn編譯器;支持 C#7。
2019年9月,Mono6.4發布,支持?
.NET Standard 2.1
;支持 .NET 4.8。
然而Mono對.NET的特性支持和性能一直落后于微軟官方的CLR,性能上也大幅落后于CoreCLR(補充:2018年Unity的官方數據是CLR比Mono快30%-3倍。Mono這些年的進步有目共睹,但奈何不了微軟爸爸的鈔能力給CoreCLR研發上的加成。經過4年的發展,一些民間測試資料已顯示CoreCLR比Mono快10-20倍,比IL2CPP快8-10倍)。
過去,Unity 選擇 Mono
上文理清了Mono
?和CoreCLR
的關系,下面說說Unity在這二者間的選擇。
早在2008年,Unity就宣布和Mono合作,但后續Mono新版本使用SGen GC取代Boehm GC時,Unity不想再次付許可證費用。直到2021年7月,Unity依然依賴 Boehm GC。這是一種沒有分代的(掃描慢)、Mark-Sweep的(會有內存碎片問題)、保守(不能精確地識別垃圾)的GC。
Unity has been and is still relying on the?Boehm GC, which is a conservative (stack-root) GC. The link above doesn't go into some details like how managed objects on the stack are collected by the GC, but basically: a conservative GC will scan the entire stack of all managed threads to "pin" memory referenced by it. Because of this blind scan, it can bring false pin, because it can interpret an integer value, as a pointer to a region of the heap memory, while it was really an int in the first place. By doing so, a conservative GC can start to block some objects from being collected (or worse for a moving-generational GC, to relocate objects). Otoh, a precise GC is able to scan precisely stack-roots and report only pointers that actually point to heap memory. In order for a precise GC to work, the (JIT) codegen needs to be GC aware, which is the case for CoreCLR.
對于Boehm GC造成的性能問題,Unity官方有一些折中方案。
先盡量分配好所有對象的內存,然后關閉GC,等到合適的時機(如關卡結束),再開啟GC;
默認開啟 incremental 模式分幀處理,注意如果在期間有大量引用關系的改寫,分幀處理反而會有大量額外性能損耗(主要來自寫屏障)
未來,Unity 選擇 CoreCLR
自2016年微軟將Mono的許可證由GPL改為MIT以來,Unity也加入了?.NET Foundation
,開始將最新的Mono集成到自己的引擎中。但隨著微軟構筑開源的大一統解決方案 .NET 5,Unity似乎改變了原先的想法。從官方論壇中可以總結出他們的規劃:
首先集成最新的Mono,因為其支持 .NET Core 的BCL;
然后將自家的 IL2CPP 也更新(其依賴Mono的輸出結果);
Unity 2021.2開始完全支持.NET Standard 2.1,C#8和部分C#9(Span,Range,default interface methods),其中
Span
的影響非常深遠,目前和BurstCompiler的NativeArray還不能無縫轉換,最大難點是Span并沒有自己的memory但后者有;支持 C#9/10,基于前面的工作,這一步并不難;
支持 .NET 6(跳過 .NET 5)。但有兩大難題:
所有dll必須重新編譯;
要修改 UnityEditor 中大量使用 AppDomain進行hot reload的部分(AppDomain在新版.NET中幾乎被廢棄,出處);目前的替代類 AssemblyLoadContext 并不能提供之前 AppDomain Reload的所有功能。
In general Assembly Load Context is cooperative, and any remaining references (static fields, GC Handles, running threads, etc) will prevent the code from being unloaded.
6. 用CoreCLR替代Mono(GC也相應升級為CoreCLR GC),在此之前,GC并不會升級為Mono的SGen。這項工作會持續比較久,目前還沒有ETA。Unity大部分代碼是C++,C#只有薄薄的一層(但是越來越多的代碼在切換到 C#)。在切換到CoreCLR后,其訪問Managed Object的方式需要徹底改變,因此改動會很大。總體順序是:先將Player替換為CoreCLR,然后將Editor也替換掉。
長遠來看,Unity該團隊已經意識到自己當年的一些輪子(Coroutine, Customized Boehm GC, IL2CPP, asmdef等)在近幾年來.NET運行時、工具鏈和整套生態的飛速發展面前顯得有些陳舊,正在致力于向開發者提供原汁原味的.NET開發體驗(出處),同時盡量不顛覆原有的使用習慣(例如出于這些原因,UPM
不會和NuGet
雙向互通)。
另外,Unity團隊還有很多高優先級的feature要做,希望Unity越來越好吧。最新消息可關注官方論壇的討論:
Unity Future .NET Development Status - Unity Forumforum.unity.com/threads/unity-future-net-development-status.1092205/
另外,文中提到的《CLR via C#》雖然內容基于 .NET Framework,但由于大部分內容在 .NET5+ 中并沒有變化,該書憑借其內容深入和廣度,依然是 C# 進階學習的必讀經典。