Start Debugging

Migre um app Android do .NET MAUI de Mono para CoreCLR no .NET 11

Uma migração passo a passo de Mono para CoreCLR no .NET MAUI para Android: o piso da API 24, as propriedades de MSBuild exclusivas do Mono que agora quebram sua build, por que seu APK cresceu, como perfilar a regressão de inicialização com dotnet-dsrouter e dotnet-trace, e como é um rollback de verdade agora que o caminho do Mono acabou.

Para um app pequeno, esta migração é uma troca de TargetFramework, uma troca de android:minSdkVersion e uma tarde de medições. Para um app grande, reserve uma semana, e espere que a semana inteira vá para duas coisas: apagar propriedades de MSBuild da era Mono que agora não fazem nada ou quebram a build ativamente, e caçar uma regressão de inicialização que não tem nada a ver com o seu código. O retorno é real (diagnóstico unificado, JIT em camadas, PGO dinâmico, um caminho plausível para o Native AOT no Android), mas a leitura honesta é que isso não é opcional. Desde o .NET 11 Preview 6, a Microsoft não expõe mais um caminho separado do Mono para Android, iOS ou Mac Catalyst. Este guia mira o .NET 11 Preview 7 (11.0.100-preview.7, lançado em 2026-08-11) com o .NET MAUI 11.0.0-preview.7, migrando a partir do .NET 10 com Mono. A versão final do .NET 11 está marcada para 2026-11-10.

Por que vale a pena além de “você não tem escolha”

O que quebra

ÁreaMudançaSeveridade
API mínima do AndroidSobe de 21 (Android 5.0) para 24 (Android 7.0)alta
ABIs do AndroidAndroid x86 (32 bits) não é suportado no CoreCLRalta
Propriedades do Mono AOTRunAOTCompilation, AndroidAotMode, UseInterpreter são exclusivas do Mono; RunAOTCompilation=true ainda pode invocar o MonoAOTCompiler e quebrar a buildalta
Tempo de inicializaçãoApps grandes relataram regressões de vários segundos e ANRsalta (depende do caso)
Tamanho do APKAs imagens R2R ficam dentro dos seus arquivos .dll, então os assemblies crescemmédia
Pacotes NuGetNU1703 quando um pacote resolve ativos MonoAndroid em vez de net6.0-android ou posteriormédia
Recursos legadosXA0149 para recursos legados do Xamarin.Android embutidos em uma dependênciabaixa
Microsoft.Maui.Controls.CompatibilityPacote removido no Preview 6média (só se referenciado explicitamente)
Erros HTTPFalhas de transporte do AndroidMessageHandler lançam HttpRequestException em vez de WebExceptionbaixa
Embedding do runtimeAs APIs de embedding do Android não seguem para o CoreCLRalta (se você as usa)

O piso de nível de API é o que chega aos seus usuários. Segundo o aviso de breaking change, apps compilados com .NET 11 não podem ser instalados nem executados em API 21, 22 ou 23. Confira seus números de distribuição no Play Console antes de começar, porque esta é uma decisão sobre usuários, não uma configuração de build.

Checklist de preparação

Passos da migração

  1. Capture a linha de base do Mono. Na sua build Release atual do .NET 10, instale o APK e meça a inicialização a frio com o gerenciador de atividades do Android, que reporta TotalTime em milissegundos:

    # .NET 10, Mono, Release
    adb shell am force-stop com.example.myapp
    adb shell am start -W -n com.example.myapp/crc64...MainActivity

    Rode cinco vezes, descarte a primeira e anote a mediana. Anote também o tamanho do APK ou AAB de Release. Verifique: você tem dois números escritos em algum lugar que não seja o histórico do terminal.

  2. Mova o target framework e o piso de API juntos. As duas mudanças, no mesmo commit, porque o CoreCLR no Android exige API 24:

    <!-- .NET 11 Preview 7, MAUI 11.0.0-preview.7 -->
    <PropertyGroup>
      <TargetFrameworks>net11.0-android;net11.0-ios;net11.0-maccatalyst</TargetFrameworks>
      <SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'android'">24.0</SupportedOSPlatformVersion>
    </PropertyGroup>

    Se você define android:minSdkVersion na mão em Platforms/Android/AndroidManifest.xml, suba para 24 para que o manifesto e o projeto concordem. Verifique: dotnet build -f net11.0-android -c Release passa e o manifesto gerado mostra minSdkVersion="24".

  3. Apague ou condicione toda propriedade de MSBuild exclusiva do Mono. Faça grep no seu .csproj, no Directory.Build.props e em qualquer propriedade injetada pelo CI atrás de RunAOTCompilation, AndroidAotMode, AndroidEnableProfiledAot, UseInterpreter e UseMonoRuntime. Deixar RunAOTCompilation=true em um Directory.Build.props é uma quebra de build conhecida: o target MonoAOTCompiler ainda roda mesmo com o app no CoreCLR (dotnet/android#11068). Apague de vez ou, se você ainda compila um TFM antigo em paralelo, condicione:

    <PropertyGroup Condition="'$(UseMonoRuntime)' == 'true'">
      <RunAOTCompilation>true</RunAOTCompilation>
      <AndroidEnableProfiledAot>true</AndroidEnableProfiledAot>
    </PropertyGroup>

    Verifique: dotnet build -f net11.0-android -c Release -bl e depois procure MonoAOTCompiler no log binário. Zero ocorrências é a condição de aprovação.

  4. Limpe a lista de ABIs e os avisos de pacote. Tire x86 de RuntimeIdentifiers se ainda estiver lá, já que o CoreCLR não distribui essa arquitetura:

    <RuntimeIdentifiers>android-arm64;android-x64</RuntimeIdentifiers>

    Depois lide com o NU1703. Introduzido no Preview 5, ele dispara quando um pacote resolve ativos da pasta obsoleta MonoAndroid: “Package ‘PackageName’ 1.0.0 uses the deprecated MonoAndroid framework instead of ‘net6.0-android’ or later.” Atualize o pacote se existir uma versão moderna. Se não existir, você encontrou uma dependência da era Xamarin que está com os dias contados, e suprimir o aviso é uma decisão de carregar esse risco, não uma correção. Verifique: dotnet restore está limpo, ou cada NU1703 restante é um pacote que você triou conscientemente.

  5. Recompile em Release e meça de novo contra o passo 1. Mesmo aparelho, mesmo procedimento, mesmo número de execuções:

    # .NET 11 Preview 7, CoreCLR, Release
    dotnet publish -f net11.0-android -c Release
    adb install -r bin/Release/net11.0-android/publish/com.example.myapp-Signed.apk
    adb shell am force-stop com.example.myapp
    adb shell am start -W -n com.example.myapp/crc64...MainActivity

    A posição da própria Microsoft é que o Android fica “dentro de 10 por cento do Mono em inicialização e tamanho de app” para um app de template base. Verifique: se você está dentro dessa faixa, o trabalho de desempenho acabou. Se está em 2x ou pior, vá para o passo 6 em vez de sair alternando propriedades de MSBuild na sorte.

  6. Perfile a regressão em vez de adivinhar. Adicione um arquivo app.env ao lado do .csproj contendo DOTNET_DiagnosticPorts=127.0.0.1:9000,suspend e referencie de forma condicional:

    <ItemGroup Condition="'$(AndroidEnableProfiler)'=='true'">
      <AndroidEnvironment Include="app.env" />
    </ItemGroup>

    Suba o router, compile com o profiler habilitado, abra o app e então conecte:

    dotnet-dsrouter server-server -ipcs ~/mylocalport -tcps 127.0.0.1:9000 --forward-port Android &
    dotnet build -f net11.0-android -c Release -t:Run /p:AndroidEnableProfiler=true
    dotnet-trace collect --diagnostic-port ~/mylocalport,connect

    Como a porta foi configurada com suspend, o runtime trava na inicialização até o dotnet-trace conectar, que é exatamente o que você precisa para ver o caminho de inicialização e não tudo o que vem depois. No Windows, use mylocalport em vez de ~/mylocalport, já que o canal IPC é um named pipe. Verifique: você tem um arquivo .nettrace com uma janela de inicialização preenchida e consegue nomear os três métodos com maior tempo inclusivo.

  7. Ajuste só o que o trace justificar. Se o problema é o tamanho dos assemblies, o R2R é o primeiro botão, porque as imagens R2R vão empacotadas dentro dos arquivos .dll e é por isso que seus assemblies cresceram:

    <PropertyGroup Condition="'$(Configuration)' == 'Release'">
      <PublishReadyToRun>false</PublishReadyToRun>  <!-- smaller APK, slower startup -->
      <TrimMode>full</TrimMode>                     <!-- default is partial -->
    </PropertyGroup>

    Os dois puxam em direções opostas: desligar o R2R troca inicialização por tamanho, e TrimMode=full recupera tamanho mas passa a recortar o seu código e suas referências NuGet, então exige uma rodada completa de regressão. Mude um de cada vez e refaça o passo 5 entre cada um. Verifique: cada botão está justificado por um delta medido que você consegue citar, não por um post de blog.

  8. Faça um rollout em fases. Publique primeiro em uma trilha interna e observe especificamente a taxa de ANR, não só a de crashes. O modo de falha relatado do CoreCLR em apps grandes é uma inicialização que demora o suficiente para o Android matar o processo, o que aparece como ANR e não como exceção. Verifique: a taxa de ANR no Play Console depois de uma semana de teste interno está estável em relação à sua build com Mono.

Checklist de verificação

Plano de rollback

Diga isso em voz alta: não existe mais rollback no nível do runtime. <UseMonoRuntime>true</UseMonoRuntime> foi documentado como a saída de emergência quando o CoreCLR virou padrão no Preview 4, e na época foi apresentado como um desbloqueio temporário enquanto você reportava uma regressão. O Preview 6 removeu o caminho separado do Mono para Android, iOS e Mac Catalyst. Trate a propriedade como inexistente e não monte um plano de release em cima dela.

Seu rollback de verdade é o target framework: mantenha a build net10.0-android verde em um branch até a build do .NET 11 sobreviver a um rollout real em produção. Isso é um rollback bem mais pesado do que virar uma propriedade, e é exatamente por isso que os passos 1 e 5 existem.

Armadilhas que custam tempo de verdade

A regressão de inicialização é real e não está distribuída de forma uniforme. Duas issues documentam o modo de falha: a dotnet/android#10588 relata que “an app that takes 1s to launch on mono can take 6s on coreclr”, com ANRs no ControlCatalog.Android do Avalonia, e a dotnet/android#10914 relata cerca de 1,0 s para 6,0 s de inicialização a frio e um crescimento de APK de 21 MB para 38 MB no 11.0.100-preview.2. As duas são do Avalonia, não do MAUI, e as duas são anteriores ao trabalho de R2R composto parcial e de perfis MIBC que chegou mais tarde no ciclo de preview, então não leia isso como o seu resultado esperado. Leia como o motivo pelo qual o passo 1 é obrigatório.

Os caminhos de inicialização pesados em XAML são os que doem. O fio comum nos relatos é reflexão e parsing de XAML durante a inicialização, que é exatamente o trabalho que o R2R parcial não consegue pré-compilar se o perfil .mibc distribuído não cobrir o formato do seu app. Se o seu app monta uma árvore visual grande antes do primeiro frame, é ali que se olha primeiro.

O UseInterpreter silenciosamente deixa de importar. Ele era true por padrão em Debug no Mono, e era o que fazia o Hot Reload da era Mono funcionar. No CoreCLR ele é inerte. Se você o tinha ligado por um motivo (algum caminho de código dinâmico que o Mono AOT não dava conta), esse motivo não sumiu, só mudou de lugar: o CoreCLR no Android roda um JIT de verdade em Debug, então o código vai funcionar, mas teste de novo de propósito em vez de assumir.

O conteúdo do seu APK muda de forma. No Mono você distribuía libmonosgen-2.0.so mais imagens libaot-*.dll.so. No CoreCLR você distribui libcoreclr.so, libclrjit.so, libmonodroid.so (a cola do Android mantém o nome da era Mono) e um único libassemblies.arm64-v8a.so com MSIL comprimido e imagens R2R. Se você tem scripts de build, orçamentos de tamanho ou configuração de ProGuard/R8 que citam esses arquivos, eles precisam ser atualizados.

O tamanho está mesmo no trimming. O MAUI ainda usa TrimMode=partial por padrão, que recorta os assemblies do framework mas deixa o seu código e suas referências NuGet intactos. A maioria das reclamações de tamanho vira reclamação de trimming assim que você olha o detalhamento por assembly.

Relacionados

Fontes

Comments

Sign in with GitHub to comment. Reactions and replies thread back to the comments repo.

< Voltar