Start Debugging

Eine .NET MAUI Android-App in .NET 11 von Mono zu CoreCLR migrieren

Eine schrittweise Migration von Mono zu CoreCLR für .NET MAUI unter Android: die API-24-Untergrenze, die Mono-spezifischen MSBuild-Eigenschaften, die jetzt den Build brechen, warum das APK gewachsen ist, wie sich die Startregression mit dotnet-dsrouter und dotnet-trace analysieren lässt, und wie ein Rollback tatsächlich aussieht, nachdem der Mono-Pfad verschwunden ist.

Bei einer kleinen App ist diese Migration eine Änderung an TargetFramework, eine Änderung an android:minSdkVersion und ein Nachmittag mit Messungen. Bei einer großen App planen Sie eine Woche ein, und die gesamte Woche geht erfahrungsgemäß in zwei Dinge: das Entfernen von MSBuild-Eigenschaften aus der Mono-Ära, die jetzt entweder wirkungslos sind oder den Build aktiv brechen, und die Jagd auf eine Startregression, die nichts mit Ihrem Code zu tun hat. Der Gewinn ist real (einheitliche Diagnose, Tiered JIT, dynamisches PGO, ein plausibler Weg zu Native AOT unter Android), aber ehrlich betrachtet ist das hier nicht optional. Seit .NET 11 Preview 6 bietet Microsoft keinen separaten Mono-Pfad mehr für Android, iOS oder Mac Catalyst an. Dieser Leitfaden zielt auf .NET 11 Preview 7 (11.0.100-preview.7, veröffentlicht am 2026-08-11) mit .NET MAUI 11.0.0-preview.7 und migriert von .NET 10 mit Mono. Das finale .NET 11 ist für den 2026-11-10 geplant.

Warum sich das lohnt, unabhängig von “Sie haben keine Wahl”

Was bricht

BereichÄnderungSchweregrad
Minimale Android-APIAngehoben von 21 (Android 5.0) auf 24 (Android 7.0)hoch
Android-ABIsAndroid x86 (32 Bit) wird unter CoreCLR nicht unterstützthoch
Mono-AOT-EigenschaftenRunAOTCompilation, AndroidAotMode, UseInterpreter sind Mono-spezifisch; RunAOTCompilation=true kann weiterhin MonoAOTCompiler aufrufen und den Build brechenhoch
StartzeitGroße Apps melden Regressionen von mehreren Sekunden und ANRshoch (situativ)
APK-GrößeR2R-Images liegen in Ihren .dll-Dateien, die Assemblys wachsen entsprechendmittel
NuGet-PaketeNU1703, wenn ein Paket MonoAndroid-Assets statt net6.0-android oder neuer auflöstmittel
Alte RessourcenXA0149 für alte Xamarin.Android-Ressourcen in einer Abhängigkeitniedrig
Microsoft.Maui.Controls.CompatibilityPaket in Preview 6 entferntmittel (nur bei expliziter Referenz)
HTTP-FehlerTransportfehler von AndroidMessageHandler werfen HttpRequestException statt WebExceptionniedrig
Runtime-EmbeddingDie Android-Embedding-APIs werden nicht nach CoreCLR übernommenhoch (falls verwendet)

Die API-Untergrenze ist die Änderung, die bei Ihren Nutzern ankommt. Laut Breaking-Change-Hinweis lassen sich mit .NET 11 gebaute Apps auf API 21, 22 oder 23 weder installieren noch ausführen. Prüfen Sie Ihre Verteilungszahlen in der Play Console, bevor Sie anfangen, denn das ist eine Entscheidung über Nutzer, keine Build-Einstellung.

Checkliste vor dem Start

Migrationsschritte

  1. Erfassen Sie die Mono-Basislinie. Installieren Sie das APK Ihres aktuellen .NET 10-Release-Builds und messen Sie den Kaltstart mit dem Android Activity Manager, der TotalTime in Millisekunden ausgibt:

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

    Führen Sie das fünfmal aus, verwerfen Sie den ersten Lauf und notieren Sie den Median. Notieren Sie auch die Größe des Release-APK oder -AAB. Verifizierung: Sie haben zwei Zahlen an einem Ort notiert, der nicht Ihr Terminal-Scrollback ist.

  2. Ändern Sie Target Framework und API-Untergrenze gemeinsam. Beide Änderungen in einem Commit, denn CoreCLR unter Android setzt API 24 voraus:

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

    Wenn Sie android:minSdkVersion von Hand in Platforms/Android/AndroidManifest.xml setzen, heben Sie den Wert ebenfalls auf 24, damit Manifest und Projekt übereinstimmen. Verifizierung: dotnet build -f net11.0-android -c Release läuft durch und das generierte Manifest zeigt minSdkVersion="24".

  3. Entfernen oder konditionieren Sie jede Mono-spezifische MSBuild-Eigenschaft. Durchsuchen Sie .csproj, Directory.Build.props und alle vom CI eingeschleusten Eigenschaften nach RunAOTCompilation, AndroidAotMode, AndroidEnableProfiledAot, UseInterpreter und UseMonoRuntime. Ein in Directory.Build.props verbliebenes RunAOTCompilation=true ist ein bekannter Build-Bruch: Das Target MonoAOTCompiler läuft weiterhin, obwohl die App auf CoreCLR steht (dotnet/android#11068). Löschen Sie die Eigenschaften, oder konditionieren Sie sie, falls Sie noch ein älteres TFM parallel bauen:

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

    Verifizierung: dotnet build -f net11.0-android -c Release -bl ausführen und im Binärlog nach MonoAOTCompiler suchen. Null Treffer ist die Bestehensbedingung.

  4. Bereinigen Sie die ABI-Liste und die Paketwarnungen. Entfernen Sie x86 aus RuntimeIdentifiers, falls es noch dort steht, denn CoreCLR liefert diese Architektur nicht aus:

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

    Kümmern Sie sich dann um NU1703. Die in Preview 5 eingeführte Warnung erscheint, wenn ein Paket Assets aus dem veralteten MonoAndroid-Ordner auflöst: “Package ‘PackageName’ 1.0.0 uses the deprecated MonoAndroid framework instead of ‘net6.0-android’ or later.” Aktualisieren Sie das Paket, falls eine moderne Version existiert. Falls nicht, haben Sie eine Abhängigkeit aus der Xamarin-Ära gefunden, deren Zeit abläuft, und das Unterdrücken der Warnung ist die Entscheidung, dieses Risiko zu tragen, keine Lösung. Verifizierung: dotnet restore ist sauber, oder jede verbleibende NU1703 gehört zu einem Paket, das Sie bewusst bewertet haben.

  5. Bauen Sie in Release neu und messen Sie erneut gegen Schritt 1. Gleiches Gerät, gleiches Vorgehen, gleiche Anzahl an Läufen:

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

    Microsofts eigene Position lautet, dass Android bei einer Basis-Template-App “innerhalb von 10 Prozent von Mono bei Start und App-Größe” liegt. Verifizierung: Liegen Sie in diesem Band, ist die Performance-Arbeit erledigt. Liegen Sie bei 2x oder schlechter, gehen Sie zu Schritt 6, statt planlos MSBuild-Eigenschaften umzuschalten.

  6. Analysieren Sie die Regression, statt zu raten. Legen Sie neben der .csproj eine Datei app.env mit dem Inhalt DOTNET_DiagnosticPorts=127.0.0.1:9000,suspend an und referenzieren Sie sie bedingt:

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

    Starten Sie den Router, bauen Sie mit aktiviertem Profiler, starten Sie die App und verbinden Sie sich dann:

    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

    Da der Port mit suspend konfiguriert wurde, blockiert die Laufzeit beim Start, bis dotnet-trace sich verbindet, und genau das brauchen Sie, um den Startpfad zu sehen statt alles danach. Unter Windows verwenden Sie mylocalport statt ~/mylocalport, da der IPC-Kanal dort eine Named Pipe ist. Verifizierung: Sie haben eine .nettrace-Datei mit einem gefüllten Startfenster und können die drei teuersten Methoden nach inklusiver Zeit benennen.

  7. Drehen Sie nur an dem, was der Trace rechtfertigt. Ist die Assembly-Größe das Problem, ist R2R der erste Regler, denn R2R-Images stecken in den .dll-Dateien und genau deshalb sind Ihre Assemblys gewachsen:

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

    Beide ziehen in entgegengesetzte Richtungen: R2R abzuschalten tauscht Startzeit gegen Größe, und TrimMode=full holt Größe zurück, trimmt jetzt aber auch Ihren eigenen Code und Ihre NuGet-Referenzen, braucht also einen vollständigen Regressionsdurchlauf. Ändern Sie eines nach dem anderen und wiederholen Sie dazwischen Schritt 5. Verifizierung: Jeder Regler ist durch ein gemessenes Delta gerechtfertigt, das Sie benennen können, nicht durch einen Blogbeitrag.

  8. Rollen Sie stufenweise aus. Veröffentlichen Sie zuerst in einem internen Track und beobachten Sie gezielt die ANR-Rate, nicht nur die Absturzrate. Der gemeldete CoreCLR-Fehlermodus bei großen Apps ist ein Start, der lange genug dauert, dass Android den Prozess beendet, was sich als ANR und nicht als Ausnahme zeigt. Verifizierung: Die ANR-Rate in der Play Console ist nach einer Woche internem Test gegenüber Ihrem Mono-Build unverändert.

Verifizierungscheckliste

Rollback-Plan

Sagen Sie es deutlich: Ein Rollback auf Laufzeitebene gibt es nicht mehr. <UseMonoRuntime>true</UseMonoRuntime> war als Notausstieg dokumentiert, als CoreCLR in Preview 4 zum Standard wurde, und wurde damals ausdrücklich als vorübergehende Entsperrung während einer Regressionsmeldung eingeordnet. Preview 6 hat den separaten Mono-Pfad für Android, iOS und Mac Catalyst entfernt. Behandeln Sie die Eigenschaft als nicht mehr vorhanden und bauen Sie keinen Release-Plan darauf auf.

Ihr tatsächliches Rollback ist das Target Framework: Halten Sie den net10.0-android-Build in einem Branch grün, bis der .NET 11-Build ein echtes Produktions-Rollout überstanden hat. Das ist ein deutlich schwereres Rollback als das Umlegen einer Eigenschaft, und genau deshalb gibt es die Schritte 1 und 5.

Fallstricke, die echte Zeit kosten

Die Startregression ist real und ungleich verteilt. Zwei Issues dokumentieren den Fehlermodus: dotnet/android#10588 berichtet “an app that takes 1s to launch on mono can take 6s on coreclr”, mit ANRs in Avalonias ControlCatalog.Android, und dotnet/android#10914 meldet rund 1,0 s auf 6,0 s Kaltstart und ein APK-Wachstum von 21 MB auf 38 MB unter 11.0.100-preview.2. Beide betreffen Avalonia, nicht MAUI, und beide liegen vor der Arbeit an composite partial R2R und den MIBC-Profilen, die später im Preview-Zyklus landete. Lesen Sie sie also nicht als Ihr zu erwartendes Ergebnis, sondern als Begründung dafür, dass Schritt 1 verpflichtend ist.

XAML-lastige Startpfade sind die schmerzhaften. Der gemeinsame Nenner der Berichte sind Reflexion und XAML-Parsing während der Initialisierung, also genau die Arbeit, die partielles R2R nicht vorkompilieren kann, wenn das ausgelieferte .mibc-Profil die Form Ihrer App nicht abdeckt. Baut Ihre App vor dem ersten Frame einen großen visuellen Baum auf, schauen Sie dort zuerst hin.

UseInterpreter verliert stillschweigend seine Bedeutung. Unter Mono war die Eigenschaft im Debug standardmäßig true und ließ das Hot Reload der Mono-Ära funktionieren. Auf CoreCLR ist sie wirkungslos. Falls Sie sie aus einem Grund gesetzt hatten (ein dynamischer Codepfad, den Mono AOT nicht abbilden konnte), ist dieser Grund nicht verschwunden, sondern nur verschoben: CoreCLR unter Android führt im Debug einen echten JIT aus, der Code läuft also, aber testen Sie ihn bewusst erneut, statt es anzunehmen.

Der Inhalt Ihres APK ändert seine Form. Unter Mono lieferten Sie libmonosgen-2.0.so plus libaot-*.dll.so-Images aus. Unter CoreCLR liefern Sie libcoreclr.so, libclrjit.so, libmonodroid.so (der Android-Klebecode behält seinen Namen aus der Mono-Ära) und eine einzelne libassemblies.arm64-v8a.so mit komprimiertem MSIL samt R2R-Images. Wenn Sie Build-Skripte, Größenbudgets oder ProGuard/R8-Konfiguration haben, die diese Dateien benennen, müssen sie angepasst werden.

Die Größe steckt tatsächlich im Trimming. MAUI verwendet weiterhin TrimMode=partial als Standard, das Framework-Assemblys trimmt, Ihren Code und Ihre NuGet-Referenzen aber unangetastet lässt. Die meisten Größenbeschwerden werden zu Trimming-Beschwerden, sobald man die Aufschlüsselung pro Assembly betrachtet.

Verwandte Beiträge

Quellen

Comments

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

< Zurück