Wenn Sie mit einer instabilen Klasse konfrontiert sind, die Leistungsprobleme verursacht, sollten Sie optimieren, wie Compose sie vergleicht oder instanziiert. In diesem Dokument werden verschiedene Techniken beschrieben, die Sie dafür verwenden können.
Starkes Überspringen aktivieren
Der starke Überspringmodus ist ab Kotlin 2.0.20 standardmäßig aktiviert.
Im Modus „Starkes Überspringen“ können Composables mit instabilen Parametern übersprungen werden, indem instabile Parameter mithilfe der Instanzgleichheit (===) verglichen werden. Stabile Parameter werden weiterhin mithilfe der strukturellen Gleichheit (Object.equals()) verglichen.
Wenn Sie eine Kotlin-Version vor 2.0.20 verwenden, müssen Sie zuerst den Modus für starkes Überspringen aktivieren. Wenn der Modus für starkes Überspringen bereits aktiviert ist und eine zusammensetzbare Funktion immer noch unnötig neu zusammengesetzt wird, prüfen Sie, ob bei jedem Durchlauf der Neuzusammensetzung neue Instanzen instabiler Parameter zugewiesen werden.
Weitere Informationen finden Sie unter Starkes Überspringen.
Klasse unveränderlich machen
Sie können eine instabile Klasse auch vollständig unveränderlich machen, damit der Compiler ihre Stabilität automatisch ableiten kann.
- Unveränderlich: Gibt einen Typ an, bei dem sich der Wert von Attributen nach dem Erstellen einer Instanz dieses Typs nicht mehr ändern kann und alle Methoden referenziell transparent sind.
- Achten Sie darauf, dass alle Attribute der Klasse
valund nichtvarsind und unveränderliche Typen haben. - Primitive Typen wie
String,IntundFloatsind immer unveränderlich. - Flache
data class-Typen mit primitivenval- oderString-Eigenschaften in Modulen, die mit dem Compose-Compiler erstellt wurden, werden bereits als stabil abgeleitet und automatisch mit.equals()verglichen. Sie müssen sie nicht mit@Immutableannotieren. - Wenn Unveränderlichkeit nicht möglich ist, müssen Sie den Compose-Status für alle änderbaren Properties verwenden.
- Achten Sie darauf, dass alle Attribute der Klasse
- Stabil: Gibt einen Typ an, der veränderlich ist. Die Compose-Laufzeit erfährt nicht, ob und wann die öffentlichen Properties oder das Methodenverhalten des Typs andere Ergebnisse als bei einem vorherigen Aufruf liefern würden.
Sammlungen und Stabilität
Ein häufiger Grund, warum Compose eine Klasse als instabil einstuft, sind Sammlungen. Wie auf der Seite Stabilitätsprobleme diagnostizieren beschrieben, kann der Compose-Compiler nicht vollständig sicher sein, dass Sammlungsschnittstellen wie List, Map und Set wirklich unveränderlich sind, und markiert sie daher als instabil.
Sehen Sie sich diese Klasse aus dem Leitfaden Stabilitätsprobleme diagnostizieren an:
unstable class Snack {
…
unstable val tags: Set<String>
…
}
Wenn der Modus für striktes Überspringen aktiviert ist (Standardeinstellung in Kotlin 2.0.20 und höher), wird die Neuzusammensetzung für instabile Sammlungen und Klassen, die Sammlungen enthalten, bereits übersprungen. Dabei wird die Instanzgleichheit (===) verwendet, wenn dieselbe Referenz übergeben wird.
Seien Sie vorsichtig, bevor Sie kotlin.collections.List, Set oder Map durch unveränderliche Kotlinx-Sammlungen (ImmutableList, ImmutableSet, ImmutableMap) ersetzen oder @Immutable-, @Stable- oder stability_config.conf-Regeln zu Modellen hinzufügen, die Sammlungen enthalten. Im Strong Skipping Mode überspringt die Standard-List die Verwendung der Referenzgleichheit (===), während ImmutableList und annotierte Sammlungsmodelle bei jedem Neuzusammensetzungsdurchlauf elementweise .equals()-Vergleiche durchführen.
Bei großen Sammlungen oder Lazy Layouts (LazyColumn, LazyRow, LazyGrid) kann die Prüfung von .equals() für jedes Element länger dauern als die Neuzusammensetzung. Diese Kosten verdoppeln sich, wenn Sie ein ImmutableList-Attribut von einem data class an eine untergeordnete komponierbare Funktion übergeben:
data class SnackCategory(
val name: String,
val snacks: ImmutableList<Snack>
)
@Composable
fun CategorySection(category: SnackCategory) {
Text(category.name)
// Compose compares category.snacks twice: once in SnackCategory.equals()
// for CategorySection, and again for HighlightedSnacks.
HighlightedSnacks(snacks = category.snacks)
}
Anstatt Sammlungen eine strukturelle Gleichheit aufzuzwingen, sollten Sie die Instanzreferenz der Sammlung über Emissionen hinweg beibehalten (z. B. durch das Caching von Transformationen in Ihrem ViewModel oder mit remember), damit Compose die Instanzgleichheit (===) überspringen kann. Ziehen Sie ImmutableList oder Stabilitätsannotationen für Modelle, die Sammlungen enthalten, nur dann in Betracht, wenn Sie die Instanzreferenz der Sammlung nicht beibehalten können und Benchmarks bestätigen, dass strukturelle Gleichheitsprüfungen schneller sind als die Neuzusammensetzung.
Wenn Sie eine Kotlin-Version vor 2.0.20 verwenden, aktivieren Sie zuerst den Modus für starkes Überspringen.
Mit Stable oder Immutable annotieren
Eine mögliche Lösung für Stabilitätsprobleme bei kleinen Klassen mit kostengünstigen .equals()-Prüfungen besteht darin, instabile Klassen entweder mit @Stable oder @Immutable zu annotieren.
Durch das Annotieren einer Klasse wird das überschrieben, was der Compiler sonst über Ihre Klasse ableiten würde. Er ähnelt dem !!-Operator in Kotlin. Sie sollten sehr vorsichtig sein, wie Sie diese Annotationen verwenden. Das Überschreiben des Compilerverhaltens kann zu unvorhergesehenen Fehlern führen, z. B. dass Ihre zusammensetzbare Funktion nicht wie erwartet neu zusammengesetzt wird.
Wenn der Modus für starkes Überspringen aktiviert ist, beachten Sie die folgenden Richtlinien, wenn Sie entscheiden, ob Sie @Stable oder @Immutable hinzufügen möchten:
- Klassen, die bereits als stabil abgeleitet wurden, nicht annotieren: Klassen mit
val-Properties von stabilen Typen in Modulen, die mit dem Compose-Compiler erstellt wurden, werden bereits als stabil abgeleitet. - Klassen nur mit günstigen
.equals()-Prüfungen annotieren, wenn Instanzen neu erstellt werden: Fügen Sie@Stableoder@Immutablehinzu, wenn der Compiler die Stabilität nicht ableiten kann (z. B. Schnittstellen oder Klassen aus Nicht-Compose-Modulen) und eine Datenquelle bei jeder Ausgabe neue Objektinstanzen für unveränderte Elemente zuweist, sofern die Klasse klein ist und eine kostengünstige.equals()-Prüfung hat. - Vorsicht bei Modellen mit Sammlungen: Vermeiden Sie es,
@Stableoder@Immutablezu Datenmodellen hinzuzufügen, die große Sammlungen (List,Set,Map) enthalten, insbesondere bei Modellen, die in Lazy Layouts (LazyColumn,LazyRow,LazyGrid,Pager) verwendet werden. Wenn Sie diese Modelle nicht annotieren, kann Compose sie anhand der Instanzgleichheit (===) vergleichen, anstatt strukturelle.equals()-Prüfungen für jedes Element auszuführen.
Das folgende Snippet zeigt ein minimales Beispiel für eine flache Datenklasse (ohne Sammlungseigenschaften), die als unveränderlich annotiert ist, wenn sie in einem Modul definiert wird, in dem der Compiler die Stabilität nicht ableiten kann:
@Immutable
data class SnackSummary(
val id: Long,
val name: String,
val price: Long
)
Unabhängig davon, ob Sie die Annotation @Immutable oder @Stable verwenden, markiert der Compose-Compiler die Klasse SnackSummary als stabil und vergleicht sie mit Object.equals().
Klassen mit Annotationen in Sammlungen
Angenommen, Sie haben eine zusammensetzbare Funktion mit einem Parameter vom Typ List<Snack>:
restartable skippable scheme("[androidx.compose.ui.UiComposable]") fun HighlightedSnacks(
stable index: Int
unstable snacks: List<Snack>
stable onSnackClick: Function1<Long, Unit>
stable modifier: Modifier? = @static Companion
)
Auch wenn Snack stabil ist oder mit @Immutable annotiert wurde, markiert der Compose-Compiler den Parameter snacks in HighlightedSnacks weiterhin als unstable, da List eine Schnittstelle ist.
Wenn der Modus für starkes Überspringen aktiviert ist, wird HighlightedSnacks weiterhin als skippable markiert und die Neuzusammensetzung wird mithilfe der Instanzgleichheit (===) übersprungen, wenn dieselbe List-Instanz übergeben wird. Sie können den Parameter snacks als stable festlegen, indem Sie List<Snack> in eine Wrapper-Klasse @Immutable einbinden, List<Snack> durch ImmutableList<Snack> ersetzen oder kotlin.collections.* zu Ihrer Stabilitätskonfigurationsdatei hinzufügen.
Dies führt jedoch dazu, dass Compose bei jedem Rekompositionsdurchlauf einen elementweisen .equals()-Vergleich durchführt. Verwenden Sie diese Ansätze nur, wenn Sie die Instanzreferenz der Liste nicht beibehalten können und Benchmarks einen Vorteil zeigen.
Konfigurationsdatei für die Stabilität
Ab Compose Compiler 1.5.5 kann zur Kompilierzeit eine Konfigurationsdatei mit Klassen bereitgestellt werden, die als stabil gelten sollen. So können Klassen, die Sie nicht kontrollieren, z. B. Standardbibliotheksklassen wie LocalDateTime, als stabil betrachtet werden.
Die Konfigurationsdatei ist eine Nur-Text-Datei mit einer Klasse pro Zeile. Kommentare, einzelne und doppelte Platzhalter werden unterstützt.
Beispielkonfiguration:
// Consider LocalDateTime stable
java.time.LocalDateTime
// Consider my datalayer stable
com.datalayer.*
// Consider my datalayer and all submodules stable
com.datalayer.**
// Consider my generic type stable based off it's first type parameter only
com.example.GenericClass<*,_>
Wenn Sie diese Funktion aktivieren möchten, übergeben Sie den Pfad der Konfigurationsdatei an den Optionsblock composeCompiler der Konfiguration des Compose Compiler Gradle-Plug-ins.
composeCompiler {
stabilityConfigurationFile = rootProject.layout.projectDirectory.file("stability_config.conf")
}
Da der Compose-Compiler für jedes Modul in Ihrem Projekt separat ausgeführt wird, können Sie bei Bedarf unterschiedliche Konfigurationen für verschiedene Module bereitstellen. Alternativ können Sie eine Konfiguration auf der Stammebene Ihres Projekts erstellen und diesen Pfad an jedes Modul übergeben.
Mehrere Module
Ein weiteres häufiges Problem betrifft die Multi-Modul-Architektur. Der Compose-Compiler kann nur dann ableiten, ob eine Klasse stabil ist, wenn alle nicht primitiven Typen, auf die sie verweist, entweder explizit als stabil gekennzeichnet sind oder sich in einem Modul befinden, das ebenfalls mit dem Compose-Compiler erstellt wurde.
Wenn sich Ihre Datenschicht in einem separaten Modul von Ihrer UI-Schicht befindet, was der empfohlene Ansatz ist, kann dieses Problem auftreten. Im Modus für striktes Überspringen werden Klassen aus Nicht-Compose-Modulen als instabil behandelt. Die Neuzusammensetzung wird jedoch weiterhin übersprungen, wenn dieselbe Instanz übergeben wird, da die Instanzen gleich sind (===). Wenn Ihre Datenebene jedoch häufig neue Instanzen kleiner, flacher Modelle mit identischen Werten bei jeder Ausgabe zuweist, können Sie die strukturelle Gleichheit (Object.equals()) aktivieren.
Lösung
Sie haben folgende Möglichkeiten, dieses Problem zu beheben:
- Fügen Sie Ihrer Compiler-Konfigurationsdatei kleine, flache Klassen mit günstigen
.equals()-Prüfungen hinzu. - Aktivieren Sie den Compose-Compiler für Ihre Datenebenenmodule oder taggen Sie Ihre Klassen gegebenenfalls mit
@Stableoder@Immutable.- Dazu müssen Sie Ihrer Datenschicht eine Compose-Abhängigkeit hinzufügen. Es ist jedoch nur die Abhängigkeit für die Compose-Laufzeit und nicht für
Compose-UI.
- Dazu müssen Sie Ihrer Datenschicht eine Compose-Abhängigkeit hinzufügen. Es ist jedoch nur die Abhängigkeit für die Compose-Laufzeit und nicht für
- Verpacke in deinem UI-Modul die Klassen der Datenschicht in UI-spezifische Wrapper-Klassen.
Dasselbe Problem tritt auch bei der Verwendung externer Bibliotheken auf, wenn diese nicht den Compose-Compiler verwenden.
Nicht jede zusammensetzbare Funktion sollte überspringbar sein
Wenn du versuchst, Stabilitätsprobleme zu beheben, solltest du nicht versuchen, jedes Composable überspringbar zu machen oder für jeden Parameter strukturelle Gleichheit zu erzwingen. Das Überspringen einer zusammensetzbaren Funktion ist nicht kostenlos: Jeder Parameter einer überspringbaren zusammensetzbaren Funktion wird von der Compose-Laufzeit so gespeichert, als ob er gemerkt wurde. Compose muss jeden Parameter bei jeder Neuzusammensetzung der Funktion mit seinem vorherigen Wert vergleichen, um festzustellen, ob er übersprungen werden soll. Der Versuch, das Überspringen überall zu erzwingen, kann zu einer vorzeitigen Optimierung führen, die mehr Aufwand verursacht als einspart.
Es gibt viele Situationen, in denen das Erzwingen der strukturellen Stabilität keinen wirklichen Vorteil hat und die Leistung oder Wartbarkeit beeinträchtigen kann. Beispiel:
- Eine zusammensetzbare Funktion, die selten oder gar nicht neu zusammengesetzt wird.
- Ein Compose-Element, das selbst nur überspringbare Compose-Elemente aufruft.
- Eine komponierbare Funktion mit einer großen Anzahl von Parametern oder Sammlungsmodellen mit aufwendigen
.equals()-Implementierungen. In diesem Fall können die Kosten für die Prüfung, ob sich ein Parameter geändert hat, die Kosten einer einfachen Neuzusammensetzung übersteigen.
Konzentrieren Sie sich nicht nur auf die Anzahl der Neuzusammensetzungen. Konzentrieren Sie sich darauf, dass sich Composables schnell ausführen lassen, verschieben Sie schnell wechselnde Statuslesevorgänge in die Layout- oder Draw-Phase und überprüfen Sie Änderungen mit Benchmarks. Wenn ein Compose-Element übersprungen werden kann, entsteht ein geringer Mehraufwand, der sich möglicherweise nicht lohnt. Sie können Ihre zusammensetzbare Funktion sogar als nicht neu startbar oder nicht überspringbar kennzeichnen, wenn Sie feststellen, dass die Möglichkeit zum Neustart oder Überspringen mehr Aufwand verursacht, als es wert ist.