Příprava knihoven .NET na oříznutí
Sada .NET SDK umožňuje zmenšit velikost samostatných aplikací oříznutím. Oříznutí odebere z aplikace nepoužitý kód a jeho závislosti. Ne všechny kódy jsou kompatibilní s oříznutím. .NET poskytuje upozornění analýzy oříznutí k detekci vzorů, které můžou přerušit oříznuté aplikace. Tento článek:
- Popisuje, jak připravit knihovny na oříznutí.
- Poskytuje doporučení pro řešení běžných upozornění oříznutí.
Požadavky
Sada .NET 6 SDK nebo novější
Pokud chcete získat nejaktuálnější upozornění a pokrytí analyzátoru oříznutí:
- Nainstalujte a použijte sadu .NET 8 SDK nebo novější.
- Cíl
net8.0
nebo novější.
Sada .NET 7 SDK nebo novější
Pokud chcete získat nejaktuálnější upozornění a pokrytí analyzátoru oříznutí:
- Nainstalujte a použijte sadu .NET 8 SDK nebo novější.
- Cíl
net8.0
nebo novější.
Sada .NET 8 SDK nebo novější
Povolení upozornění oříznutí knihovny
Upozornění oříznutí v knihovně najdete pomocí některé z následujících metod:
- Povolení oříznutí specifického
IsTrimmable
pro projekt pomocí vlastnosti - Vytvoření testovací aplikace pro oříznutí, která používá knihovnu a povolení oříznutí pro testovací aplikaci Není nutné odkazovat na všechna rozhraní API v knihovně.
Doporučujeme používat oba přístupy. Oříznutí specifické pro projekt je pohodlné a zobrazuje upozornění oříznutí pro jeden projekt, ale spoléhá na odkazy, které jsou označené jako kompatibilní s oříznutím, aby se zobrazila všechna upozornění. Oříznutí testovací aplikace je více funkční, ale zobrazí se všechna upozornění.
Povolení oříznutí specifického pro projekt
Nastavte <IsTrimmable>true</IsTrimmable>
v souboru projektu.
<PropertyGroup>
<IsTrimmable>true</IsTrimmable>
</PropertyGroup>
Nastavením vlastnosti IsTrimmable
MSBuild označí true
sestavení jako "trimmable" a povolí upozornění oříznutí. "Trimmable" znamená projekt:
- Považuje se za kompatibilní s oříznutím.
- Při vytváření by se nemělo generovat upozornění související s oříznutím. Při použití v oříznuté aplikaci má sestavení jeho nepoužívané členy oříznuté v konečném výstupu.
Vlastnost IsTrimmable
je výchozí true
při konfiguraci projektu jako AOT kompatibilní s <IsAotCompatible>true</IsAotCompatible>
. Další informace najdete v tématu Analyzátory kompatibility AOT.
Chcete-li generovat upozornění oříznutí bez označení projektu jako kompatibilní s oříznutím, použijte <EnableTrimAnalyzer>true</EnableTrimAnalyzer>
místo <IsTrimmable>true</IsTrimmable>
.
Zobrazení všech upozornění pomocí testovací aplikace
Aby se zobrazila všechna upozornění analýzy pro knihovnu, musí se při zatřižování analyzovat implementace knihovny a všech závislostí, které knihovna používá.
Při sestavování a publikování knihovny:
- Implementace závislostí nejsou k dispozici.
- Dostupná referenční sestavení nemají dostatek informací pro oříznutí, aby bylo možné určit, jestli jsou kompatibilní s oříznutím.
Kvůli omezením závislostí se musí vytvořit samostatná testovací aplikace, která používá knihovnu a její závislosti. Testovací aplikace obsahuje všechny informace, které zastřihovač vyžaduje k vydání upozornění na nekompatibilitu oříznutí v:
- Kód knihovny.
- Kód, na který knihovna odkazuje ze závislostí.
Poznámka:
Pokud má knihovna jiné chování v závislosti na cílovém rozhraní, vytvořte testovací aplikaci pro oříznutí pro každou z cílových architektur, která podporuje oříznutí. Pokud například knihovna používá podmíněnou kompilaci , například #if NET7_0
ke změně chování.
Vytvoření testovací aplikace pro oříznutí:
- Vytvořte samostatný projekt konzolové aplikace.
- Přidejte odkaz na knihovnu.
- Upravte projekt podobný následujícímu projektu pomocí následujícího seznamu:
Pokud knihovna cílí na TFM, který není možné oříznout, nebo net472
netstandard2.0
neexistuje žádný přínos pro vytvoření testovací aplikace pro oříznutí. Oříznutí se podporuje jenom pro .NET 6 a novější.
- Nastavte
<TrimmerDefaultAction>
na hodnotulink
. - Přidat
<PublishTrimmed>true</PublishTrimmed>
. - Přidejte odkaz na projekt knihovny pomocí
<ProjectReference Include="/Path/To/YourLibrary.csproj" />
. - Zadejte knihovnu jako kořenové sestavení se zatřižením pomocí
<TrimmerRootAssembly Include="YourLibraryName" />
.TrimmerRootAssembly
zajišťuje, aby byla analyzována každá část knihovny. Říká zatřižovači, že toto sestavení je "root". Sestavení "root" znamená, že zatřižovač analyzuje každé volání v knihovně a prochází všechny cesty kódu, které pocházejí z daného sestavení.
- Přidat
<PublishTrimmed>true</PublishTrimmed>
. - Přidejte odkaz na projekt knihovny pomocí
<ProjectReference Include="/Path/To/YourLibrary.csproj" />
. - Zadejte knihovnu jako kořenové sestavení se zatřižením pomocí
<TrimmerRootAssembly Include="YourLibraryName" />
.TrimmerRootAssembly
zajišťuje, aby byla analyzována každá část knihovny. Říká zatřižovači, že toto sestavení je "root". Sestavení "root" znamená, že zatřižovač analyzuje každé volání v knihovně a prochází všechny cesty kódu, které pocházejí z daného sestavení.
- Přidat
<PublishTrimmed>true</PublishTrimmed>
. - Přidejte odkaz na projekt knihovny pomocí
<ProjectReference Include="/Path/To/YourLibrary.csproj" />
. - Zadejte knihovnu jako kořenové sestavení se zatřižením pomocí
<TrimmerRootAssembly Include="YourLibraryName" />
.TrimmerRootAssembly
zajišťuje, aby byla analyzována každá část knihovny. Říká zatřižovači, že toto sestavení je "root". Sestavení "root" znamená, že zatřižovač analyzuje každé volání v knihovně a prochází všechny cesty kódu, které pocházejí z daného sestavení.
Soubor .csproj
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<PublishTrimmed>true</PublishTrimmed>
<!-- Prevent warnings from unused code in dependencies -->
<TrimmerDefaultAction>link</TrimmerDefaultAction>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="path/to/MyLibrary.csproj" />
<!-- Analyze the whole library, even if attributed with "IsTrimmable" -->
<TrimmerRootAssembly Include="MyLibrary" />
</ItemGroup>
</Project>
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<PublishTrimmed>true</PublishTrimmed>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\MyLibrary\MyLibrary.csproj" />
<TrimmerRootAssembly Include="MyLibrary" />
</ItemGroup>
</Project>
Poznámka: V předchozím souboru projektu při použití .NET 7 nahraďte <TargetFramework>net8.0</TargetFramework>
.<TargetFramework>net7.0</TargetFramework>
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<PublishTrimmed>true</PublishTrimmed>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\MyLibrary\MyLibrary.csproj" />
<TrimmerRootAssembly Include="MyLibrary" />
</ItemGroup>
</Project>
Po aktualizaci souboru projektu spusťte dotnet publish
identifikátor cílovou modul runtime (RID).
dotnet publish -c Release -r <RID>
Postupujte podle předchozího vzoru pro více knihoven. Pokud chcete zobrazit upozornění analýzy oříznutí pro více než jednu knihovnu najednou, přidejte je všechny do stejného projektu jako ProjectReference
položky a TrimmerRootAssembly
položky. Přidání všech knihoven do stejného projektu a ProjectReference
TrimmerRootAssembly
položek upozorní na závislosti, pokud některá z kořenových knihoven používá v závislosti rozhraní API pro oříznutí. Pokud chcete zobrazit upozornění, která je potřeba udělat jenom s konkrétní knihovnou, odkazujte pouze na tuto knihovnu.
Poznámka: Výsledky analýzy závisí na podrobnostech implementace závislostí. Aktualizace na novou verzi závislosti může zavádět upozornění analýzy:
- Pokud nová verze přidala nepochopené vzory reflexe.
- I když nedošlo k žádným změnám rozhraní API.
- Představujeme upozornění analýzy oříznutí je zásadní změna při použití knihovny s
PublishTrimmed
.
Řešení upozornění oříznutí
Předchozí kroky generují upozornění na kód, který může způsobovat problémy při použití v oříznuté aplikaci. Následující příklady ukazují nejběžnější upozornění s doporučeními pro jejich opravu.
RequiresUnreferencedCode
Vezměte v úvahu následující kód, který používá [RequiresUnreferencedCode]
k označení, že zadaná metoda vyžaduje dynamický přístup k kódu, který není odkazován staticky, například prostřednictvím System.Reflection.
public class MyLibrary
{
public static void MyMethod()
{
// warning IL2026 :
// MyLibrary.MyMethod: Using 'MyLibrary.DynamicBehavior'
// which has [RequiresUnreferencedCode] can break functionality
// when trimming app code.
DynamicBehavior();
}
[RequiresUnreferencedCode(
"DynamicBehavior is incompatible with trimming.")]
static void DynamicBehavior()
{
}
}
Předchozí zvýrazněný kód označuje knihovnu volání metody, která byla explicitně označena jako nekompatibilní s oříznutím. Chcete-li se zbavit upozornění, zvažte, zda MyMethod
je třeba zavolat DynamicBehavior
. Pokud ano, označte volajícího MyMethod
poznámkami [RequiresUnreferencedCode]
, se kterým se upozornění rozšíří, aby volajícím MyMethod
místo toho zobrazilo upozornění:
public class MyLibrary
{
[RequiresUnreferencedCode("Calls DynamicBehavior.")]
public static void MyMethod()
{
DynamicBehavior();
}
[RequiresUnreferencedCode(
"DynamicBehavior is incompatible with trimming.")]
static void DynamicBehavior()
{
}
}
Po rozšíření atributu až do veřejného rozhraní API aplikace volají knihovnu:
- Zobrazí se upozornění jenom pro veřejné metody, které se nedají oříznout.
- Nezobrazují se upozornění jako
IL2104: Assembly 'MyLibrary' produced trim warnings
.
DynamickyAccessedMembers
public class MyLibrary3
{
static void UseMethods(Type type)
{
// warning IL2070: MyLibrary.UseMethods(Type): 'this' argument does not satisfy
// 'DynamicallyAccessedMemberTypes.PublicMethods' in call to
// 'System.Type.GetMethods()'.
// The parameter 't' of method 'MyLibrary.UseMethods(Type)' doesn't have
// matching annotations.
foreach (var method in type.GetMethods())
{
// ...
}
}
}
V předchozím kódu volá metodu reflexe, UseMethods
která má [DynamicallyAccessedMembers]
požadavek. Požadavek uvádí, že veřejné metody typu jsou k dispozici. Splnění požadavku přidáním stejného požadavku na parametr .UseMethods
static void UseMethods(
// State the requirement in the UseMethods parameter.
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] Type type)
{
// ...
}
Když teď všechna volání UseMethods
vygenerují upozornění, předávají hodnoty, které nevyhovují PublicMethods požadavkům. [RequiresUnreferencedCode]
Podobně jako v případě, že po rozšíření těchto upozornění do veřejných rozhraní API budete hotovi.
V následujícím příkladu neznámý typ proudí do anotovaného parametru metody. Neznámé Type
je z pole:
static Type type;
static void UseMethodsHelper()
{
// warning IL2077: MyLibrary.UseMethodsHelper(Type): 'type' argument does not satisfy
// 'DynamicallyAccessedMemberTypes.PublicMethods' in call to
// 'MyLibrary.UseMethods(Type)'.
// The field 'System.Type MyLibrary::type' does not have matching annotations.
UseMethods(type);
}
Podobně zde problém spočívá v tom, že pole type
se předává do parametru s těmito požadavky. Opravili jsme ho přidáním [DynamicallyAccessedMembers]
do pole. [DynamicallyAccessedMembers]
varuje o kódu, který přiřadí nekompatibilní hodnoty k poli. Někdy tento proces pokračuje, dokud nebude veřejné rozhraní API opatřeno poznámkami, a jindy skončí, když konkrétní typ přejde do umístění s těmito požadavky. Příklad:
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)]
static Type type;
static void UseMethodsHelper()
{
MyLibrary.type = typeof(System.Tuple);
}
V tomto případě analýza oříznutí uchovává veřejné metody Tuplea vytváří další upozornění.
Doporučení
- Pokud je to možné, vyhněte se reflexi. Při použití reflexe minimalizujte rozsah reflexe tak, aby byl dostupný jenom z malé části knihovny.
- Pokud je to možné, přiřazte kód staticky
DynamicallyAccessedMembers
a vyjádřete požadavky na oříznutí. - Zvažte přeuspořádání kódu, aby postupoval podle analyzovatelného vzoru, který může být opatřen poznámkami
DynamicallyAccessedMembers
- Pokud kód není kompatibilní s oříznutím, označte ho poznámkami
RequiresUnreferencedCode
a rozšíříte tuto poznámku volajícím, dokud nebudou příslušná veřejná rozhraní API opatřena poznámkami. - Nepoužívejte kód, který používá reflexi způsobem, kterému statická analýza nerozumí. Například reflexe statických konstruktorů by se měla vyhnout. Použití staticky neanalyzovatelné reflexe ve statických konstruktorech vede k šíření upozornění na všechny členy třídy.
- Vyhněte se přidávání poznámek k virtuálním metodám nebo metodám rozhraní. Přidávání poznámek k virtuálním metodám nebo metod rozhraní vyžaduje, aby všechny přepsání měly odpovídající poznámky.
- Pokud je rozhraní API většinou nekompatibilní, možná bude potřeba zvážit alternativní přístupy k kódování rozhraní API. Běžným příkladem jsou serializátory založené na reflexi. V těchto případech zvažte přijetí dalších technologií, jako jsou generátory zdrojů, k vytvoření kódu, který je snadněji staticky analyzován. Podívejte se například, jak používat generování zdrojového kódu v souboru System.Text.Json.
Řešení upozornění pro ne analyzovatelné vzory
Upozornění je lepší vyřešit vyjádřením záměru kódu a [RequiresUnreferencedCode]
DynamicallyAccessedMembers
v případě, že je to možné. V některých případech vás ale může zajímat povolení oříznutí knihovny, která používá vzory, které nelze vyjádřit pomocí těchto atributů nebo bez refaktoringu existujícího kódu. Tato část popisuje některé pokročilé způsoby řešení upozornění analýzy oříznutí.
Upozorňující
Tyto techniky můžou změnit chování nebo kód nebo vést k nesprávným výjimkám doby běhu.
Nepodmíněné zprávy
Vezměte v úvahu kód, který:
- Záměr nelze vyjádřit pomocí poznámek.
- Vygeneruje upozornění, ale nepředstavuje skutečný problém za běhu.
Upozornění lze potlačit UnconditionalSuppressMessageAttribute. To se podobá SuppressMessageAttribute
, ale to je trvalé v IL a respektovat během analýzy oříznutí.
Upozorňující
Při potlačení upozornění zodpovídáte za záruku kompatibility oříznutí kódu na základě invariantů, o kterých víte, že jsou pravdivé kontrolou a testováním. U těchto poznámek buďte opatrní, protože pokud jsou nesprávné nebo pokud jsou invarianty změny kódu, můžou nakonec skrýt nesprávný kód.
Příklad:
class TypeCollection
{
Type[] types;
// Ensure that only types with preserved constructors are stored in the array
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)]
public Type this[int i]
{
// warning IL2063: TypeCollection.Item.get: Value returned from method
// 'TypeCollection.Item.get' can't be statically determined and may not meet
// 'DynamicallyAccessedMembersAttribute' requirements.
get => types[i];
set => types[i] = value;
}
}
class TypeCreator
{
TypeCollection types;
public void CreateType(int i)
{
types[i] = typeof(TypeWithConstructor);
Activator.CreateInstance(types[i]); // No warning!
}
}
class TypeWithConstructor
{
}
V předchozím kódu byla vlastnost indexeru anotována tak, aby vrácená Type
hodnota splňovala požadavky .CreateInstance
Tím se zajistí, že TypeWithConstructor
je konstruktor zachován a že volání CreateInstance
se nevaruje. Poznámka k setter indexeru zajišťuje, že všechny typy uložené v konstruktoru Type[]
. Analýza ale tuto možnost nevidí a vygeneruje upozornění pro getter, protože neví, že vrácený typ má svůj konstruktor zachován.
Pokud jste si jistí, že jsou splněné požadavky, můžete toto upozornění mlčet přidáním [UnconditionalSuppressMessage]
do getteru:
class TypeCollection
{
Type[] types;
// Ensure that only types with preserved constructors are stored in the array
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)]
public Type this[int i]
{
[UnconditionalSuppressMessage("ReflectionAnalysis", "IL2063",
Justification = "The list only contains types stored through the annotated setter.")]
get => types[i];
set => types[i] = value;
}
}
class TypeCreator
{
TypeCollection types;
public void CreateType(int i)
{
types[i] = typeof(TypeWithConstructor);
Activator.CreateInstance(types[i]); // No warning!
}
}
class TypeWithConstructor
{
}
Je důležité podtrhnout, že je platné potlačit upozornění pouze v případě, že existují poznámky nebo kód, které zajistí, aby odrazové členy byly viditelné cíle reflexe. Nestačí, aby byl člen cílem přístupu k volání, poli nebo vlastnosti. Může se zdát, že se jedná o případ, ale takový kód je vázán k přerušení nakonec, protože se přidají další optimalizace oříznutí. Vlastnosti, pole a metody, které nejsou viditelné cíle reflexe, mohou být vloženy, mají jejich názvy odebrány, přesunout se do různých typů nebo jinak optimalizovat způsobem, který se na nich odráží. Když potlačíte upozornění, je možné odrážet pouze cíle, které byly viditelné cíle odrazu, na jiném místě analyzátoru oříznutí.
// Invalid justification and suppression: property being non-reflectively
// used by the app doesn't guarantee that the property will be available
// for reflection. Properties that are not visible targets of reflection
// are already optimized away with Native AOT trimming and may be
// optimized away for non-native deployment in the future as well.
[UnconditionalSuppressMessage("ReflectionAnalysis", "IL2063",
Justification = "*INVALID* Only need to serialize properties that are used by"
+ "the app. *INVALID*")]
public string Serialize(object o)
{
StringBuilder sb = new StringBuilder();
foreach (var property in o.GetType().GetProperties())
{
AppendProperty(sb, property, o);
}
return sb.ToString();
}
DynamicDependency
Atribut [DynamicDependency]
lze použít k označení, že člen má dynamickou závislost na jiných členech. Výsledkem je zachování odkazovaných členů vždy, když je člen s atributem zachován, ale neumlčuje upozornění sama. Na rozdíl od ostatních atributů, které informují analýzu oříznutí o chování reflexe kódu, [DynamicDependency]
udržuje pouze ostatní členy. Můžete ho použít společně s [UnconditionalSuppressMessage]
opravou některých upozornění analýzy.
Upozorňující
Atribut používejte [DynamicDependency]
pouze jako poslední možnost, pokud ostatní přístupy nejsou přijatelné. Je vhodnější vyjádřit chování reflexe pomocí [RequiresUnreferencedCode]
nebo [DynamicallyAccessedMembers]
.
[DynamicDependency("Helper", "MyType", "MyAssembly")]
static void RunHelper()
{
var helper = Assembly.Load("MyAssembly").GetType("MyType").GetMethod("Helper");
helper.Invoke(null, null);
}
Bez DynamicDependency
ořezávání se může odebrat nebo úplně odebrat MyAssembly
Helper
MyAssembly
, pokud není odkazováno jinde, což vygeneruje upozornění, které značí možné selhání v době běhu. Atribut zajišťuje, že Helper
se zachová.
Atribut určuje členy, které mají být zachovány prostřednictvím nebo string
prostřednictvím DynamicallyAccessedMemberTypes
. Typ a sestavení jsou buď implicitní v kontextu atributu, nebo explicitně zadané v atributu (podle Type
, nebo podle string
s pro typ a název sestavení).
Typ a členské řetězce používají variantu formátu řetězce ID komentáře dokumentace jazyka C# bez předpony člena. Řetězec člena by neměl obsahovat název deklarujícího typu a může vynechat parametry pro zachování všech členů zadaného názvu. Některé příklady formátu jsou uvedeny v následujícím kódu:
[DynamicDependency("MyMethod()")]
[DynamicDependency("MyMethod(System,Boolean,System.String)")]
[DynamicDependency("MethodOnDifferentType()", typeof(ContainingType))]
[DynamicDependency("MemberName")]
[DynamicDependency("MemberOnUnreferencedAssembly", "ContainingType"
, "UnreferencedAssembly")]
[DynamicDependency("MemberName", "Namespace.ContainingType.NestedType", "Assembly")]
// generics
[DynamicDependency("GenericMethodName``1")]
[DynamicDependency("GenericMethod``2(``0,``1)")]
[DynamicDependency(
"MethodWithGenericParameterTypes(System.Collections.Generic.List{System.String})")]
[DynamicDependency("MethodOnGenericType(`0)", "GenericType`1", "UnreferencedAssembly")]
[DynamicDependency("MethodOnGenericType(`0)", typeof(GenericType<>))]
Atribut [DynamicDependency]
je navržen tak, aby byl použit v případech, kdy metoda obsahuje vzory reflexe, které nelze analyzovat ani s pomocí DynamicallyAccessedMembersAttribute
.