Подготовка библиотек .NET для обрезки
Пакет SDK для .NET позволяет уменьшить размер автономных приложений путем обрезки. Обрезка удаляет неиспользуемый код из приложения и его зависимостей. Не все коды совместимы с обрезкой. .NET предоставляет предупреждения об анализе для обнаружения шаблонов, которые могут нарушить обрезку приложений. В этой статье:
- Описывает, как подготовить библиотеки для обрезки.
- Предоставляет рекомендации по устранению распространенных предупреждений обрезки.
Необходимые компоненты
Пакет SDK для .NET 8 или более поздней версии.
Включение предупреждений об обрезке в библиотеке
Предупреждения обрезки в библиотеке можно найти с помощью любого из следующих методов:
- Включение обрезки для конкретного проекта с помощью
IsTrimmable
свойства. - Создание тестового приложения для обрезки, использующего библиотеку и включение обрезки для тестового приложения. Не обязательно ссылаться на все API в библиотеке.
Мы рекомендуем использовать оба подхода. Обрезка для конкретного проекта удобна и отображает предупреждения об обрезки для одного проекта, но использует ссылки, помеченные как обрезные, чтобы просмотреть все предупреждения. Обрезка тестового приложения более трудоемка, но отображает все предупреждения.
Включение обрезки для конкретного проекта
Задайте <IsTrimmable>true</IsTrimmable>
в файле проекта.
<PropertyGroup>
<IsTrimmable>true</IsTrimmable>
</PropertyGroup>
Задание свойства IsTrimmable
MSBuild для true
метки сборки как "обрезаемой" и включает предупреждения обрезки. "Trimmable" означает проект:
- Считается совместимым с обрезкой.
- При сборке не следует создавать предупреждения, связанные с обрезками. При использовании в обрезаном приложении сборка имеет неиспользуемые элементы, обрезанные в окончательных выходных данных.
Свойство IsTrimmable
по умолчанию используется true
при настройке проекта как совместимого с <IsAotCompatible>true</IsAotCompatible>
AOT. Дополнительные сведения см . в анализаторах совместимости AOT.
Чтобы создать предупреждения об обрезки без маркировки проекта как совместимого с обрезкой, используйте <EnableTrimAnalyzer>true</EnableTrimAnalyzer>
вместо <IsTrimmable>true</IsTrimmable>
этого.
Отображение всех предупреждений с помощью тестового приложения
Чтобы отобразить все предупреждения анализа для библиотеки, триммер должен проанализировать реализацию библиотеки и всех зависимостей, которые использует библиотека.
При создании и публикации библиотеки:
- Реализации зависимостей недоступны.
- Доступные эталонные сборки не имеют достаточно сведений для триммера, чтобы определить, совместимы ли они с обрезкой.
Из-за ограничений зависимостей необходимо создать автономное тестовое приложение, которое использует библиотеку и его зависимости. Тестовое приложение содержит всю информацию, которую триммер требует, чтобы вывести предупреждение о несовместимости с обрезкой в:
- Код библиотеки.
- Код, ссылающийся на библиотеку из его зависимостей.
Примечание.
Если библиотека имеет другое поведение в зависимости от целевой платформы, создайте тестовое приложение обрезки для каждой из целевых платформ, поддерживающих обрезку. Например, если библиотека использует условную компиляцию , например #if NET7_0
для изменения поведения.
Чтобы создать тестовое приложение для обрезки, выполните следующие действия.
- Создайте отдельный проект консольного приложения.
- Добавьте ссылку на библиотеку.
- Измените проект, аналогичный проекту, приведенному ниже, с помощью следующего списка:
Если библиотека нацелена на TFM, которая не является обрезаемой, например net472
или netstandard2.0
нет преимуществ для создания тестового приложения обрезки. Обрезка поддерживается только для .NET 6 и более поздних версий.
- Добавьте
<PublishTrimmed>true</PublishTrimmed>
. - Добавьте ссылку на проект библиотеки с
<ProjectReference Include="/Path/To/YourLibrary.csproj" />
помощью . - Укажите библиотеку в качестве корневой сборки триммера.
<TrimmerRootAssembly Include="YourLibraryName" />
TrimmerRootAssembly
обеспечивает анализ каждой части библиотеки. Он сообщает триммеру, что эта сборка является корневым. Сборка root означает, что триммер анализирует каждый вызов в библиотеке и проходит все пути кода, исходящие из этой сборки.
CSPROJ-файл
<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>
После обновления файла проекта запустите dotnet publish
с помощью целевого идентификатора среды выполнения (RID).
dotnet publish -c Release -r <RID>
Следуйте приведенному выше шаблону для нескольких библиотек. Чтобы отображать предупреждения анализа обрезки одновременно для нескольких библиотек, добавьте их все в один проект в качестве элементов ProjectReference
и TrimmerRootAssembly
. Добавление всех библиотек в один и тот же проект с ProjectReference
TrimmerRootAssembly
элементами предупреждает о зависимостях, если какая-либо из корневых библиотек использует НЕуправляемый API в зависимости. Чтобы просмотреть предупреждения, связанные только с определенной библиотекой, следует ссылаться только на эту библиотеку.
Примечание.
Результаты анализа зависят от сведений о реализации зависимостей. Обновление до новой версии зависимости может привести к предупреждениям анализа:
- Если новая версия добавила неясные шаблоны отражения.
- Даже если не было изменений в API.
- Введение предупреждений об анализе обрезок является критическим изменением при использовании
PublishTrimmed
библиотеки.
Устранение предупреждений об обрезке
Описанные выше действия создают предупреждения о коде, которые могут вызвать проблемы при использовании в обрезаном приложении. В следующих примерах показаны наиболее распространенные предупреждения с рекомендациями по их устранению.
RequiresUnreferencedCode
Рассмотрим следующий код, который используется [RequiresUnreferencedCode]
для указания того, что указанный метод требует динамического доступа к коду, который не ссылается статически, например через 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()
{
}
}
Предыдущий выделенный код указывает, что библиотека вызывает метод, явно помеченный как несовместимый с обрезкой. Чтобы избавиться от предупреждения, рассмотрите MyMethod
необходимость вызова DynamicBehavior
. Если да, заметите вызывающий объект MyMethod
, с [RequiresUnreferencedCode]
помощью которого распространяется предупреждение, чтобы вызывающие абоненты MyMethod
получили предупреждение:
public class MyLibrary
{
[RequiresUnreferencedCode("Calls DynamicBehavior.")]
public static void MyMethod()
{
DynamicBehavior();
}
[RequiresUnreferencedCode(
"DynamicBehavior is incompatible with trimming.")]
static void DynamicBehavior()
{
}
}
После распространения атрибута вплоть до общедоступного API приложения, вызывающие библиотеку:
- Получение предупреждений только для общедоступных методов, которые не являются обрезаемыми.
- Не получайте предупреждения, как
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())
{
// ...
}
}
}
В приведенном выше коде UseMethods
вызывается метод отражения, имеющий [DynamicallyAccessedMembers]
требование. Требование указывает, что общедоступные методы типа доступны. Удовлетворить требование путем добавления того же требования к параметру UseMethods
.
static void UseMethods(
// State the requirement in the UseMethods parameter.
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] Type type)
{
// ...
}
Теперь все вызовы для UseMethods
создания предупреждений, если они передают значения, которые не соответствуют требованию PublicMethods . [RequiresUnreferencedCode]
Как и после распространения таких предупреждений на общедоступные API, все готово.
В следующем примере неизвестный тип передается в параметр аннотированного метода. Type
Неизвестно из поля:
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);
}
Аналогичным образом здесь проблема заключается в том, что поле type
передается в параметр с этими требованиями. Исправлено путем добавления [DynamicallyAccessedMembers]
в поле. [DynamicallyAccessedMembers]
предупреждает о коде, который назначает несовместимые значения полю. Иногда этот процесс продолжается до тех пор, пока общедоступный API не будет аннотирован, и в другое время он заканчивается, когда конкретный тип переходит в расположение с этими требованиями. Например:
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)]
static Type type;
static void UseMethodsHelper()
{
MyLibrary.type = typeof(System.Tuple);
}
В этом случае анализ обрезки сохраняет общедоступные методы Tupleи создает дополнительные предупреждения.
Рекомендации
- Избегайте отражения, когда это возможно. При использовании отражения свести к минимуму область отражения, чтобы она была доступна только из небольшой части библиотеки.
- Заметьте код со
DynamicallyAccessedMembers
статическим выражением требований к обрезке, когда это возможно. - Рекомендуется переорганизовать код, чтобы он соответствовал отанализируемому шаблону, который можно ознамещать с помощью
DynamicallyAccessedMembers
- Если код несовместим с обрезкой, аннотировать его и
RequiresUnreferencedCode
распространять эту заметку вызывающим абонентам до тех пор, пока соответствующие общедоступные API не будут аннотированы. - Избегайте использования кода, использующего отражение таким образом, чтобы не пониматься статическим анализом. Например, следует избегать отражения в статических конструкторах. Использование статически неанализируемого отражения в статических конструкторах приводит к тому, что предупреждение распространяется на все члены класса.
- Избегайте аннотирования виртуальных методов или методов интерфейса. Для аннотирования виртуальных или интерфейсных методов требуется, чтобы все переопределения имели соответствующие заметки.
- Если API в основном несовместим, альтернативные подходы к программированию к API могут быть рассмотрены. Распространенным примером являются сериализаторы на основе отражения. В таких случаях рассмотрите возможность внедрения других технологий, таких как генераторы источников, для создания кода, который проще анализировать статически. Например, см. раздел "Использование создания источника" в System.Text.Json
Устранение предупреждений для шаблонов, которые невозможно проанализировать
Лучше разрешать предупреждения, указывая назначение кода с помощью [RequiresUnreferencedCode]
и DynamicallyAccessedMembers
, когда это возможно. Однако в некоторых случаях может потребоваться включить обрезку библиотеки, использующую шаблоны, которые не могут быть выражены с этими атрибутами или без рефакторинга существующего кода. В этом разделе описаны некоторые расширенные способы устранения предупреждений анализа обрезки.
Предупреждение
Эти методы могут изменить поведение или код или привести к исключениям времени выполнения при неправильном использовании.
UnconditionalSuppressMessage
Рассмотрим код, который:
- Намерение не может быть выражено с заметками.
- Создает предупреждение, но не представляет реальную проблему во время выполнения.
Предупреждения можно отключить.UnconditionalSuppressMessageAttribute Это аналогично SuppressMessageAttribute
, но сохраняется в IL и учитывается во время анализа обрезки.
Предупреждение
При подавлении предупреждений вы несете ответственность за гарантию совместимости с обрезкой кода на основе инвариантных, которые вы знаете, что это верно путем проверки и тестирования. Используйте осторожность с этими заметками, так как если они неверны или если инварианты изменения кода, они могут скрыть неправильный код.
Например:
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
{
}
В приведенном выше коде свойство индексатора было аннотировано таким образом, чтобы возвращаемое Type
соответствовало требованиям CreateInstance
. Это гарантирует, что TypeWithConstructor
конструктор хранится и что вызов CreateInstance
не предупреждает. Заметка о наборе индексатора гарантирует, что все типы, хранящиеся в конструкторе Type[]
. Однако анализ не может увидеть это и создает предупреждение для метода получения, так как он не знает, что возвращаемый тип имеет его конструктор.
Если вы уверены, что выполнены требования, вы можете замолчать это предупреждение, добавив [UnconditionalSuppressMessage]
в метод получения:
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
{
}
Важно подчеркнуть, что это допустимо только для подавления предупреждения, если есть заметки или код, которые гарантируют, что отраженные элементы являются видимыми целевыми объектами отражения. Недостаточно, чтобы член был объектом доступа к вызову, полю или свойству. Иногда это может быть так, но такой код привязан к разрыву в конечном итоге, так как добавляются дополнительные оптимизации обрезки. Свойства, поля и методы, которые не являются видимыми целевыми объектами отражения, могут быть удалены, удалены их имена, перемещаются в разные типы или иначе оптимизированы таким образом, чтобы отражать их. При подавлении предупреждения можно выполнять отражение только тех целевых объектов, которые являются видимыми для анализатора обрезки в другом расположении.
// 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
Атрибут [DynamicDependency]
можно использовать для указания того, что член имеет динамическую зависимость от других членов. В результате указанные элементы сохраняются при каждом сохранении элемента с атрибутом, но без вывода предупреждения. В отличие от других атрибутов, которые информируют анализ обрезки о поведении отражения кода, [DynamicDependency]
сохраняет только другие элементы. Его можно использовать вместе с [UnconditionalSuppressMessage]
для устранения некоторых предупреждений анализа.
Предупреждение
Используйте [DynamicDependency]
атрибут только в качестве последнего способа, когда другие подходы недоступны. Предпочтительнее выразить поведение отражения с помощью [RequiresUnreferencedCode]
или [DynamicallyAccessedMembers]
.
[DynamicDependency("Helper", "MyType", "MyAssembly")]
static void RunHelper()
{
var helper = Assembly.Load("MyAssembly").GetType("MyType").GetMethod("Helper");
helper.Invoke(null, null);
}
Без DynamicDependency
обрезка может удалить Helper
из MyAssembly
или полностью удалить MyAssembly
, если на него нет ссылок в других местах, что приведет к предупреждениям о возможном сбое во время выполнения. Атрибут гарантирует, что Helper
будет сохранен.
Атрибут указывает элементы, которые должны быть сохранены, с помощью string
или DynamicallyAccessedMemberTypes
. Тип и сборка либо являются неявными в контексте атрибута, либо явно указаны в атрибуте (с помощью Type
или string
для типа и имени сборки).
В строках типа и элемента используется разновидность формата строки идентификатора комментария документации C# без префикса элемента. Строка-член не должна содержать имя декларационного типа и может опустить параметры, чтобы сохранить все члены указанного имени. Некоторые примеры формата показаны в следующем коде:
[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<>))]
Атрибут [DynamicDependency]
предназначен для использования в случаях, когда метод содержит шаблоны отражения, которые не могут быть проанализированы даже с помощью метода DynamicallyAccessedMembersAttribute
.