Bagikan melalui


Menyiapkan pustaka .NET untuk pemangkasan

.NET SDK memungkinkan untuk mengurangi ukuran aplikasi mandiri dengan memangkas. Pemangkasan menghapus kode yang tidak digunakan dari aplikasi dan dependensinya. Tidak semua kode kompatibel dengan pemangkasan. .NET menyediakan peringatan analisis pemangkasan untuk mendeteksi pola yang dapat merusak aplikasi yang dipangkas. Artikel ini:

Prasyarat

.NET 6 SDK atau yang lebih baru.

Untuk mendapatkan peringatan pemangkasan terbaru dan cakupan penganalisis:

  • Instal dan gunakan .NET 8 SDK atau yang lebih baru.
  • Target net8.0 atau yang lebih baru.

.NET 7 SDK atau yang lebih baru.

Untuk mendapatkan peringatan pemangkasan terbaru dan cakupan penganalisis:

  • Instal dan gunakan .NET 8 SDK atau yang lebih baru.
  • Target net8.0 atau yang lebih baru.

.NET 8 SDK atau yang lebih baru.

Mengaktifkan peringatan pemangkasan pustaka

Memangkas peringatan dalam pustaka dapat ditemukan dengan salah satu metode berikut:

  • Mengaktifkan pemangkasan khusus proyek menggunakan IsTrimmable properti .
  • Membuat aplikasi pengujian pemangkasan yang menggunakan pustaka dan mengaktifkan pemangkasan untuk aplikasi pengujian. Tidak perlu mereferensikan semua API di pustaka.

Sebaiknya gunakan kedua pendekatan. Pemangkasan khusus proyek nyaman dan menunjukkan peringatan pemangkasan untuk satu proyek, tetapi bergantung pada referensi yang ditandai kompatibel dengan pemangkasan untuk melihat semua peringatan. Pemangkasan aplikasi pengujian lebih berfungsi, tetapi menampilkan semua peringatan.

Mengaktifkan pemangkasan khusus proyek

Atur <IsTrimmable>true</IsTrimmable> dalam file proyek.

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

Mengatur properti IsTrimmable MSBuild untuk true menandai rakitan sebagai "dapat dipangkas" dan memungkinkan peringatan pemangkasan. "Dapat dipangkas" berarti proyek:

  • Dianggap kompatibel dengan pemangkasan.
  • Seharusnya tidak menghasilkan peringatan terkait pemangkasan saat membangun. Saat digunakan dalam aplikasi yang dipangkas, rakitan memiliki anggota yang tidak digunakan dipangkas dalam output akhir.

Properti IsTrimmable default ke true saat mengonfigurasi proyek sebagai kompatibel dengan AOT dengan <IsAotCompatible>true</IsAotCompatible>. Untuk informasi selengkapnya, lihat Penganalisis kompatibilitas AOT.

Untuk menghasilkan peringatan pemangkasan tanpa menandai proyek sebagai kompatibel dengan trim, gunakan <EnableTrimAnalyzer>true</EnableTrimAnalyzer> daripada <IsTrimmable>true</IsTrimmable>.

Menampilkan semua peringatan dengan aplikasi pengujian

Untuk menampilkan semua peringatan analisis untuk pustaka, pemangkas harus menganalisis implementasi pustaka dan semua dependensi yang digunakan pustaka.

Saat membangun dan menerbitkan pustaka:

  • Implementasi dependensi tidak tersedia.
  • Rakitan referensi yang tersedia tidak memiliki informasi yang cukup untuk pemangkas untuk menentukan apakah mereka kompatibel dengan pemangkasan.

Karena keterbatasan dependensi, aplikasi pengujian mandiri yang menggunakan pustaka dan dependensinya harus dibuat. Aplikasi pengujian mencakup semua informasi yang diperlukan pemangkas untuk mengeluarkan peringatan tentang ketidakcocokan pemangkasan dalam:

  • Kode pustaka.
  • Kode yang dirujuk pustaka dari dependensinya.

Catatan

Jika pustaka memiliki perilaku yang berbeda tergantung pada kerangka kerja target, buat aplikasi pengujian pemangkasan untuk setiap kerangka kerja target yang mendukung pemangkasan. Misalnya, jika pustaka menggunakan kompilasi bersyarkat seperti #if NET7_0 mengubah perilaku.

Untuk membuat aplikasi pengujian pemangkasan:

  • Buat proyek aplikasi konsol terpisah.
  • Tambahkan referensi ke pustaka.
  • Ubah proyek yang mirip dengan proyek yang ditunjukkan di bawah ini menggunakan daftar berikut:

Jika pustaka menargetkan TFM yang tidak dapat dipangkas, misalnya net472 atau netstandard2.0, tidak ada manfaat untuk membuat aplikasi pengujian pemangkasan. Pemangkasan hanya didukung untuk .NET 6 dan yang lebih baru.

  • Atur <TrimmerDefaultAction> ke link.
  • Tambahkan <PublishTrimmed>true</PublishTrimmed>.
  • Tambahkan referensi ke proyek pustaka dengan <ProjectReference Include="/Path/To/YourLibrary.csproj" />.
  • Tentukan pustaka sebagai rakitan akar pemangkas dengan <TrimmerRootAssembly Include="YourLibraryName" />.
    • TrimmerRootAssembly memastikan bahwa setiap bagian pustaka dianalisis. Ini memberi tahu pemangkas bahwa rakitan ini adalah "root". Rakitan "root" berarti pemangkas menganalisis setiap panggilan di pustaka dan melintasi semua jalur kode yang berasal dari rakitan tersebut.
  • Tambahkan <PublishTrimmed>true</PublishTrimmed>.
  • Tambahkan referensi ke proyek pustaka dengan <ProjectReference Include="/Path/To/YourLibrary.csproj" />.
  • Tentukan pustaka sebagai rakitan akar pemangkas dengan <TrimmerRootAssembly Include="YourLibraryName" />.
    • TrimmerRootAssembly memastikan bahwa setiap bagian pustaka dianalisis. Ini memberi tahu pemangkas bahwa rakitan ini adalah "root". Rakitan "root" berarti pemangkas menganalisis setiap panggilan di pustaka dan melintasi semua jalur kode yang berasal dari rakitan tersebut.
  • Tambahkan <PublishTrimmed>true</PublishTrimmed>.
  • Tambahkan referensi ke proyek pustaka dengan <ProjectReference Include="/Path/To/YourLibrary.csproj" />.
  • Tentukan pustaka sebagai rakitan akar pemangkas dengan <TrimmerRootAssembly Include="YourLibraryName" />.
    • TrimmerRootAssembly memastikan bahwa setiap bagian pustaka dianalisis. Ini memberi tahu pemangkas bahwa rakitan ini adalah "root". Rakitan "root" berarti pemangkas menganalisis setiap panggilan di pustaka dan melintasi semua jalur kode yang berasal dari rakitan tersebut.

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

Catatan: Dalam file proyek sebelumnya, saat menggunakan .NET 7, ganti <TargetFramework>net8.0</TargetFramework> dengan <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>

Setelah file proyek diperbarui, jalankan dotnet publish dengan pengidentifikasi runtime target (RID).

dotnet publish -c Release -r <RID>

Ikuti pola sebelumnya untuk beberapa pustaka. Untuk melihat peringatan analisis pemangkasan untuk lebih dari satu pustaka sekaligus, tambahkan semuanya ke proyek yang sama dengan ProjectReference item dan TrimmerRootAssembly . Menambahkan semua pustaka ke proyek yang sama dengan item dan TrimmerRootAssembly memperingatkan tentang dependensi jika salah satu pustaka akar menggunakan API trim-unfriendly dalam dependensi.ProjectReference Untuk melihat peringatan yang hanya ada hubungannya dengan pustaka tertentu, referensikan pustaka tersebut saja.

Catatan: Hasil analisis bergantung pada detail implementasi dependensi. Memperbarui ke versi baru dependensi dapat memperkenalkan peringatan analisis:

  • Jika versi baru menambahkan pola refleksi yang tidak dipahami.
  • Bahkan jika tidak ada perubahan API.
  • Memperkenalkan peringatan analisis pemangkasan adalah perubahan yang melanggar ketika pustaka digunakan dengan PublishTrimmed.

Mengatasi peringatan pemangkasan

Langkah-langkah sebelumnya menghasilkan peringatan tentang kode yang dapat menyebabkan masalah saat digunakan dalam aplikasi yang dipangkas. Contoh berikut menunjukkan peringatan yang paling umum dengan rekomendasi untuk memperbaikinya.

RequiresUnreferencedCode

Pertimbangkan kode berikut yang menggunakan [RequiresUnreferencedCode] untuk menunjukkan bahwa metode yang ditentukan memerlukan akses dinamis ke kode yang tidak dirujuk secara statis, misalnya, melalui 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()
    {
    }
}

Kode yang disorot sebelumnya menunjukkan pustaka memanggil metode yang secara eksplisit telah dianotasikan sebagai tidak kompatibel dengan pemangkasan. Untuk menyingkirkan peringatan, pertimbangkan apakah MyMethod perlu memanggil DynamicBehavior. Jika demikian, anotasi pemanggil MyMethod yang [RequiresUnreferencedCode] menyebarkan peringatan sehingga penelepon MyMethod mendapatkan peringatan sebagai gantinya:

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

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

Setelah Anda menyebarkan atribut ke API publik, aplikasi yang memanggil pustaka:

  • Dapatkan peringatan hanya untuk metode publik yang tidak dapat dipangkas.
  • Jangan mendapatkan peringatan seperti 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())
        {
            // ...
        }
    }
}

Dalam kode sebelumnya, UseMethods memanggil metode pantulan yang memiliki [DynamicallyAccessedMembers] persyaratan. Persyaratan menyatakan bahwa metode publik jenis tersedia. Penuhi persyaratan dengan menambahkan persyaratan yang sama ke parameter UseMethods.

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

Sekarang setiap panggilan untuk UseMethods menghasilkan peringatan jika mereka meneruskan nilai yang tidak memenuhi PublicMethods persyaratan. Mirip [RequiresUnreferencedCode]dengan , setelah Anda menyebarkan peringatan tersebut ke API publik, Anda selesai.

Dalam contoh berikut, Jenis yang tidak diketahui mengalir ke parameter metode yang dianotasi. Yang tidak diketahui Type berasal dari bidang:

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

Demikian pula, di sini masalahnya adalah bahwa bidang type diteruskan ke parameter dengan persyaratan ini. Ini diperbaiki dengan menambahkan [DynamicallyAccessedMembers] ke bidang . [DynamicallyAccessedMembers] memperingatkan tentang kode yang menetapkan nilai yang tidak kompatibel ke bidang . Terkadang proses ini berlanjut sampai API publik diannotasi, dan di lain waktu berakhir ketika jenis beton mengalir ke lokasi dengan persyaratan ini. Misalnya:

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

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

Dalam hal ini, analisis pemangkasan menyimpan metode publik , Tupledan menghasilkan peringatan lebih lanjut.

Rekomendasi

  • Hindari refleksi jika memungkinkan. Saat menggunakan pantulan, minimalkan cakupan pantulan sehingga hanya dapat dijangkau dari sebagian kecil pustaka.
  • Anotasi kode dengan DynamicallyAccessedMembers untuk secara statis mengekspresikan persyaratan pemangkasan jika memungkinkan.
  • Pertimbangkan untuk mengatur ulang kode untuk membuatnya mengikuti pola yang dapat dianotasi yang dapat dianotasi dengan DynamicallyAccessedMembers
  • Ketika kode tidak kompatibel dengan pemangkasan, anotasi dengan RequiresUnreferencedCode dan sebarkan anotasi ini ke penelepon sampai API publik yang relevan diannotasi.
  • Hindari menggunakan kode yang menggunakan pantulan dengan cara yang tidak dipahami oleh analisis statis. Misalnya, refleksi dalam konstruktor statis harus dihindari. Menggunakan refleksi yang tidak dapat dianalisis secara statis dalam konstruktor statis menghasilkan peringatan yang menyebar ke semua anggota kelas.
  • Hindari menganotasi metode virtual atau metode antarmuka. Menganotasi metode virtual atau antarmuka mengharuskan semua penimpaan memiliki anotasi yang cocok.
  • Jika API sebagian besar tidak kompatibel dengan pemangkasan, pendekatan pengodean alternatif ke API mungkin perlu dipertimbangkan. Contoh umumnya adalah serializer berbasis refleksi. Dalam kasus ini, pertimbangkan untuk mengadopsi teknologi lain seperti generator sumber untuk menghasilkan kode yang lebih mudah dianalisis secara statis. Misalnya, lihat Cara menggunakan pembuatan sumber di System.Text.Json

Mengatasi peringatan untuk pola yang tidak dapat dianalisa

Lebih baik menyelesaikan peringatan dengan mengekspresikan niat kode Anda menggunakan [RequiresUnreferencedCode] dan DynamicallyAccessedMembers jika memungkinkan. Namun, dalam beberapa kasus, Anda mungkin tertarik untuk mengaktifkan pemangkasan pustaka yang menggunakan pola yang tidak dapat diekspresikan dengan atribut tersebut, atau tanpa merefaktor kode yang ada. Bagian ini menjelaskan beberapa cara tingkat lanjut untuk mengatasi peringatan analisis pemangkasan.

Peringatan

Teknik ini dapat mengubah perilaku atau kode Anda atau menghasilkan pengecualian run time jika digunakan dengan tidak benar.

UnconditionalSuppressMessage

Pertimbangkan kode yang:

  • Niat tidak dapat diekspresikan dengan anotasi.
  • Menghasilkan peringatan tetapi tidak mewakili masalah nyata pada run time.

Peringatan dapat ditekan UnconditionalSuppressMessageAttribute. Ini mirip SuppressMessageAttributedengan , tetapi bertahan di IL dan dihormati selama analisis pemangkasan.

Peringatan

Saat menekan peringatan, Anda bertanggung jawab untuk menjamin kompatibilitas pemangkasan kode berdasarkan invarian yang Anda ketahui benar oleh inspeksi dan pengujian. Berhati-hatilah dengan anotasi ini, karena jika salah, atau jika invarian kode Anda berubah, mereka mungkin akhirnya menyembunyikan kode yang salah.

Misalnya:

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

Dalam kode sebelumnya, properti pengindeks telah dianotasikan sehingga yang dikembalikan Type memenuhi persyaratan CreateInstance. Ini memastikan bahwa TypeWithConstructor konstruktor disimpan, dan bahwa panggilan ke CreateInstance tidak memperingatkan. Anotasi setter pengindeks memastikan bahwa setiap jenis yang disimpan dalam Type[] memiliki konstruktor. Namun, analisis tidak dapat melihat ini dan menghasilkan peringatan untuk getter, karena tidak tahu bahwa jenis yang dikembalikan memiliki konstruktornya dipertahankan.

Jika Anda yakin bahwa persyaratan terpenuhi, Anda dapat membungkam peringatan ini dengan menambahkan [UnconditionalSuppressMessage] ke getter:

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

Penting untuk digaris bawahi bahwa hanya valid untuk menekan peringatan jika ada anotasi atau kode yang memastikan anggota yang tercermin adalah target pantulan yang terlihat. Tidak cukup bahwa anggota adalah target panggilan, bidang, atau akses properti. Ini mungkin tampak seperti kasus kadang-kadang tetapi kode tersebut terikat untuk memecah akhirnya karena lebih banyak pengoptimalan pemangkasan ditambahkan. Properti, bidang, dan metode yang tidak terlihat target pantulan dapat di-inlin, nama mereka dihapus, dipindahkan ke jenis yang berbeda, atau dioptimalkan dengan cara yang memecah pantulannya. Saat menekan peringatan, hanya diizinkan untuk merefleksikan target yang merupakan target pantulan yang terlihat ke penganalisis pemangkasan di tempat lain.

// 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] dapat digunakan untuk menunjukkan bahwa anggota memiliki dependensi dinamis pada anggota lain. Ini menghasilkan anggota yang dirujuk disimpan setiap kali anggota dengan atribut disimpan, tetapi tidak membungkam peringatan sendiri. Tidak seperti atribut lain, yang menginformasikan analisis pemangkasan tentang perilaku pantulan kode, [DynamicDependency] hanya menyimpan anggota lain. Ini dapat digunakan bersama-sama dengan [UnconditionalSuppressMessage] untuk memperbaiki beberapa peringatan analisis.

Peringatan

Gunakan [DynamicDependency] atribut hanya sebagai upaya terakhir ketika pendekatan lain tidak layak. Lebih baik mengekspresikan perilaku refleksi menggunakan [RequiresUnreferencedCode] atau [DynamicallyAccessedMembers].

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

Tanpa DynamicDependency, pemangkasan dapat menghapus Helper dari MyAssembly atau menghapus MyAssembly sepenuhnya jika tidak dirujuk di tempat lain, menghasilkan peringatan yang menunjukkan kemungkinan kegagalan pada waktu proses. Atribut memastikan bahwa Helper disimpan.

Atribut menentukan anggota untuk disimpan melalui string atau melalui DynamicallyAccessedMemberTypes. Jenis dan assembly adalah implisit dalam konteks atribut, atau secara eksplisit ditentukan dalam atribut (menurut Type, atau dengan strings untuk jenis dan nama rakitan).

String jenis dan anggota menggunakan variasi format string ID komentar dokumentasi C#, tanpa awalan anggota. String anggota tidak boleh menyertakan nama jenis deklarasikan, dan dapat menghilangkan parameter untuk mempertahankan semua anggota dari nama yang ditentukan. Beberapa contoh format diperlihatkan dalam kode berikut:

[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] ini dirancang untuk digunakan dalam kasus di mana metode berisi pola pantulan yang tidak dapat dianalisis bahkan dengan bantuan DynamicallyAccessedMembersAttribute.