Langsames Rendering

Beim UI-Rendering wird ein Frame aus Ihrer App generiert und auf dem Bildschirm angezeigt. Damit die Interaktion eines Nutzers mit Ihrer App reibungslos verläuft, muss Ihre App Frames in unter 16 ms rendern, um 60 Bilder pro Sekunde (fps) zu erreichen. Weitere Informationen dazu, warum 60 fps bevorzugt werden, findest du unter Android-Leistungsmuster: Warum 60 fps?. Wenn Sie versuchen, 90 fps zu erreichen, fällt dieses Fenster auf 11 ms und bei 120 fps auf 8 ms.

Wenn Sie dieses Fenster um 1 ms überschreiten, bedeutet dies nicht, dass der Frame mit einer Verzögerung von 1 ms angezeigt wird, sondern Choreographer löscht den Frame vollständig. Wenn die Benutzeroberfläche Ihrer App langsam gerendert wird, ist das System gezwungen, Frames zu überspringen, und der Nutzer nimmt ein Ruckeln in der Anwendung wahr. Dies wird als Verzögerung bezeichnet. Auf dieser Seite erfahren Sie, wie Sie eine Verzögerung diagnostizieren und beheben.

Wenn Sie Spiele entwickeln, die das View-System nicht verwenden, umgehen Sie Choreographer. In diesem Fall hilft die Frame Pacing Library den OpenGL- und Vulkan-Spielen dabei, unter Android ein reibungsloses Rendering und eine korrekte Frame-Taktung zu erreichen.

Zur Verbesserung der App-Qualität prüft Android deine App automatisch auf Verzögerungen und zeigt die entsprechenden Informationen im Android Vitals-Dashboard an. Informationen dazu, wie die Daten erhoben werden, finden Sie unter Technische Qualität der App mit Android Vitals überwachen.

Verzögerung erkennen

Es kann schwierig sein, den Code in Ihrer App zu finden, der zu Verzögerungen führt. In diesem Abschnitt werden drei Methoden zum Identifizieren von Verzögerungen beschrieben:

Mit der visuellen Überprüfung können Sie alle Anwendungsfälle in Ihrer App in wenigen Minuten durchgehen. Sie liefert jedoch nicht so viele Details wie Systrace. Systrace bietet weitere Details. Wenn Sie jedoch Systrace für alle Anwendungsfälle in Ihrer Anwendung ausführen, können Sie mit einer Menge Daten überflutet werden, die sich nur schwer analysieren lassen. Sowohl die visuelle Überprüfung als auch Systrace erkennen eine Verzögerung auf Ihrem lokalen Gerät. Wenn sich die Verzögerung auf lokalen Geräten nicht reproduzieren lässt, können Sie eine benutzerdefinierte Leistungsüberwachung erstellen, um bestimmte Teile Ihrer App auf vor Ort ausgeführten Geräten zu messen.

Sichtprüfung

Mithilfe einer visuellen Überprüfung können Sie Anwendungsfälle identifizieren, die zu Verzögerungen führen. Wenn Sie eine visuelle Inspektion durchführen möchten, öffnen Sie die App, gehen Sie die verschiedenen Teile der App manuell durch und suchen Sie in der UI nach einer Verzögerung.

Hier sind einige Tipps zur Durchführung einer Sichtprüfung:

  • Führen Sie eine Release- oder mindestens eine nicht debugfähige Version Ihrer Anwendung aus. Die ART-Laufzeit deaktiviert mehrere wichtige Optimierungen zur Unterstützung von Debugging-Funktionen. Achten Sie daher darauf, dass Sie etwas Ähnliches sehen, was ein Nutzer sieht.
  • Aktivieren Sie das Rendering der Profil-GPU. Profil-GPU-Rendering zeigt Balken auf dem Bildschirm an, die visuell darstellen, wie viel Zeit zum Rendern der Frames eines UI-Fensters im Verhältnis zur Benchmark von 16 ms pro Frame benötigt wird. Jeder Balken hat farbige Komponenten, die einer Phase in der Rendering-Pipeline zugeordnet sind. So können Sie sehen, welcher Teil am längsten dauert. Wenn der Frame beispielsweise viel Zeit mit der Verarbeitung von Eingaben verbringt, sehen Sie sich den App-Code an, der die Nutzereingabe verarbeitet.
  • Prüfen Sie Komponenten, die häufige Verzögerungen verursachen, z. B. RecyclerView.
  • Starten Sie die App in einem Kaltstart.
  • Führen Sie Ihre App auf einem langsameren Gerät aus, um das Problem zu verstärken.

Wenn Sie Anwendungsfälle finden, die eine Verzögerung verursachen, haben Sie vielleicht eine Vorstellung davon, was die Ursache dafür in Ihrer Anwendung ist. Wenn Sie weitere Informationen benötigen, können Sie mit Systrace die Ursache genauer untersuchen.

Systrace

Obwohl Systrace ein Tool ist, das zeigt, was das gesamte Gerät tut, kann es nützlich sein, um eine Verzögerung in Ihrer Anwendung zu identifizieren. Systrace hat nur einen geringen Systemaufwand, sodass es während der Instrumentierung zu realistischen Verzögerungen kommen kann.

Zeichnen Sie mit Systrace einen Trace auf, während Sie den Anwendungsfall mit einer Verzögerung auf Ihrem Gerät ausführen. Eine Anleitung zur Verwendung von Systrace finden Sie unter System-Trace in der Befehlszeile erfassen. Systrace ist nach Prozessen und Threads aufgeteilt. Suchen Sie in Systrace nach dem Prozess Ihrer Anwendung. Er sieht in etwa wie Abbildung 1 aus.

Systrace-Beispiel
Abbildung 1: Beispiel für Systrace

Das Systrace-Beispiel in Abbildung 1 enthält die folgenden Informationen zur Identifizierung einer Verzögerung:

  1. Systrace zeigt an, wann die einzelnen Frames gezeichnet werden, und farbcodiert jeden Frame, um langsame Renderingzeiten hervorzuheben. So können Sie einzelne stockende Frames genauer finden als die Sichtprüfung. Weitere Informationen finden Sie unter UI-Frames und -Benachrichtigungen prüfen.
  2. Systrace erkennt Probleme in Ihrer App und zeigt Benachrichtigungen sowohl in einzelnen Frames als auch im Benachrichtigungsbereich an. Folgen Sie am besten der Anleitung in der Benachrichtigung.
  3. Teile des Android-Frameworks und der Android-Bibliotheken wie RecyclerView enthalten Tracemarkierungen. Die Systrace-Zeitachse zeigt also, wann diese Methoden im UI-Thread ausgeführt werden und wie lange ihre Ausführung dauert.

Nachdem Sie sich die Systrace-Ausgabe angesehen haben, gibt es möglicherweise Methoden in Ihrer Anwendung, die Ihrer Meinung nach für Verzögerungen sorgen. Wenn die Zeitachse beispielsweise zeigt, dass ein langsamer Frame dadurch verursacht wird, dass RecyclerView lange Zeit in Anspruch nimmt, können Sie dem entsprechenden Code benutzerdefinierte Trace-Ereignisse hinzufügen und Systrace noch einmal ausführen, um weitere Informationen zu erhalten. Im neuen Systrace zeigt die Zeitachse an, wann die Methoden Ihrer App aufgerufen werden und wie lange ihre Ausführung dauert.

Wenn Systrace keine Details dazu anzeigt, warum die Arbeit mit UI-Threads sehr lange dauert, verwenden Sie Android CPU Profiler, um entweder einen Trace für eine Stichprobe oder instrumentierte Methode aufzuzeichnen. Im Allgemeinen eignen sich Methoden-Traces nicht dazu, eine Verzögerung zu identifizieren, da sie aufgrund eines hohen Aufwands falsch positive Verzögerungen erzeugen und nicht sehen können, ob Threads ausgeführt oder blockiert werden. Methoden-Traces können Ihnen jedoch helfen, die Methoden in Ihrer Anwendung zu identifizieren, die am meisten Zeit benötigen. Nachdem Sie diese Methoden identifiziert haben, fügen Sie Trace-Markierungen hinzu und führen Sie Systrace noch einmal aus, um festzustellen, ob diese Methoden Verzögerungen verursachen.

Weitere Informationen finden Sie unter Systrace.

Benutzerdefinierte Leistungsüberwachung

Wenn Sie die Verzögerung auf einem lokalen Gerät nicht reproduzieren können, können Sie eine benutzerdefinierte Leistungsüberwachung in Ihre Anwendung einbinden, um die Ursache der Verzögerung auf Geräten vor Ort zu ermitteln.

Dazu erfassen Sie Frame-Renderingzeiten bestimmter Teile Ihrer App mit FrameMetricsAggregator und erfassen und analysieren die Daten mit Firebase Performance Monitoring.

Weitere Informationen finden Sie unter Erste Schritte mit Performance Monitoring für Android.

Eingefrorene Frames

Eingefrorene Frames sind UI-Frames, deren Rendering länger als 700 ms dauert. Dies ist ein Problem, da Ihre App während des Renderings des Frames fast eine ganze Sekunde lang nicht auf Nutzereingaben reagiert. Wir empfehlen, Apps so zu optimieren, dass ein Frame innerhalb von 16 ms gerendert wird, um eine reibungslose Benutzeroberfläche zu gewährleisten. Es ist jedoch normal, dass beim Start der App oder beim Wechsel zu einem anderen Bildschirm länger als 16 ms dauert, bis der erste Frame gezeichnet ist. Dies liegt daran, dass Ihre App die Ansichten erhöhen, den Bildschirm anordnen und die anfängliche Zeichnung komplett neu ausführen muss. Aus diesem Grund verfolgt Android eingefrorene Frames getrennt vom langsamen Rendering. Das Rendering von Frames in Ihrer App sollte niemals länger als 700 ms dauern.

Zur Verbesserung der App-Qualität prüft Android deine App automatisch auf eingefrorene Frames und zeigt die entsprechenden Informationen im Android Vitals-Dashboard an. Informationen dazu, wie die Daten erhoben werden, finden Sie unter Mit Android Vitals die technische Qualität von Apps beobachten.

Eingefrorene Frames sind eine extreme Form des langsamen Renderings. Daher ist die Verfahren zur Diagnose und Behebung des Problems gleich.

Tracking-Verzögerung

FrameTimeline in Perfetto kann beim Tracking langsamer oder eingefrorener Frames helfen.

Beziehung zwischen langsamen Frames, eingefrorenen Frames und ANRs

Langsame Frames, eingefrorene Frames und ANRs sind verschiedene Arten von Verzögerungen, die bei Ihrer Anwendung auftreten können. In der Tabelle unten sehen Sie den Unterschied.

Langsame Frames Eingefrorene Frames ANRs
Renderingzeit Zwischen 16 ms und 700 ms Zwischen 700 ms und 5 s Mehr als 5 s
Sichtbarer Wirkungsbereich für Nutzer
  • Scrollverhalten von RecyclerView abrupt
  • Auf Bildschirmen mit komplexen Animationen werden nicht richtig animiert.
  • Beim Start der App
  • Von einem Bildschirm zu einem anderen wechseln, z. B. zum Bildschirmübergang
  • Solange deine Aktivitäten im Vordergrund ausgeführt werden, hat deine App nicht innerhalb von fünf Sekunden auf ein Eingabeereignis oder BroadcastReceiver reagiert, z. B. auf Tastendruck oder auf das Tippen auf den Bildschirm.
  • Es wird keine Aktivität im Vordergrund ausgeführt. Die Ausführung von BroadcastReceiver wurde aber noch nicht innerhalb eines angemessenen Zeitraums abgeschlossen.

Langsame und eingefrorene Frames separat erfassen

Während des Starts der App oder beim Wechsel zu einem anderen Bildschirm ist es normal, dass der erste Frame länger als 16 ms gezeichnet wird, da die App die Ansichten erhöhen, den Bildschirm neu anordnen und die anfängliche Zeichnung von Grund auf neu durchführen muss.

Best Practices zum Priorisieren und Beheben von Verzögerungen

Beachten Sie die folgenden Best Practices, wenn Sie eine Verzögerung in Ihrer Anwendung beheben möchten:

  • Identifizieren und beheben Sie die am einfachsten reproduzierbaren Fälle von Verzögerungen.
  • ANR-Fehler priorisieren Während langsame oder eingefrorene Frames dazu führen können, dass eine App langsam wirkt, können ANR-Fehler dazu führen, dass die App nicht mehr reagiert.
  • Ein langsames Rendering ist schwer zu reproduzieren, Sie können jedoch damit beginnen, 700 ms eingefrorene Frames zu beenden. Dies ist häufig der Fall, wenn die App gestartet oder der Bildschirm gewechselt wird.

Verzögerung beheben

Um Verzögerungen zu beheben, prüfen Sie, welche Frames in 16 ms nicht fertiggestellt werden, und suchen Sie nach dem Fehler. Prüfen Sie, ob Record View#draw oder Layout in einigen Frames ungewöhnlich lange dauert. Weitere Informationen zu diesen und weiteren Problemen finden Sie unter Häufige Ursachen von Verzögerungen.

Führen Sie lang andauernde Aufgaben asynchron außerhalb des UI-Threads aus, um Verzögerungen zu vermeiden. Achten Sie immer darauf, in welchem Thread Ihr Code ausgeführt wird, und seien Sie vorsichtig, wenn Sie nicht einfache Aufgaben im Hauptthread posten.

Wenn Sie eine komplexe und wichtige primäre UI für Ihre App haben (z. B. die zentrale Scrollliste), sollten Sie Schreibinstrumentierungstests in Betracht ziehen, die langsame Renderingzeiten automatisch erkennen und die Tests häufig ausführen, um Regressionen zu vermeiden.

Häufige Ursachen für Verzögerungen

In den folgenden Abschnitten werden häufige Ursachen von Verzögerungen in Anwendungen, die das View-System verwenden, sowie Best Practices zu deren Behebung erläutert. Informationen zum Beheben von Leistungsproblemen mit Jetpack Compose finden Sie unter Jetpack Compose-Leistung.

Scrollbare Listen

ListView – und insbesondere RecyclerView – werden häufig für komplexe Scroll-Listen verwendet, die anfällig für Verzögerungen sind. Beide enthalten Systrace-Markierungen, sodass Sie mit Systrace feststellen können, ob sie zu einer Verzögerung in Ihrer Anwendung beitragen. Übergeben Sie das Befehlszeilenargument -a <your-package-name>, um Trace-Abschnitte in RecyclerView sowie alle von Ihnen hinzugefügten Trace-Markierungen zu erhalten. Folgen Sie gegebenenfalls den Anleitungen der in der Systrace-Ausgabe generierten Warnungen. In Systrace können Sie auf RecyclerView-Trace-Abschnitte klicken, um eine Erklärung der Arbeit von RecyclerView anzuzeigen.

RecyclerView: benachrichtigen DataSetChanged()

Wenn du siehst, dass jedes Element in deinem RecyclerView Rebound und somit neu gelegt und in einem Frame neu gezeichnet wird, solltest du nicht notifyDataSetChanged(), setAdapter(Adapter) oder swapAdapter(Adapter, boolean) für kleine Aktualisierungen aufrufen. Diese Methoden signalisieren, dass Änderungen am gesamten Listeninhalt vorgenommen wurden, und werden in Systrace als RV FullInvalid angezeigt. Verwenden Sie stattdessen SortedList oder DiffUtil, um minimale Aktualisierungen zu generieren, wenn Inhalte geändert oder hinzugefügt werden.

Angenommen, eine App erhält von einem Server eine neue Version einer Liste von Nachrichteninhalten. Wenn du diese Informationen an den Adapter sendest, kannst du notifyDataSetChanged() aufrufen, wie im folgenden Beispiel gezeigt:

Kotlin

fun onNewDataArrived(news: List<News>) {
    myAdapter.news = news
    myAdapter.notifyDataSetChanged()
}

Java

void onNewDataArrived(List<News> news) {
    myAdapter.setNews(news);
    myAdapter.notifyDataSetChanged();
}

Dies hat den Nachteil, dass RecyclerView nicht erkannt wird, wenn eine einfache Änderung vorliegt, z. B. ein einzelnes Element, das ganz oben hinzugefügt wird. Daher wird er angewiesen, den gesamten im Cache gespeicherten Elementstatus zu löschen und somit alles neu zu binden.

Wir empfehlen die Verwendung von DiffUtil. Damit werden minimale Aktualisierungen für Sie berechnet und verteilt:

Kotlin

fun onNewDataArrived(news: List<News>) {
    val oldNews = myAdapter.items
    val result = DiffUtil.calculateDiff(MyCallback(oldNews, news))
    myAdapter.news = news
    result.dispatchUpdatesTo(myAdapter)
}

Java

void onNewDataArrived(List<News> news) {
    List<News> oldNews = myAdapter.getItems();
    DiffResult result = DiffUtil.calculateDiff(new MyCallback(oldNews, news));
    myAdapter.setNews(news);
    result.dispatchUpdatesTo(myAdapter);
}

Wenn Sie DiffUtil darüber informieren möchten, wie Ihre Listen geprüft werden sollen, definieren Sie die MyCallback als Callback-Implementierung.

RecyclerView: Verschachtelte RecyclerViews

Es ist üblich, mehrere Instanzen von RecyclerView zu verschachteln, insbesondere mit einer vertikalen Liste von horizontal scrollbaren Listen. Ein Beispiel dafür sind die App-Raster auf der Hauptseite des Play Store. Das kann großartig sein, aber es gibt auch viele Ansichten.

Wenn sich viele innere Elemente erhöhen, wenn Sie zum ersten Mal auf der Seite nach unten scrollen, sollten Sie prüfen, ob Sie RecyclerView.RecycledViewPool zwischen den inneren (horizontalen) Instanzen von RecyclerView teilen. Standardmäßig hat jede RecyclerView einen eigenen Pool von Elementen. Wenn jedoch gleichzeitig ein Dutzend itemViews auf dem Bildschirm angezeigt werden, ist es problematisch, wenn itemViews nicht von den verschiedenen horizontalen Listen geteilt werden kann, wenn in allen Zeilen ähnliche Ansichten angezeigt werden.

Kotlin

class OuterAdapter : RecyclerView.Adapter<OuterAdapter.ViewHolder>() {

    ...

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        // Inflate inner item, find innerRecyclerView by ID.
        val innerLLM = LinearLayoutManager(parent.context, LinearLayoutManager.HORIZONTAL, false)
        innerRv.apply {
            layoutManager = innerLLM
            recycledViewPool = sharedPool
        }
        return OuterAdapter.ViewHolder(innerRv)
    }
    ...

Java

class OuterAdapter extends RecyclerView.Adapter<OuterAdapter.ViewHolder> {
    RecyclerView.RecycledViewPool sharedPool = new RecyclerView.RecycledViewPool();

    ...

    @Override
    public void onCreateViewHolder(ViewGroup parent, int viewType) {
        // Inflate inner item, find innerRecyclerView by ID.
        LinearLayoutManager innerLLM = new LinearLayoutManager(parent.getContext(),
                LinearLayoutManager.HORIZONTAL);
        innerRv.setLayoutManager(innerLLM);
        innerRv.setRecycledViewPool(sharedPool);
        return new OuterAdapter.ViewHolder(innerRv);

    }
    ...

Wenn Sie weitere Optimierungen vornehmen möchten, können Sie auch setInitialPrefetchItemCount(int) für den LinearLayoutManager des inneren RecyclerView aufrufen. Wenn beispielsweise immer 3,5 Elemente in einer Zeile sichtbar sind, rufen Sie innerLLM.setInitialItemPrefetchCount(4) auf. Dadurch wird RecyclerView signalisiert, dass im Falle einer horizontalen Zeile versuchen muss, die darin enthaltenen Elemente vorab abzurufen, wenn im UI-Thread Zeit übrig ist.

RecyclerView: Zu viel Inflation oder Erstellung dauert zu lange

In den meisten Fällen kann das Prefetch-Feature in RecyclerView dazu beitragen, die Kosten der Inflation zu umgehen, indem es die Arbeit im Voraus erledigt, während der UI-Thread inaktiv ist. Wenn die Inflation während eines Frames und nicht in einem Abschnitt mit der Bezeichnung RV Prefetch (RV-Prefetch) angezeigt wird, solltest du darauf achten, dass du auf einem unterstützten Gerät testest und eine aktuelle Version der Support Library verwendest. Prefetch wird nur unter Android 5.0 API-Level 21 und höher unterstützt.

Wenn häufig eine Inflation auftritt, die zu Verzögerungen führt, wenn neue Elemente auf dem Bildschirm erscheinen, prüfen Sie, ob Sie nicht mehr Aufrufe als nötig haben. Je weniger die Ansichtstypen im Inhalt von RecyclerView sind, desto weniger Inflation ist erforderlich, wenn neue Elementtypen auf dem Bildschirm erscheinen. Führen Sie nach Möglichkeit Ansichtstypen zusammen, sofern dies sinnvoll ist. Wenn sich nur ein Symbol, eine Farbe oder nur ein Textelement zwischen Typen ändert, können Sie diese Änderung zum Bindezeitpunkt vornehmen und die Inflation vermeiden, wodurch gleichzeitig der Arbeitsspeicherbedarf der Anwendung reduziert wird.

Wenn die Aufruftypen gut aussehen, kannst du versuchen, die Kosten deiner Inflation zu senken. Es kann hilfreich sein, unnötige Container- und Strukturansichten zu reduzieren. Sie können itemViews mit ConstraintLayout erstellen, um Strukturansichten reduzieren zu können.

Wenn Sie die Leistung weiter optimieren möchten, die Elementhierarchien einfach sind und Sie keine komplexen Themen- und Stilelemente benötigen, sollten Sie die Konstruktoren selbst aufrufen. Häufig lohnt es sich jedoch nicht, auf die Einfachheit und die Funktionen von XML zu verzichten.

RecyclerView: Bindung dauert zu lange

Bindung – d. h. onBindViewHolder(VH, int) – muss einfach sein und für alle Elemente außer den komplexesten weniger als eine Millisekunde dauern. Sie muss POJO-Elemente (Einfache alte Java-Objekte) aus den internen Artikeldaten Ihres Adapters und Aufrufsetter für Ansichten im ViewHolder verwenden. Wenn die Ausführung von RV OnBindView sehr lange dauert, prüfen Sie, ob Sie mit Ihrem Bindungscode nur minimal arbeiten.

Wenn Sie einfache POJO-Objekte zum Speichern von Daten in Ihrem Adapter verwenden, können Sie mithilfe der Data Binding Library vollständig vermeiden, den Bindungscode in onBindViewHolder zu schreiben.

RecyclerView oder ListView: Layout oder Zeichen dauern zu lange

Informationen zu Problemen mit der Darstellung und dem Layout finden Sie in den Abschnitten Layoutleistung und Renderingleistung.

Listenansicht: Inflation

Sie können das Recycling in ListView auch versehentlich deaktivieren, wenn Sie nicht vorsichtig sind. Wenn jedes Mal, wenn ein Element auf dem Bildschirm zu sehen ist, eine Inflation angezeigt wird, prüfen Sie, ob Ihre Implementierung von Adapter.getView() den Parameter convertView täuscht, neu bindet und zurückgibt. Wenn sich die getView()-Implementierung immer erhöht, profitiert Ihre App nicht von den Vorteilen des Recyclings in ListView. Die Struktur von getView() muss fast immer der folgenden Implementierung ähneln:

Kotlin

fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
    return (convertView ?: layoutInflater.inflate(R.layout.my_layout, parent, false)).apply {
        // Bind content from position to convertView.
    }
}

Java

View getView(int position, View convertView, ViewGroup parent) {

    if (convertView == null) {
        // Only inflate if no convertView passed.
        convertView = layoutInflater.inflate(R.layout.my_layout, parent, false)
    }
    // Bind content from position to convertView.
    return convertView;
}

Layoutleistung

Wenn Systrace anzeigt, dass das Layout-Segment von Choreographer#doFrame zu oft oder zu oft funktioniert, bedeutet dies, dass Probleme mit der Layoutleistung auftreten. Die Layoutleistung Ihrer Anwendung hängt davon ab, welcher Teil der Ansichtshierarchie sich ändernde Layoutparameter oder Eingaben enthält.

Layoutleistung: Kosten

Wenn die Segmente länger als ein paar Millisekunden sind, kann es sein, dass die Verschachtelungsleistung für RelativeLayouts oder weighted-LinearLayouts vom schlechtesten Fall betroffen ist. Jedes dieser Layouts kann mehrere Messungs- und Layoutdurchläufe seiner untergeordneten Elemente auslösen. Eine Verschachtelung dieser Layouts kann also in Bezug auf die Verschachtelungstiefe zu einem O(n^2)-Verhalten führen.

Vermeiden Sie RelativeLayout oder das Gewichtungsfeature von LinearLayout in allen bis auf die untersten Blattknoten der Hierarchie. Sie haben folgende Möglichkeiten:

  • Strukturelle Ansichten neu anordnen.
  • Definieren Sie die benutzerdefinierte Layoutlogik. Ein konkretes Beispiel finden Sie unter Layouthierarchien optimieren. Sie können versuchen, eine Konvertierung in ConstraintLayout durchzuführen. Diese bietet ähnliche Funktionen ohne Leistungseinbußen.

Layoutleistung: Häufigkeit

Das Layout wird erwartet, wenn neue Inhalte auf dem Bildschirm erscheinen, z. B. wenn in RecyclerView ein neues Element in den sichtbaren Bereich bewegt wird. Wenn in jedem Frame ein wichtiges Layout auftritt, ist es möglich, dass Sie ein Layout animieren, was wahrscheinlich dazu führt, dass Frames ausgelassen werden.

Im Allgemeinen müssen Animationen mit Zeicheneigenschaften von View ausgeführt werden, z. B.:

All das lässt sich viel kostengünstiger ändern als Layouteigenschaften wie Abstände oder Ränder. Im Allgemeinen ist es auch viel günstiger, die Zeicheneigenschaften einer Ansicht zu ändern. Dazu rufen Sie einen Setter auf, der ein invalidate() auslöst, gefolgt von draw(Canvas) im nächsten Frame. Dadurch werden Zeichenvorgänge für die Ansicht neu aufgezeichnet, die entwertet wurde und im Allgemeinen auch viel günstiger als Layout ist.

Rendering-Leistung

Die Benutzeroberfläche von Android funktioniert in zwei Phasen:

  • Aufzeichnen von View#draw im UI-Thread, der draw(Canvas) für jede ungültige Ansicht ausführt und Aufrufe in benutzerdefinierte Ansichten oder in Ihren Code aufrufen kann.
  • DrawFrame im RenderThread, das auf dem nativen RenderThread ausgeführt wird, aber auf der Arbeit basiert, die in der Phase Aufzeichnen von View#draw generiert wurde.

Rendering-Leistung: UI-Thread

Wenn das Aufzeichnen von View#draw sehr lange dauert, wird eine Bitmap häufig im UI-Thread dargestellt. Beim Malen auf eine Bitmap wird CPU-Rendering verwendet. Daher sollte dies möglichst vermieden werden. Sie können Methoden-Tracing mit dem Android CPU Profiler verwenden, um festzustellen, ob dies das Problem ist.

Das Übermalen einer Bitmap erfolgt oft, wenn eine App eine Bitmap dekorieren möchte, bevor sie angezeigt wird. Manchmal kommt auch eine Verzierung wie das Hinzufügen abgerundeter Ecken zum Einsatz:

Kotlin

val paint = Paint().apply {
    isAntiAlias = true
}
Canvas(roundedOutputBitmap).apply {
    // Draw a round rect to define the shape:
    drawRoundRect(
            0f,
            0f,
            roundedOutputBitmap.width.toFloat(),
            roundedOutputBitmap.height.toFloat(),
            20f,
            20f,
            paint
    )
    paint.xfermode = PorterDuffXfermode(PorterDuff.Mode.MULTIPLY)
    // Multiply content on top to make it rounded.
    drawBitmap(sourceBitmap, 0f, 0f, paint)
    setBitmap(null)
    // Now roundedOutputBitmap has sourceBitmap inside, but as a circle.
}

Java

Canvas bitmapCanvas = new Canvas(roundedOutputBitmap);
Paint paint = new Paint();
paint.setAntiAlias(true);
// Draw a round rect to define the shape:
bitmapCanvas.drawRoundRect(0, 0,
        roundedOutputBitmap.getWidth(), roundedOutputBitmap.getHeight(), 20, 20, paint);
paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.MULTIPLY));
// Multiply content on top to make it rounded.
bitmapCanvas.drawBitmap(sourceBitmap, 0, 0, paint);
bitmapCanvas.setBitmap(null);
// Now roundedOutputBitmap has sourceBitmap inside, but as a circle.

Wenn Sie diese Arbeit im UI-Thread ausführen, können Sie dies stattdessen im Decodierungsthread im Hintergrund tun. In einigen Fällen, wie im vorherigen Beispiel, können Sie die Arbeit sogar während der Zeichenzeit erledigen. Wenn Ihr Drawable- oder View-Code also in etwa so aussieht:

Kotlin

fun setBitmap(bitmap: Bitmap) {
    mBitmap = bitmap
    invalidate()
}

override fun onDraw(canvas: Canvas) {
    canvas.drawBitmap(mBitmap, null, paint)
}

Java

void setBitmap(Bitmap bitmap) {
    mBitmap = bitmap;
    invalidate();
}

void onDraw(Canvas canvas) {
    canvas.drawBitmap(mBitmap, null, paint);
}

Sie können es durch Folgendes ersetzen:

Kotlin

fun setBitmap(bitmap: Bitmap) {
    shaderPaint.shader = BitmapShader(bitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP)
    invalidate()
}

override fun onDraw(canvas: Canvas) {
    canvas.drawRoundRect(0f, 0f, width, height, 20f, 20f, shaderPaint)
}

Java

void setBitmap(Bitmap bitmap) {
    shaderPaint.setShader(
            new BitmapShader(bitmap, TileMode.CLAMP, TileMode.CLAMP));
    invalidate();
}

void onDraw(Canvas canvas) {
    canvas.drawRoundRect(0, 0, width, height, 20, 20, shaderPaint);
}

Sie können dies auch für den Hintergrundschutz tun, z. B. wenn Sie einen Farbverlauf auf der Bitmap darstellen oder die Bilder mit ColorMatrixColorFilter filtern. Zwei weitere gängige Vorgänge zum Ändern von Bitmaps.

Wenn Sie aus einem anderen Grund eine Bitmap zeichnen und diese möglicherweise als Cache verwenden, versuchen Sie, auf das hardwarebeschleunigte Canvas zu zeichnen, das direkt an Ihre View oder Drawable übergeben wird. Bei Bedarf können Sie auch setLayerType() mit LAYER_TYPE_HARDWARE aufrufen, um komplexe Rendering-Ausgaben im Cache zu speichern und trotzdem das GPU-Rendering zu nutzen.

Rendering-Leistung: RenderThread

Einige Canvas-Vorgänge lassen sich kostengünstig aufzeichnen, lösen aber eine teure Berechnung im RenderThread aus. Systrace weist diese in der Regel mit Benachrichtigungen an.

Große Pfade animieren

Wenn Canvas.drawPath() auf dem hardwarebeschleunigten Canvas aufgerufen wird, das an View übergeben wird, nutzt Android diese Pfade zuerst auf der CPU und lädt sie in die GPU hoch. Große Pfade sollten nicht von Frame zu Frame bearbeitet werden, damit sie effizient im Cache gespeichert und gezeichnet werden können. drawPoints(), drawLines() und drawRect/Circle/Oval/RoundRect() sind effizienter und besser geeignet, selbst wenn Sie mehr Zeichenaufrufe verwenden.

Canvas.clipPath

clipPath(Path) löst teures Clipping-Verhalten aus und muss im Allgemeinen vermieden werden. Wenn möglich, sollten Sie Formen zeichnen, anstatt sie auf Nicht-Rechtecke zuzuschneiden. Es bietet eine bessere Leistung und unterstützt Anti-Aliasing. Der folgende clipPath-Aufruf kann beispielsweise anders ausgedrückt werden:

Kotlin

canvas.apply {
    save()
    clipPath(circlePath)
    drawBitmap(bitmap, 0f, 0f, paint)
    restore()
}

Java

canvas.save();
canvas.clipPath(circlePath);
canvas.drawBitmap(bitmap, 0f, 0f, paint);
canvas.restore();

Formulieren Sie stattdessen das vorherige Beispiel so:

Kotlin

paint.shader = BitmapShader(bitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP)
// At draw time:
canvas.drawPath(circlePath, mPaint)

Java

// One time init:
paint.setShader(new BitmapShader(bitmap, TileMode.CLAMP, TileMode.CLAMP));
// At draw time:
canvas.drawPath(circlePath, mPaint);
Bitmap-Uploads

Android zeigt Bitmaps als OpenGL-Texturen an. Wenn eine Bitmap zum ersten Mal in einem Frame angezeigt wird, wird sie in die GPU hochgeladen. Sie können dies in Systrace als Texture upload(id) width x height sehen. Dies kann, wie in Abbildung 2 gezeigt, mehrere Millisekunden dauern, aber es ist erforderlich, das Bild mit der GPU anzuzeigen.

Wenn dies sehr lange dauert, prüfen Sie zuerst die Werte für Breite und Höhe im Trace. Achten Sie darauf, dass die angezeigte Bitmap nicht wesentlich größer als der Bildschirmbereich ist, in dem sie zu sehen ist. Ist dies der Fall, werden dadurch Uploadzeit und Arbeitsspeicher verschwendet. Im Allgemeinen bieten Bitmap-Ladebibliotheken eine Möglichkeit, eine Bitmap der passenden Größe anzufordern.

Unter Android 7.0 kann der Bitmap-Ladecode – in der Regel von Bibliotheken durchgeführt – prepareToDraw() aufrufen, um einen frühen Upload auszulösen, bevor er benötigt wird. Auf diese Weise erfolgt der Upload frühzeitig, während RenderThread inaktiv ist. Das ist nach der Decodierung oder beim Binden einer Bitmap an eine Ansicht möglich, solange Sie die Bitmap kennen. Im Idealfall übernimmt die Bibliothek zum Laden der Bitmap das für Sie. Wenn Sie Ihre eigene Bibliothek verwalten oder dafür sorgen möchten, dass keine Uploads auf neueren Geräten erfolgen, können Sie prepareToDraw() in Ihrem eigenen Code aufrufen.

Eine App benötigt viel Zeit in einem Frame und lädt eine große Bitmap hoch.
Abbildung 2: Eine App benötigt viel Zeit in einem Frame, um eine große Bitmap hochzuladen. Reduzieren Sie entweder seine Größe oder lösen Sie es frühzeitig aus, wenn Sie es mit prepareToDraw() decodieren.

Verzögerungen bei der Threadplanung

Der Thread-Planer ist der Teil des Android-Betriebssystems, der darüber entscheidet, welche Threads im System wann und wie lange ausgeführt werden müssen.

Manchmal tritt eine Verzögerung auf, weil der UI-Thread Ihrer Anwendung blockiert ist oder nicht ausgeführt wird. Systrace verwendet verschiedene Farben, wie in Abbildung 3 gezeigt, um anzuzeigen, wenn ein Thread im Ruhezustand (grau), ausführbar (blau: er ausgeführt werden kann, aber vom Planer noch nicht ausgewählt wird), aktiv (grün) oder sich im unterbrechungsfreien Schlaf (rot oder orange) befindet. Dies ist sehr hilfreich beim Beheben von Verzögerungen bei der Thread-Planung, die durch Verzögerungen bei der Thread-Planung verursacht werden.

Markiert einen Zeitraum, in dem der UI-Thread im Ruhezustand ist
Abbildung 3. Markierung eines Zeitraums, in dem der UI-Thread im Ruhezustand ist.

Häufig verursachen Binder-Aufrufe – der IPC-Mechanismus (Inter-Process Communication) unter Android – lange Pausen bei der Ausführung Ihrer App. In neueren Android-Versionen ist dies einer der häufigsten Gründe dafür, dass der UI-Thread nicht mehr ausgeführt wird. Im Allgemeinen besteht die Lösung darin, das Aufrufen von Funktionen zu vermeiden, die Binderaufrufe ausführen. Falls dies unvermeidbar ist, speichern Sie den Wert im Cache oder verschieben Sie die Arbeit in Hintergrundthreads. Wenn Codebasen größer werden, können Sie versehentlich einen Binder-Aufruf hinzufügen, indem Sie eine Low-Level-Methode aufrufen, wenn Sie nicht aufpassen. Sie können sie jedoch mit Tracing finden und beheben.

Wenn Sie Binder-Transaktionen haben, können Sie deren Aufrufstacks mit den folgenden adb-Befehlen erfassen:

$ adb shell am trace-ipc start
… use the app - scroll/animate ...
$ adb shell am trace-ipc stop --dump-file /data/local/tmp/ipc-trace.txt
$ adb pull /data/local/tmp/ipc-trace.txt

Manchmal können harmlose Aufrufe wie getRefreshRate() Binder-Transaktionen auslösen und große Probleme verursachen, wenn sie häufig aufgerufen werden. Durch regelmäßiges Tracing können Sie diese Probleme leichter finden und beheben.

Zeigt den UI-Thread an, der aufgrund von Binder-Transaktionen in einem Wohnmobil-Feed im Ruhezustand ist. Halten Sie Ihre Bindungslogik fokussiert und verwenden Sie Trace-ipc, um Binderaufrufe zu ermitteln und zu entfernen.
Abbildung 4: Der UI-Thread ist aufgrund von Binder-Transaktionen in einem Wohnmobil-Feed im Ruhezustand. Halten Sie Ihre Bindungslogik einfach und verwenden Sie trace-ipc, um Binderaufrufe zu ermitteln und zu entfernen.

Wenn keine Binder-Aktivität angezeigt wird, Ihr UI-Thread aber trotzdem nicht ausgeführt wird, prüfen Sie, ob Sie auf eine Sperre oder einen anderen Vorgang von einem anderen Thread warten. Normalerweise muss der UI-Thread nicht auf Ergebnisse von anderen Threads warten. In anderen Threads müssen Informationen gepostet werden.

Objektzuweisung und automatische Speicherbereinigung

Die Objektzuweisung und die automatische Speicherbereinigung sind seit der Einführung von ART als Standardlaufzeit in Android 5.0 wesentlich weniger ein Problem. Trotzdem können Sie Ihre Threads mit diesem zusätzlichen Aufwand abschwächen. Es ist in Ordnung, eine Zuweisung als Reaktion auf ein seltenes Ereignis vorzunehmen, das nicht viele Male pro Sekunde eintritt, z. B. wenn ein Nutzer auf eine Schaltfläche tippt. Denken Sie jedoch daran, dass jede Zuweisung Kosten verursacht. Wenn er sich in einer engen Schleife befindet, die häufig aufgerufen wird, sollten Sie die Zuweisung vermeiden, um die Belastung der Speicherbereinigung zu verringern.

Mit Systrace erfahren Sie, ob die automatische Speicherbereinigung häufig ausgeführt wird, und mit Android Memory Profiler können Sie feststellen, woher die Zuweisungen stammen. Wenn Sie Zuweisungen nach Möglichkeit vermeiden, insbesondere in Schleifen, treten weniger Probleme auf.

Zeigt eine 94-ms-GC im HeapTaskDaemon an
Abbildung 5: Eine Speicherbereinigung mit 94 ms im HeapTaskDaemon-Thread.

Unter neueren Android-Versionen wird GC in der Regel auf einem Hintergrundthread namens HeapTaskDaemon ausgeführt. Erhebliche Zuweisungsmengen können bedeuten, dass mehr CPU-Ressourcen für die automatische Speicherbereinigung ausgegeben werden, wie in Abbildung 5 dargestellt.