Dela via


Förbereda .NET-bibliotek för trimning

.NET SDK gör det möjligt att minska storleken på fristående appar genom att trimma. Trimning tar bort oanvänd kod från appen och dess beroenden. All kod är inte kompatibel med trimning. .NET tillhandahåller trimanalysvarningar för att identifiera mönster som kan bryta trimmade appar. Den här artikeln:

Förutsättningar

.NET 8 SDK eller senare.

Aktivera varningar för bibliotekstrimning

Trimvarningar i ett bibliotek finns med någon av följande metoder:

  • Aktivera projektspecifik trimning med hjälp av IsTrimmable egenskapen .
  • Skapa en trimningstestapp som använder biblioteket och aktivera trimning för testappen. Det är inte nödvändigt att referera till alla API:er i biblioteket.

Vi rekommenderar att du använder båda metoderna. Projektspecifik trimning är praktiskt och visar trimvarningar för ett projekt, men förlitar sig på att referenserna markeras som trimkompatibla för att se alla varningar. Att trimma en testapp är mer arbete, men visar alla varningar.

Aktivera projektspecifik trimning

Ange <IsTrimmable>true</IsTrimmable> i projektfilen.

<PropertyGroup>
    <IsTrimmable>true</IsTrimmable>
</PropertyGroup>

Om du anger egenskapen IsTrimmable MSBuild så markeras true sammansättningen som "trimmable" och trimvarningar aktiveras. "Trimmable" innebär projektet:

  • Anses vara kompatibelt med trimning.
  • Bör inte generera trimrelaterade varningar när du skapar. När den används i en trimmad app har sammansättningen sina oanvända medlemmar trimmade i de slutliga utdata.

Egenskapen IsTrimmable används som standard true när du konfigurerar ett projekt som AOT-kompatibelt med <IsAotCompatible>true</IsAotCompatible>. Mer information finns i AOT-kompatibilitetsanalyser.

Om du vill generera trimvarningar utan att markera projektet som trimkompatibelt använder du <EnableTrimAnalyzer>true</EnableTrimAnalyzer> i stället <IsTrimmable>true</IsTrimmable>för .

Visa alla varningar med testappen

Om du vill visa alla analysvarningar för ett bibliotek måste trimmern analysera implementeringen av biblioteket och alla beroenden som biblioteket använder.

När du skapar och publicerar ett bibliotek:

  • Implementeringarna av beroendena är inte tillgängliga.
  • De tillgängliga referenssammansättningarna har inte tillräckligt med information för trimmern för att avgöra om de är kompatibla med trimning.

På grund av beroendebegränsningarna måste en fristående testapp som använder biblioteket och dess beroenden skapas. Testappen innehåller all information som trimmern behöver för att utfärda varning om trimningskompatibiliteter i:

  • Bibliotekskoden.
  • Den kod som biblioteket refererar till från dess beroenden.

Kommentar

Om biblioteket har olika beteende beroende på målramverket skapar du en trimningstestapp för vart och ett av målramverken som stöder trimning. Om biblioteket till exempel använder villkorsstyrd kompilering , till exempel #if NET7_0 för att ändra beteende.

Så här skapar du trimningstestappen:

  • Skapa ett separat konsolprogramprojekt.
  • Lägg till en referens till biblioteket.
  • Ändra projektet som liknar det projekt som visas nedan med hjälp av följande lista:

Om biblioteket riktar in sig på en TFM som inte är trimmad, till exempel net472 eller netstandard2.0, finns det ingen fördel med att skapa en trimningstestapp. Trimning stöds endast för .NET 6 och senare.

  • Lägg till <PublishTrimmed>true</PublishTrimmed>.
  • Lägg till en referens till biblioteksprojektet med <ProjectReference Include="/Path/To/YourLibrary.csproj" />.
  • Ange biblioteket som en trimmerrotsammansättning med <TrimmerRootAssembly Include="YourLibraryName" />.
    • TrimmerRootAssembly säkerställer att varje del av biblioteket analyseras. Den talar om för trimmern att den här sammansättningen är en "rot". En "rot"-sammansättning innebär att trimmern analyserar varje anrop i biblioteket och passerar alla kodsökvägar som kommer från den sammansättningen.

.csproj-fil

<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>

När projektfilen har uppdaterats kör du dotnet publish med målkörningsidentifieraren (RID).

dotnet publish -c Release -r <RID>

Följ föregående mönster för flera bibliotek. Om du vill se trimningsanalysvarningar för fler än ett bibliotek i taget lägger du till dem alla i samma projekt som ProjectReference och TrimmerRootAssembly objekt. När du lägger till alla bibliotek i samma projekt med ProjectReference och TrimmerRootAssembly objekt varnas om beroenden om något av rotbiblioteken använder ett trim-unfriendly-API i ett beroende. Om du vill se varningar som bara har att göra med ett visst bibliotek refererar du endast till det biblioteket.

Kommentar

Analysresultaten beror på implementeringsinformationen för beroendena. Uppdatering till en ny version av ett beroende kan medföra analysvarningar:

  • Om den nya versionen har lagt till icke-förstådda reflektionsmönster.
  • Även om det inte fanns några API-ändringar.
  • Introduktion till trimningsanalysvarningar är en icke-bakåtkompatibel ändring när biblioteket används med PublishTrimmed.

Lösa trimvarningar

Föregående steg ger varningar om kod som kan orsaka problem när de används i en trimmad app. I följande exempel visas de vanligaste varningarna med rekommendationer för att åtgärda dem.

RequiresUnreferencedCode

Tänk på följande kod som används [RequiresUnreferencedCode] för att indikera att den angivna metoden kräver dynamisk åtkomst till kod som inte refereras statiskt, till exempel via 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()
    {
    }
}

Föregående markerade kod anger att biblioteket anropar en metod som uttryckligen har kommenterats som inkompatibel med trimning. Om du vill bli av med varningen bör du överväga om MyMethod du behöver anropa DynamicBehavior. I så fall kommenterar du anroparen MyMethod med [RequiresUnreferencedCode] vilken varningen sprids så att anropare får MyMethod en varning i stället:

public class MyLibrary
{
    [RequiresUnreferencedCode("Calls DynamicBehavior.")]
    public static void MyMethod()
    {
        DynamicBehavior();
    }

    [RequiresUnreferencedCode(
        "DynamicBehavior is incompatible with trimming.")]
    static void DynamicBehavior()
    {
    }
}

När du har spridit attributet hela vägen till offentligt API anropar appar biblioteket:

  • Hämta endast varningar för offentliga metoder som inte är trimmade.
  • Få inte varningar som IL2104: Assembly 'MyLibrary' produced trim warnings.

DynamicallyAccessedMembers

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())
        {
            // ...
        }
    }
}

I föregående kod UseMethods anropar du en reflektionsmetod som har ett [DynamicallyAccessedMembers] krav. Kravet anger att typens offentliga metoder är tillgängliga. Uppfylla kravet genom att lägga till samma krav i parametern UseMethodsför .

static void UseMethods(
   // State the requirement in the UseMethods parameter.
   [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] Type type)
{
    // ...
}

Nu alla anrop för att UseMethods skapa varningar om de skickar in värden som inte uppfyller PublicMethods kravet. [RequiresUnreferencedCode]Precis som när du har spridit sådana varningar till offentliga API:er är du klar.

I följande exempel flödar en okänd typ in i den kommenterade metodparametern. Det okända Type kommer från ett fält:

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);
}

På samma sätt är problemet att fältet type skickas till en parameter med dessa krav. Det åtgärdas genom att lägga [DynamicallyAccessedMembers] till i fältet. [DynamicallyAccessedMembers] varnar för kod som tilldelar inkompatibla värden till fältet. Ibland fortsätter den här processen tills ett offentligt API kommenteras och andra gånger slutar det när en konkret typ flödar till en plats med dessa krav. Till exempel:

[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)]
static Type type;

static void UseMethodsHelper()
{
    MyLibrary.type = typeof(System.Tuple);
}

I det här fallet behåller trimanalysen offentliga metoder för Tupleoch genererar ytterligare varningar.

Rekommendationer

  • Undvik reflektion när det är möjligt. När du använder reflektion minimerar du reflektionsomfånget så att det endast kan nås från en liten del av biblioteket.
  • Kommentera kod med DynamicallyAccessedMembers för att statiskt uttrycka trimningskraven när det är möjligt.
  • Överväg att ordna om koden så att den följer ett analysbart mönster som kan kommenteras med DynamicallyAccessedMembers
  • När koden inte är kompatibel med trimning kommenterar du den med RequiresUnreferencedCode och sprider den här kommentaren till anropare tills relevanta offentliga API:er kommenteras.
  • Undvik att använda kod som använder reflektion på ett sätt som inte förstås av den statiska analysen. Till exempel bör reflektion i statiska konstruktorer undvikas. Om statiskt oföränderlig reflektion används i statiska konstruktorer sprids varningen till alla medlemmar i klassen.
  • Undvik att kommentera virtuella metoder eller gränssnittsmetoder. För att kommentera virtuella metoder eller gränssnittsmetoder krävs att alla åsidosättningar har matchande anteckningar.
  • Om ett API mestadels är trim-inkompatibelt kan alternativa kodningsmetoder för API:et behöva övervägas. Ett vanligt exempel är reflektionsbaserade serialiserare. I dessa fall bör du överväga att använda annan teknik som källgeneratorer för att producera kod som är enklare att analysera statiskt. Till exempel, se Hur man använder källgenerering i System.Text.Json.

Lösa varningar för icke-analysbara mönster

Det är bättre att lösa varningar genom att uttrycka avsikten med din kod med hjälp av [RequiresUnreferencedCode] och DynamicallyAccessedMembers när det är möjligt. I vissa fall kan du dock vara intresserad av att aktivera trimning av ett bibliotek som använder mönster som inte kan uttryckas med dessa attribut eller utan att omstrukturera befintlig kod. I det här avsnittet beskrivs några avancerade sätt att lösa varningar om trimningsanalys.

Varning

Dessa tekniker kan ändra beteendet eller koden eller resultera i körningsfel om de används felaktigt.

UnconditionalSuppressMessage

Överväg kod som:

  • Avsikten kan inte uttryckas med anteckningarna.
  • Genererar en varning men representerar inte ett verkligt problem vid körning.

Varningarna kan ignoreras av UnconditionalSuppressMessageAttribute. Detta liknar SuppressMessageAttribute, men det bevaras i IL och respekteras under trimningsanalysen.

Varning

När du undertrycker varningar ansvarar du för att garantera kodens trimkompatibilitet baserat på invarianter som du vet är sanna genom inspektion och testning. Var försiktig med de här anteckningarna, för om de är felaktiga eller om kodändringen är invariant kan det sluta med att de döljer felaktig kod.

Till exempel:

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
{
}

I föregående kod har indexeraregenskapen kommenterats så att den returnerade Type uppfyller kraven CreateInstanceför . Detta säkerställer att TypeWithConstructor konstruktorn behålls och att anropet till CreateInstance inte varnar. Indexerarens anteckning säkerställer att alla typer som lagras i Type[] har en konstruktor. Analysen kan dock inte se detta och ger en varning för gettern, eftersom den inte vet att den returnerade typen har sin konstruktor bevarad.

Om du är säker på att kraven är uppfyllda kan du tysta den här varningen genom att lägga [UnconditionalSuppressMessage] till i gettern:

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
{
}

Det är viktigt att understryka att det bara är giltigt att ignorera en varning om det finns anteckningar eller kod som säkerställer att de reflekterade medlemmarna är synliga mål för reflektion. Det räcker inte att medlemmen var ett mål för ett anrop, ett fält eller en egenskapsåtkomst. Det kan tyckas vara fallet ibland, men sådan kod kommer att brytas så småningom när fler trimningsoptimeringar läggs till. Egenskaper, fält och metoder som inte är synliga mål för reflektion kan infogas, deras namn tas bort, flyttas till olika typer eller på annat sätt optimeras på sätt som bryter reflekterande på dem. När du undertrycker en varning är det bara tillåtet att reflektera över mål som var synliga reflektionsmål för trimningsanalysatorn någon annanstans.

// 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

Attributet [DynamicDependency] kan användas för att indikera att en medlem har ett dynamiskt beroende av andra medlemmar. Detta resulterar i att de refererade medlemmarna behålls när medlemmen med attributet behålls, men inte tystar varningar på egen hand. Till skillnad från de andra attributen, som informerar trimanalysen om kodens reflektionsbeteende, [DynamicDependency] behåller bara andra medlemmar. Detta kan användas tillsammans med [UnconditionalSuppressMessage] för att åtgärda vissa analysvarningar.

Varning

Använd [DynamicDependency] endast attributet som en sista utväg när de andra metoderna inte är livskraftiga. Det är bättre att uttrycka reflektionsbeteendet med hjälp av [RequiresUnreferencedCode] eller [DynamicallyAccessedMembers].

[DynamicDependency("Helper", "MyType", "MyAssembly")]
static void RunHelper()
{
    var helper = Assembly.Load("MyAssembly").GetType("MyType").GetMethod("Helper");
    helper.Invoke(null, null);
}

Utan DynamicDependencykan trimning ta bort Helper från MyAssembly eller ta bort MyAssembly helt om det inte refereras någon annanstans, vilket ger en varning som anger ett eventuellt fel vid körning. Attributet säkerställer att Helper behålls.

Attributet anger vilka medlemmar som ska behållas via en string eller via DynamicallyAccessedMemberTypes. Typen och sammansättningen är antingen implicita i attributkontexten eller anges uttryckligen i attributet (efter Type, eller av strings för typ- och sammansättningsnamnet).

Typ- och medlemssträngarna använder en variant av C#-dokumentationens kommentars-ID-strängformat, utan medlemsprefixet. Medlemssträngen ska inte innehålla namnet på deklareringstypen och kan utelämna parametrar för att behålla alla medlemmar i det angivna namnet. Några exempel på formatet visas i följande kod:

[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<>))]

Attributet [DynamicDependency] är utformat för att användas i fall där en metod innehåller reflektionsmönster som inte kan analyseras även med hjälp av DynamicallyAccessedMembersAttribute.