Unity-托管代碼剝離(不發(fā)表)
托管代碼剝離將從構(gòu)建中刪除未使用的代碼,從而可以顯著減小最終構(gòu)建大小。使用 IL2CPP 腳本后端時,托管代碼剝離還可以減少構(gòu)建時間,因為需要轉(zhuǎn)換為 C++ 并進行編譯的代碼減少。托管代碼剝離將從托管程序集(包括從項目中的 C# 腳本構(gòu)建的程序集、包含在包和插件中的程序集以及 .NET 框架中的程序集)中刪除代碼。
托管代碼剝離的工作方式是對項目中的代碼進行靜態(tài)分析,檢測出在執(zhí)行過程中永遠無法訪問的類、類成員甚至函數(shù)的某些部分??梢酝ㄟ^?Player Settings?窗口中的?Managed Stripping Level?設(shè)置(在?Optimization?部分)來控制 Unity 刪除無法訪問的代碼的激進程度。
__重要信息:__當代碼(或插件中的代碼)使用反射來動態(tài)查找類或成員時,代碼剝離工具不能總是檢測出項目是否正在使用這些類或成員,因此可能會刪除它們。要聲明某個項目正在使用這樣的代碼,請使用?link.xml 文件或?Preserve?屬性。
托管剝離級別
使用項目的?Player Settings?中的?Managed Stripping Level?選項來控制 Unity 刪除未使用代碼的激進程度。

注意:__此選項的默認值根據(jù)?Scripting Backend__ 的當前設(shè)置而不同。
屬性功能Disabled不刪除代碼。
此選項是 Mono 腳本后端的默認剝離級別。當選擇 IL2CPP 腳本后端時,由于其對構(gòu)建時間的影響,Disabled 選項不可用。托管代碼越多意味著 IL2CPP 要生成的 C++ 代碼越多,也意味著需要編譯的 C++ 代碼越多。導(dǎo)致的結(jié)果是,從進行代碼更改到看到更改生效所間隔的時間大幅增加。Low根據(jù)一組保守的規(guī)則刪除代碼,因此應(yīng)該會刪除大多數(shù)無法訪問的代碼,同時盡量降低實際使用的代碼被剝離的可能性。剝離級別低有利于確??捎眯裕焕跍p小代碼大小。
此選項是 IL2CPP 的默認剝離級別(并且已用于 Unity Editor 的許多發(fā)行版)。Medium根據(jù)一組規(guī)則刪除代碼,確保在低 (Low) 剝離級別和高 (High) 剝離級別之間取得平衡。中等 (Medium) 剝離級別不像低剝離級別那么謹慎,也不如高剝離級別那么極端。因此,因刪除代碼而導(dǎo)致意外副作用的風險大于低剝離級別,但小于高剝離級別。
如果將中等剝離級別用于 IL2CPP 腳本后端,可進一步縮短代碼更改與測試之間的迭代時間。
使用 .NET 3.5 腳本運行時版本 (.NET 3.5 Scripting Runtime Version) 設(shè)置時,中等剝離級別不可用。High盡可能刪除無法訪問的代碼,并生成比中等 (Medium) 剝離級別更小的構(gòu)建。高 (High) 剝離級別優(yōu)先考慮減小代碼大小,而不是確??捎眯裕荒赡苄枰砑?link.xml 文件、Preserve 屬性或重寫有問題的代碼部分。
高剝離級別會執(zhí)行更耗時的分析,以進一步減小大小,因此構(gòu)建和迭代時間可能比中等剝離級別下的時間更長。
當使用 .NET 3.5 腳本運行時版本 (.NET 3.5 Scripting Runtime Version) 設(shè)置時,高剝離級別不可用。
注意__:__Managed Stripping Level?選項不會影響刪除未使用 Unity 引擎代碼的過程(使用?IL2CPP Scripting Backend?設(shè)置時可用)。
了解托管代碼剝離
本部分介紹托管代碼剝離的詳細信息,以及如何識別和糾正可能出現(xiàn)的任何相關(guān)問題。
在 Unity 中構(gòu)建項目時,構(gòu)建過程將 C# 代碼編譯為稱為公共中間語言 (CIL) 的 .NET 字節(jié)碼格式。此 CIL 字節(jié)碼被打包到稱為程序集的文件中。同樣,.NET 框架庫以及在項目中使用的插件中的所有 C# 庫也都會預(yù)先打包為 CIL 字節(jié)碼的程序集。通常,無論項目使用程序集的多少代碼,構(gòu)建過程始終包括整個程序集文件。
托管代碼剝離過程將分析項目中的程序集,以查找和刪除未實際使用的代碼。分析過程使用一組規(guī)則來確定要保留的代碼和要丟棄的代碼。這些規(guī)則將在構(gòu)建大小(包含太多代碼)與風險(刪除太多代碼)之間進行權(quán)衡。Managed Stripping Level?設(shè)置可用于控制刪除代碼的激進程度。
UnityLinker
Unity 構(gòu)建過程使用一個名為?UnityLinker?的工具來剝離托管代碼。UnityLinker 是?Mono IL Linker?的一個定制版本,專為 Unity 設(shè)計。UnityLinker 基于我們的項目分叉,此分叉密切跟蹤上游 IL Linker 項目。(請注意,該分叉中未維護 UnityLinker 的 Unity 引擎特有自定義部分。)
UnityLinker 的工作方式
UnityLinker 將分析項目中的所有程序集。首先標記頂級、根類型、方法、屬性、字段等,例如,向場景中的游戲?qū)ο筇砑拥?MonoBehaviour 派生類便是根類型。然后,UnityLinker 分析已標記為要進行識別的根,并標記這些根所依賴的托管代碼。完成此靜態(tài)分析后,所有剩余的未標記代碼都無法通過應(yīng)用程序代碼中的任何執(zhí)行路徑來訪問,并將從程序集中刪除。
請注意,這一過程不會對代碼進行混淆處理。
反射和代碼剝離
UnityLinker 不能總是檢測出項目中的代碼通過反射時來引用其他代碼的實例,因此可能會誤刪除實際在使用的代碼。將?Managed Stripping Level?設(shè)置從?Low?提升為?High?時,代碼剝離導(dǎo)致游戲中發(fā)生意外行為變化的風險也會增加。這種行為變化小到細微的邏輯變化,大到調(diào)用缺失方法造成的崩潰。
UnityLinker 能夠檢測和處理一些反射模式。如需查看該工具可以處理的最新模式的示例,請參閱 Mono IL Linker?反射測試套件。但是,如果使用的不僅僅是簡單的反射,必須給 UnityLinker 一些提示,說明哪些類不應(yīng)該被處理。可以通過?link.xml?文件和?Preserve?屬性的形式提供這些提示:
Preserve 屬性 — 直接在源代碼中標記要保留的元素。
link.xml 文件 — 聲明應(yīng)如何保留程序集中的元素。
UnityLinker 在分析程序集中未使用的代碼時,會將使用屬性或 link.xml 文件保留的每個元素視為根元素。
Preserve 屬性
在源代碼中使用 [Preserve] 屬性可防止 UnityLinker 剝離該代碼。下面的列表描述了在對不同的代碼元素應(yīng)用 Preserve 屬性時 UnityLinker 會保留哪些實體:
__Assembly__:保留程序集內(nèi)的所有類型(就好像您為每個類型輸入了 [Preserve] 屬性一樣)。要為程序集分配 Preserve 屬性,請將該屬性聲明放在程序集包含的任何 C# 文件中,但需在所有命名空間聲明之外:
__Type__:保留類型及其默認構(gòu)造函數(shù)。
__Method__:保留方法、其聲明類型、返回類型及其所有參數(shù)的類型。
__Property__:保留屬性、其聲明類型、值類型、getter 方法以及 setter 方法。
__Field__:保留字段、其聲明類型和字段類型。
__Event__:保留事件、其聲明類型、返回類型、add 方法以及 remove 方法。
__Delegate__:保留委派類型及其所有方法。
請注意,相比使用 Preserve 屬性,在 link.xml 文件中標記代碼實體可以提供更強的控制。例如,用 Preserve 屬性修飾某個類既會保留類型,也會保留默認構(gòu)造函數(shù)。而使用 link.xml 文件,可以選擇只保留類型(不保留默認構(gòu)造函數(shù))。
可以在任何程序集中和任何命名空間中定義 Preserve 屬性。因此,可以使用 UnityEngine.Scripting.PreserveAttribute 類、為其創(chuàng)建子類或創(chuàng)建您自己的?PreserveAttribute?類,例如:
AlwaysLinkAssembly 屬性
使用 [assembly: UnityEngine.Scripting.AlwaysLinkAssembly] 屬性可強制 UnityLinker 處理程序集(無論程序集是否被構(gòu)建中包含的另一個程序集引用)。AlwaysLinkAssembly?屬性只能在程序集上定義。
此屬性僅指示 UnityLinker 將其__根標記規(guī)則 (Root Marking Rules)__ 應(yīng)用于程序集。該屬性本身不會直接使程序集中的代碼被保留。如果沒有代碼元素與程序集的根標記規(guī)則匹配,UnityLinker 仍會從構(gòu)建中刪除程序集。
可在包含一個或多個方法的包或預(yù)編譯程序集上將此屬性與 [RuntimeInitializeOnLoadMethod] 屬性結(jié)合使用,但后者可能不包含在項目的任何場景中直接或間接使用的類型。
如果程序集定義 [assembly: AlwaysLinkAssembly] 屬性并由構(gòu)建中包含的其他程序集所引用,則該屬性對于輸出沒有任何影響。
Link XML
link.xml 文件是一個基于項目的列表,其中聲明如何保留程序集以及程序集中的類型和其他代碼實體。要使用 link.xml 文件,請創(chuàng)建此文件(請參閱下面的示例),并將其放入項目的 Assets 文件夾(或者 Assets 文件夾的任何子目錄)??梢栽陧椖恐惺褂萌我鈹?shù)量的 link.xml 文件,因此插件可以提供自己的保留聲明。UnityLinker 會將 link.xml 文件中保留的任何程序集、類型或成員都視為根類型。
請注意,在軟件包中不支持 link.xml 文件,但可以從非軟件包 link.xml 文件中引用軟件包程序集。
以下示例說明了使用 link.xml 文件聲明項目程序集的根類型時可以采用的不同方式:
特殊程序集 XML 屬性
link.xml 文件的 <assembly> 元素有三個特殊用途的屬性:
ignoreIfMissing?默認情況下,如果找不到 link.xml 文件中引用的程序集,那么 UnityLinker 將中止構(gòu)建。如果需要聲明在所有播放器構(gòu)建過程中不存在的程序集的保留,請在 link.xml 文件中的 <assembly> 元素上使用 ignoreIfMissing 屬性:
ignoreIfUnreferenced
在某些情況下,您可能希望僅當程序集被其他程序集引用時才保留程序集中的實體??梢栽?link.xml 文件中的 <assembly> 元素上使用 ignoreIfUnreferenced 屬性,這樣僅當程序集中引用至少一種類型時才保留程序集中的實體。
<linker> ?<assembly fullname="Bar" ignoreIfUnreferenced="1"> ? ?<type name="Type2"/> ?</assembly> </linker>
__注意:__進行引用的程序集中的代碼本身是否被剝離并不重要,被引用的程序集中具有此屬性的指定元素仍會被保留。
windowsruntime
為 Windows 運行時元數(shù)據(jù) (.winmd) 程序集定義保留時,必須在 link.xml 文件中的 <assembly> 元素中添加 windowsruntime=“true” 屬性:
<linker> ?<assembly fullname="Windows" windowsruntime="true"> ? ?<type name="Type3"/> ?</assembly> </linker>
UnityLinker 剝離程序集的方式
Unity Editor 會將包含 Unity 項目任何場景使用的類型的程序集列表進行合并,并將其傳遞給 UnityLinker。然后,UnityLinker 處理這些程序集、這些程序集的任何引用、link.xml 文件中聲明的任何程序集以及具有 AlwaysLinkAssembly 屬性的任何程序集。通常,項目中不屬于這些類別的任何程序集都不會被 UnityLinker 處理,也不會包含在播放器構(gòu)建中。
UnityLinker 在處理每個程序集時遵循一組規(guī)則,這組規(guī)則基于程序集分類,程序集是否包含場景中使用的類型,以及為構(gòu)建選擇的?Managed Stripping Level?屬性值。
根據(jù)這些規(guī)則的用途,程序集劃為以下幾種分類:
.NET 類庫程序集?— 包括 Mono 類庫(如 mscorlib.dll 和 System.dll)以及 .NET 類庫外觀程序集(如 netstandard.dll)。
平臺 SDK 程序集?— 包括特定于平臺 SDK 的托管程序集。例如,通用 Windows 平臺 SDK 中包含的 windows.winmd 程序集。
Unity 引擎模塊程序集?— 包括構(gòu)成 Unity 引擎的托管程序集,如 UnityEngine.Core.dll。
項目程序集?— 包括特定于項目的程序集,例如:
腳本程序集,如 Assembly-CSharp.dll
預(yù)編譯的程序集
程序集定義程序集
軟件包程序集
以下各節(jié)將詳細介紹 UnityLinker 如何針對每種?Managed Stripping Level?設(shè)置進行標記以及保留或剝離程序集代碼:
低剝離級別
中等剝離級別
高剝離級別
低剝離級別
將?Managed Stripping Level?設(shè)置為?Low?時,UnityLinker 將根據(jù)一組保守的規(guī)則刪除代碼,因此應(yīng)該會刪除大多數(shù)無法訪問的代碼,同時盡量降低實際使用的代碼被剝離的可能性。__低__剝離級別優(yōu)先考慮可用性,其次才是減小代碼大小。
低剝離級別的根標記規(guī)則
根標記規(guī)則決定了 UnityLinker 如何識別程序集中的頂級類型。
程序集類型操作根標記規(guī)則.NET 類和平臺 SDK剝離應(yīng)用預(yù)防性保留任何 link.xml 中定義的保留包含場景中所用類型的程序集復(fù)制標記程序集中的所有類型和成員其他全部剝離標記所有公共類型標記公共類型的所有公共成員標記包含 [RuntimeInitializeOnLoadMethod] 屬性的方法標記包含 [Preserve] 屬性的類型和成員任何 link.xml 中定義的保留在下列程序集中標記派生自 MonoBehaviour 和 ScriptableObject 的所有類型:
預(yù)編譯程序集
軟件包程序集
程序集定義程序集
Unity 腳本程序集測試剝離標記包含 NUnit.Framework 中定義的任何屬性的方法。例如:[Test]標記包含 [UnityTest] 屬性的方法
注意:____剝離__操作意味著 UnityLinker 會分析程序集來找出可以刪除的代碼。復(fù)制__操作意味著 UnityLinker 可將整個程序集復(fù)制到最終構(gòu)建(并且將其中的所有類型都標記為根類型)。
低剝離級別的依賴項標記規(guī)則
標記根類型后,UnityLinker 將執(zhí)行靜態(tài)分析以識別這些根所依賴的所有代碼。
規(guī)則目標描述Unity 類型當 UnityLinker 標記派生自 MonoBehaviour 的類型時,還會標記該類型的所有成員。當 UnityLinker 標記派生自 ScriptableObject 的類型時,還會標記該類型的所有成員。特性UnityLinker 在所有標記的程序集、類型、方法、字段、屬性等對象中標記相應(yīng)屬性。調(diào)試屬性啟用腳本調(diào)試時,UnityLinker 將標記具有 [DebuggerDisplay] 屬性的所有成員(即使沒有代碼路徑使用該成員)。.NET 外觀類庫程序集外觀程序集是指 .NET 類庫中負責將類型定義轉(zhuǎn)發(fā)給另一個程序集的程序集。例如,netstandard.dll
(屬于 .NET Standard 2.0 API 兼容性級別)就是一個外觀程序集,它定義 .NET 接口,但將該接口的實現(xiàn)轉(zhuǎn)發(fā)給其他 .NET 程序集。
外觀程序集在運行時并不是嚴格必要的,但是,由于可以編寫依賴這些程序集的反射代碼,因此低剝離級別會保留這些程序集。
DebugDisplay 屬性示例
在下面的示例中,假設(shè)在代碼中的任何地方都沒有使用?Foo.UnusedProperty
?屬性。通常,UnityLinker 會剝離該屬性,但當您啟用腳本調(diào)試時,它將標記?Foo.UnusedProperty
?并保留該屬性,因為 [DebuggerDisplay] 屬性在?Foo
?中。
[DebuggerDisplay("{UnusedProperty}")]
class Foo
{
? ?public int UnusedProperty { get; set; }
}
中等剝離級別
將?Managed Stripping Level?設(shè)置為?Medium?時,UnityLinker 將根據(jù)一組規(guī)則刪除代碼,確保在__低 (Low)__ 剝離級別和__高 (High)__ 剝離級別之間取得平衡。中等 (Medium)?剝離級別不像__低__剝離級別那么謹慎,也不如__高__剝離級別那么極端。因此,因刪除代碼而導(dǎo)致意外副作用的風險大于__低__剝離級別,但小于__高__剝離級別。
中等剝離級別的根標記規(guī)則
程序集類型操作根標記規(guī)則.NET 類和平臺 SDK剝離與低剝離級別相同,但:不適用預(yù)防性保留包含場景中引用的類型的程序集剝離不自動標記程序集中的所有類型和成員標記包含 [RuntimeInitializeOnLoadMethod] 屬性的方法標記包含 [Preserve] 屬性的類型和成員任何 link.xml 中定義的保留在下列程序集中標記派生自 MonoBehaviour 和 ScriptableObject 的所有類型:
預(yù)編譯程序集
軟件包程序集
程序集定義程序集
Unity 腳本程序集其他全部剝離與低剝離級別相同,但:
不自動標記公共類型
不自動標記公共類型的公共成員測試剝離與低剝離級別相同
中等剝離級別的依賴項標記規(guī)則
規(guī)則目標描述Unity 類型與低剝離級別相同特性與低剝離級別相同調(diào)試屬性與低剝離級別相同.NET 外觀類庫程序集與低剝離級別相同
高剝離級別
將?Managed Stripping Level?設(shè)置為?High?時,UnityLinker 將盡可能移除無法訪問的代碼,并生成比__中等 (Medium)__ 剝離級別更小的構(gòu)建。高 (High)?剝離級別優(yōu)先考慮減小代碼大小,而不是確??捎眯裕荒赡苄枰砑?link.xml 文件、Preserve?甚至重寫有問題的代碼部分。
高剝離級別的根標記規(guī)則
程序集類型操作根標記規(guī)則.NET 類和平臺 SDK剝離與中等剝離級別相同包含場景中引用的類型的程序集剝離與中等剝離級別相同其他全部剝離與中等剝離級別相同測試剝離與低剝離級別相同
Link XML 功能標簽排除項
Link.xml?文件支持不常使用的 “features” XML 屬性。例如,mscorlib.dll 中嵌入的 mscorlib.xml 文件使用此屬性,但在適當情況下,您可以在任何 link.xml 文件中使用該屬性。
在__高__級別剝離期間,UnityLinker 根據(jù)當前構(gòu)建的設(shè)置來排除不支持的功能的保留:
remoting — 以 IL2CPP 腳本后端作為目標時排除。
sre — 以 IL2CPP 腳本后端作為目標時排除。
com — 以不支持 COM 的平臺作為目標時排除。
例如,下面的 link.xml 文件在支持 COM 的平臺上保留某種類型的一個方法,并在所有平臺上保留一個方法:
<linker>
? ?<assembly fullname="Foo">
? ? ? ?<type fullname="Type1">
? ? ? ? ? ?<!--Preserve FeatureOne on platforms that support COM-->
? ? ? ? ? ?<method signature="System.Void FeatureOne()" feature="com"/>
? ? ? ? ? ?<!--Preserve FeatureTwo on all platforms-->
? ? ? ? ? ?<method signature="System.Void FeatureTwo()"/>
? ? ? ?</type>
? ?</assembly>
</linker>
高剝離級別的依賴項標記規(guī)則
規(guī)則目標描述Unity 類型與低剝離級別相同特性在所有標記的程序集、類型和成員上,如果屬性類型也已經(jīng)被標記,那么 UnityLinker 將標記屬性。
注意,UnityLinker 總是保留某些屬性,因為運行時需要這些屬性。
UnityLinker 從所有程序集、類型和成員中刪除與安全性相關(guān)的屬性(如 System.Security.Permissions.SecurityPermissionAttribute)。調(diào)試屬性UnityLinker 會始終刪除調(diào)試屬性,如 DebuggerDisplayAttribute 和 DebuggerTypeProxyAttribute。.NET 外觀類庫程序集與保留所有 .NET 外觀程序集的中低剝離級別不同,高剝離級別將刪除所有外觀,因為在運行時不需要它們。
在剝離后,以外觀程序集存在為前提的反射代碼將不起作用。
方法體的編輯
設(shè)置__高__剝離級別時,UnityLinker 將編輯方法體,以進一步減小代碼大小。本節(jié)總結(jié)了 UnityLinker 對方法體所做的一些值得注意的編輯。
UnityLinker 目前只編輯 .NET 類庫程序集中的方法體。注意,在方法體編輯之后,程序集的源代碼不再與程序集中經(jīng)過編譯的代碼匹配,因此可能使調(diào)試變得更加困難。
刪除無法訪問的分支
UnityLinker 會刪除用于檢查?System.Environment.OSVersion.Platform
?并且當前目標平臺無法訪問的 If 語句塊。
內(nèi)聯(lián) - 僅限字段訪問的方法
對于獲取或設(shè)置字段的方法調(diào)用,UnityLinker 會替換為直接字段訪問。這種做法通常能夠完全剝離方法,有助于降低大小。
如果目標是 Mono 后端,那么僅當方法的調(diào)用者獲準直接訪問字段(根據(jù)字段的可見性),UnityLinker 才會進行此更改。對于 IL2CPP,可見性規(guī)則不適用,因此 UnityLinker 會在適當情況下均進行此更改。
內(nèi)聯(lián) - 常量返回值方法
UnityLinker 會對只返回常量值的方法調(diào)用進行內(nèi)聯(lián)。
刪除空的非返回調(diào)用
UnityLinker 會刪除內(nèi)容為空以及返回類型為?void
?的方法調(diào)用。
刪除空作用域
UnityLinker 會在 Finally 塊為空時刪除 Try/Finally 模塊。刪除空調(diào)用可能會產(chǎn)生空的 Finally 塊。在方法編輯過程中發(fā)生這種情況時,UnityLinker 將刪除整個 Try/Finally 塊??赡馨l(fā)生這種情況的一個情形是編譯器在 foreach 循環(huán)中生成 Try/Finally 塊來調(diào)用?Dispose()
。