Cortar um aplicativo .NET MAUI
Ao criar seu aplicativo, a interface do usuário do aplicativo .NET multiplataforma (.NET MAUI) pode usar um vinculador chamado ILLink para reduzir o tamanho geral do aplicativo com uma técnica conhecida como corte. ILLink reduz o tamanho analisando o código intermediário produzido pelo compilador. Remove métodos, propriedades, campos, eventos, structs e classes não utilizados para produzir um aplicativo que contém apenas dependências de código e assembly necessárias para executar o aplicativo.
Para evitar alterações no comportamento ao cortar aplicativos, o .NET fornece análise estática da compatibilidade de corte por meio de avisos de corte. O Cortador produz avisos de corte quando encontra código que pode não ser compatível com corte. Se houver algum aviso de corte, ele deverá ser corrigido e o aplicativo deverá ser testado minuciosamente após o corte para garantir que não haja alterações de comportamento. Para obter mais informações, consulte Introdução aos avisos de corte.
Comportamento de corte
O comportamento de corte pode ser controlado definindo a $(TrimMode)
propriedade build como ou partial
full
:
<PropertyGroup>
<TrimMode>full</TrimMode>
</PropertyGroup>
Importante
A $(TrimMode)
propriedade build não deve ser condicionada pela configuração de build. Isso ocorre porque as opções de recursos são habilitadas ou desabilitadas com base no valor da propriedade de build, e os mesmos recursos devem ser habilitados ou desabilitados em todas as configurações de $(TrimMode)
build para que seu código se comporte de forma idêntica.
O full
modo de corte remove qualquer código que não seja usado pelo seu aplicativo. O partial
modo de corte corta a BCL (biblioteca de classes base), assemblies para as plataformas subjacentes (como Mono.Android.dll e Microsoft.iOS.dll) e quaisquer outros assemblies que tenham optado por cortar com o $(TrimmableAsssembly)
item de compilação:
<ItemGroup>
<TrimmableAssembly Include="MyAssembly" />
</ItemGroup>
Isso é equivalente à configuração [AssemblyMetadata("IsTrimmable", "True")]
ao criar o assembly.
Observação
Não é necessário definir a $(PublishTrimmed)
propriedade build como true
no arquivo de projeto do aplicativo, pois isso é definido por padrão.
Para obter mais opções de corte, consulte Opções de corte.
Padrões de corte
Por padrão, as compilações do Catalyst para Android e Mac usam corte parcial quando a configuração de compilação é definida como uma compilação de versão. O iOS usa o corte parcial para qualquer build de dispositivo, independentemente da configuração de build, e não usa o corte para builds de simulador.
Cortando incompatibilidades
Os seguintes recursos do .NET MAUI são incompatíveis com o corte completo e serão removidos pelo aparador:
- Expressões de associação em que esse caminho de associação é definido como uma cadeia de caracteres. Em vez disso, use associações compiladas. Para obter mais informações, confira Associações compiladas.
- Operadores de conversão implícitos, ao atribuir um valor de um tipo incompatível a uma propriedade em XAML ou quando duas propriedades de tipos diferentes usam uma associação de dados. Em vez disso, você deve definir a TypeConverter para o seu tipo e anexá-lo ao tipo usando o TypeConverterAttribute. Para obter mais informações, consulte Definir um TypeConverter para substituir um operador de conversão implícito.
- Carregando XAML em runtime com o método de LoadFromXaml extensão. Esse XAML pode ser tornado seguro para corte anotando todos os tipos que podem ser carregados em runtime com o
DynamicallyAccessedMembers
atributo ou oDynamicDependency
atributo. No entanto, isso é muito propenso a erros e não é recomendado. - Receber dados de navegação usando o QueryPropertyAttribute. Em vez disso, você deve implementar a IQueryAttributable interface em tipos que precisam aceitar parâmetros de consulta. Para obter mais informações, confira Processar os dados de navegação usando um só método.
- A propriedade de
SearchHandler.DisplayMemberName
. Em vez disso, você deve fornecer um ItemTemplate para definir a aparência dos resultados SearchHandler. Para obter mais informações, consulte Definir a aparência do item de resultados da pesquisa.
Como alternativa, você pode usar opções de recursos para que o aparador preserve o código desses recursos. Para obter mais informações, consulte Chaves de recurso de corte.
Para incompatibilidades de corte do .NET, consulte Incompatibilidades de corte conhecidas.
Definir um TypeConverter para substituir um operador de conversão implícito
Não é possível confiar em operadores de conversão implícitos ao atribuir um valor de um tipo incompatível a uma propriedade em XAML ou quando duas propriedades de tipos diferentes usam uma associação de dados, quando o corte completo está habilitado. Isso ocorre porque os métodos de operador implícitos podem ser removidos pelo aparador se não forem usados em seu código C#. Para obter mais informações sobre operadores de conversão implícitos, consulte Operadores de conversão explícitos e implícitos definidos pelo usuário.
Por exemplo, considere o seguinte tipo que define operadores de conversão implícitos entre SizeRequest
e Size
:
namespace MyMauiApp;
public struct SizeRequest : IEquatable<SizeRequest>
{
public Size Request { get; set; }
public Size Minimum { get; set; }
public SizeRequest(Size request, Size minimum)
{
Request = request;
Minimum = minimum;
}
public SizeRequest(Size request)
{
Request = request;
Minimum = request;
}
public override string ToString()
{
return string.Format("{{Request={0} Minimum={1}}}", Request, Minimum);
}
public bool Equals(SizeRequest other) => Request.Equals(other.Request) && Minimum.Equals(other.Minimum);
public static implicit operator SizeRequest(Size size) => new SizeRequest(size);
public static implicit operator Size(SizeRequest size) => size.Request;
public override bool Equals(object? obj) => obj is SizeRequest other && Equals(other);
public override int GetHashCode() => Request.GetHashCode() ^ Minimum.GetHashCode();
public static bool operator ==(SizeRequest left, SizeRequest right) => left.Equals(right);
public static bool operator !=(SizeRequest left, SizeRequest right) => !(left == right);
}
Com o corte completo habilitado, os operadores de conversão implícitos entre SizeRequest
e Size
podem ser removidos pelo aparador se não forem usados em seu código C#.
Em vez disso, você deve definir um TypeConverter para o seu tipo e anexá-lo ao tipo usando o TypeConverterAttribute:
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
namespace MyMauiApp;
[TypeConverter(typeof(SizeRequestTypeConverter))]
public struct SizeRequest : IEquatable<SizeRequest>
{
public Size Request { get; set; }
public Size Minimum { get; set; }
public SizeRequest(Size request, Size minimum)
{
Request = request;
Minimum = minimum;
}
public SizeRequest(Size request)
{
Request = request;
Minimum = request;
}
public override string ToString()
{
return string.Format("{{Request={0} Minimum={1}}}", Request, Minimum);
}
public bool Equals(SizeRequest other) => Request.Equals(other.Request) && Minimum.Equals(other.Minimum);
public static implicit operator SizeRequest(Size size) => new SizeRequest(size);
public static implicit operator Size(SizeRequest size) => size.Request;
public override bool Equals(object? obj) => obj is SizeRequest other && Equals(other);
public override int GetHashCode() => Request.GetHashCode() ^ Minimum.GetHashCode();
public static bool operator ==(SizeRequest left, SizeRequest right) => left.Equals(right);
public static bool operator !=(SizeRequest left, SizeRequest right) => !(left == right);
private sealed class SizeRequestTypeConverter : TypeConverter
{
public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType)
=> sourceType == typeof(Size);
public override object? ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value)
=> value switch
{
Size size => (SizeRequest)size,
_ => throw new NotSupportedException()
};
public override bool CanConvertTo(ITypeDescriptorContext? context, [NotNullWhen(true)] Type? destinationType)
=> destinationType == typeof(Size);
public override object? ConvertTo(ITypeDescriptorContext? context, CultureInfo? culture, object? value, Type destinationType)
{
if (value is SizeRequest sizeRequest)
{
if (destinationType == typeof(Size))
return (Size)sizeRequest;
}
throw new NotSupportedException();
}
}
}
Opções de recursos de corte
O .NET MAUI tem diretivas de corte, conhecidas como opções de recursos, que possibilitam preservar o código para recursos que não são seguros para corte. Essas diretivas trimmer podem ser usadas quando a $(TrimMode)
propriedade build é definida como full
, bem como para NativeAOT:
Propriedade do MSBuild | Descrição |
---|---|
MauiEnableVisualAssemblyScanning |
Quando definido como true , o .NET MAUI examinará assemblies em busca de tipos que implementam IVisual e atributos [assembly:Visual(...)] e os registrará. Por padrão, essa propriedade de build é definida como false . |
MauiShellSearchResultsRendererDisplayMemberNameSupported |
Quando definido como false , o valor de SearchHandler.DisplayMemberName será ignorado. Em vez disso, você deve fornecer um ItemTemplate para definir a aparência dos resultados SearchHandler. Por padrão, essa propriedade de build é definida como true . |
MauiQueryPropertyAttributeSupport |
Quando definidos como false , os atributos [QueryProperty(...)] não serão usados para definir valores de propriedade durante a navegação. Em vez disso, você deve implementar a interface IQueryAttributable para aceitar parâmetros de consulta. Por padrão, essa propriedade de build é definida como true . |
MauiImplicitCastOperatorsUsageViaReflectionSupport |
Quando definido como false , o .NET MAUI não procurará operadores de conversão implícitos ao converter valores de um tipo para outro. Isso pode afetar associações entre propriedades com tipos diferentes e definir o valor de propriedade de um objeto associável com um valor de um tipo diferente. Em vez disso, você deve definir um TypeConverter para o tipo e anexá-lo ao tipo usando o atributo TypeConverterAttribute. Por padrão, essa propriedade de build é definida como true . |
_MauiBindingInterceptorsSupport |
Quando definido como false , o .NET MAUI não interceptará nenhuma chamada para os métodos SetBinding e não tentará compilá-los. Por padrão, essa propriedade de build é definida como true . |
MauiEnableXamlCBindingWithSourceCompilation |
Quando definido como true , o .NET MAUI compilará todas as associações, incluindo aquelas em que a Source propriedade é usada. Se você habilitar esse recurso, certifique-se de que todas as associações tenham o correto x:DataType para que sejam compiladas ou limpe o tipo de dados com x:Data={x:Null}} se a associação não deve ser compilada. Por padrão, essa propriedade de build só é definida como quando o true corte completo ou a implantação AOT nativa está habilitada. |
Essas propriedades do MSBuild também têm opções equivalentes AppContext :
- A
MauiEnableVisualAssemblyScanning
propriedade MSBuild tem uma opção equivalente AppContext chamadaMicrosoft.Maui.RuntimeFeature.IsIVisualAssemblyScanningEnabled
. - A
MauiShellSearchResultsRendererDisplayMemberNameSupported
propriedade MSBuild tem uma opção equivalente AppContext chamadaMicrosoft.Maui.RuntimeFeature.IsShellSearchResultsRendererDisplayMemberNameSupported
. - A
MauiQueryPropertyAttributeSupport
propriedade MSBuild tem uma opção equivalente AppContext chamadaMicrosoft.Maui.RuntimeFeature.IsQueryPropertyAttributeSupported
. - A
MauiImplicitCastOperatorsUsageViaReflectionSupport
propriedade MSBuild tem uma opção equivalente AppContext chamadaMicrosoft.Maui.RuntimeFeature.IsImplicitCastOperatorsUsageViaReflectionSupported
. - A
_MauiBindingInterceptorsSupport
propriedade MSBuild tem uma opção equivalente AppContext chamadaMicrosoft.Maui.RuntimeFeature.AreBindingInterceptorsSupported
. - A
MauiEnableXamlCBindingWithSourceCompilation
propriedade MSBuild tem uma opção equivalente AppContext chamadaMicrosoft.Maui.RuntimeFeature.MauiEnableXamlCBindingWithSourceCompilationEnabled
.
A maneira mais fácil de consumir uma opção de recurso é colocar a propriedade MSBuild correspondente no arquivo de projeto do aplicativo (*.csproj), o que faz com que o código relacionado seja cortado dos assemblies MAUI do .NET.
Preservar código
Quando você usa o aparador, às vezes ele remove o código que você pode ter chamado dinamicamente, mesmo indiretamente. Você pode instruir o aparador a preservar os membros anotando-os com o DynamicDependency
atributo. Esse atributo pode ser usado para expressar uma dependência em um tipo e subconjunto de membros ou em membros específicos.
Importante
Todos os membros do BCL que não podem ser determinados estaticamente para serem usados pelo aplicativo estão sujeitos a serem removidos.
O atributo DynamicDependency
pode ser aplicado a construtores, campos e métodos:
[DynamicDependency("Helper", "MyType", "MyAssembly")]
static void RunHelper()
{
var helper = Assembly.Load("MyAssembly").GetType("MyType").GetMethod("Helper");
helper.Invoke(null, null);
}
Neste exemplo, DynamicDependency
garante que o método Helper
seja mantido. Sem o atributo, o corte removeria Helper
MyAssembly
ou removeria MyAssembly
completamente se não fosse referenciado em outro lugar.
O atributo especifica o membro a ser mantido por meio de um string
ou por meio do atributo DynamicallyAccessedMembers
. O tipo e o assembly são implícitos no contexto do atributo ou explicitamente especificados no atributo (por Type
, ou por string
s para o tipo e nome do assembly).
As cadeias de caracteres de tipo e membro usam uma variação do formato de cadeia de caracteres de ID de comentário da documentação C#, sem o prefixo do membro. A cadeia de caracteres de membro não deve incluir o nome do tipo declarativo e pode omitir parâmetros para manter todos os membros do nome especificado. Os exemplos a seguir mostram usos válidos:
[DynamicDependency("Method()")]
[DynamicDependency("Method(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<>))]
Preservar assemblies
É possível especificar assemblies que devem ser excluídos do processo de corte, permitindo que outros assemblies sejam cortados. Essa abordagem pode ser útil quando você não pode usar facilmente o DynamicDependency
atributo ou não controla o código que está sendo cortado.
Quando ele corta todos os assemblies, você pode dizer ao trimmer para ignorar um assembly definindo um TrimmerRootAssembly
item do MSBuild no arquivo de projeto:
<ItemGroup>
<TrimmerRootAssembly Include="MyAssembly" />
</ItemGroup>
Observação
A extensão .dll
não é necessária ao definir a propriedade TrimmerRootAssembly
MSBuild.
Se o aparador ignorar um assembly, ele será considerado enraizado, o que significa que ele e todas as suas dependências estaticamente compreendidas serão mantidos. Você pode ignorar os outros assemblies adicionando mais propriedades TrimmerRootAssembly
MSBuild ao <ItemGroup>
.
Preservar assemblies, tipos e membros
Você pode passar ao aparador um arquivo de descrição XML que especifica quais montagens, tipos e membros precisam ser mantidos.
Para excluir um membro do processo de corte ao aparar todos os assemblies, defina o TrimmerRootDescriptor
item do MSBuild no arquivo de projeto como o arquivo XML que define os membros a serem excluídos:
<ItemGroup>
<TrimmerRootDescriptor Include="MyRoots.xml" />
</ItemGroup>
Em seguida, o arquivo XML usa o formato de descritor de corte para definir quais membros excluir:
<linker>
<assembly fullname="MyAssembly">
<type fullname="MyAssembly.MyClass">
<method name="DynamicallyAccessedMethod" />
</type>
</assembly>
</linker>
Neste exemplo, o arquivo XML especifica um método que é acessado dinamicamente pelo aplicativo, que é excluído do corte.
Quando uma montagem, tipo ou membro é listado no XML, a ação padrão é preservação, o que significa que, independentemente de o aparador achar que é usado ou não, ele é preservado na saída.
Observação
As marcas de preservação são ambíguas inclusivas. Se você não fornecer o próximo nível de detalhes, ele incluirá todos os filhos. Se um assembly estiver listado sem nenhum tipo, todos os tipos e membros do assembly serão preservados.
Marcar uma montagem como segura para corte
Se você tiver uma biblioteca em seu projeto ou for um desenvolvedor de uma biblioteca reutilizável e quiser que o aparador trate seu assembly como aparável, poderá marcar o assembly como seguro para corte adicionando a IsTrimmable
propriedade MSBuild ao arquivo de projeto do assembly:
<PropertyGroup>
<IsTrimmable>true</IsTrimmable>
</PropertyGroup>
Isso marca o assembly como "pode ser cortado" e habilita os avisos de corte para o respectivo projeto. "Pode ser cortado" significa que o assembly é considerado compatível com cortes e não deve haver avisos relacionados a cortes quando o assembly for criado. Quando usados em um aplicativo cortado, os membros não utilizados do assembly serão removidos na saída final.
Definir a propriedade IsTrimmable
MSBuild como true
no arquivo de projeto insere o atributo AssemblyMetadata
no assembly:
[assembly: AssemblyMetadata("IsTrimmable", "True")]
Como alternativa, você pode adicionar o atributo AssemblyMetadata
ao assembly sem ter adicionado a propriedade IsTrimmable
MSBuild ao arquivo de projeto do assembly.
Observação
Se a propriedade IsTrimmable
MSBuild estiver definida para um assembly, isso substituirá o atributo AssemblyMetadata("IsTrimmable", "True")
. Isso permite que você opte pelo corte em um assembly mesmo que ele não tenha o atributo ou desabilite o corte de um assembly que tenha o atributo.
Suprimir avisos de análise
Quando o aparador está ativado, ele remove IL que não é estaticamente acessível. Como resultado, os aplicativos que usam reflexão ou outros padrões que criam dependências dinâmicas podem ser interrompidos. Para avisar sobre esses padrões, ao marcar um assembly como seguro para corte, os autores da biblioteca devem definir a SuppressTrimAnalysisWarnings
propriedade MSBuild como false
:
<PropertyGroup>
<SuppressTrimAnalysisWarnings>false</SuppressTrimAnalysisWarnings>
</PropertyGroup>
Não suprimir avisos de análise de corte incluirá avisos sobre todo o aplicativo, incluindo seu próprio código, código de biblioteca e código SDK.
Mostrar avisos detalhados
A análise de corte produz no máximo um aviso para cada assembly proveniente de um PackageReference
, indicando que os internos do assembly não são compatíveis com cortes. Como autor da biblioteca, ao marcar um assembly como trim safe, você deve habilitar avisos individuais para todos os assemblies definindo a TrimmerSingleWarn
propriedade MSBuild como false
:
<PropertyGroup>
<TrimmerSingleWarn>false</TrimmerSingleWarn>
</PropertyGroup>
Essa configuração mostra todos os avisos detalhados, em vez de recolhê-los em um único aviso por assembly.