11 März 2025 (updated: 11 März 2025)
Chapters
Haben Sie jemals einen plötzlichen Freeze einer App bemerkt, die Sie entwickelt haben? Stellen Sie sich vor, es ist gerade passiert. Ist Ihr nächster Schritt, den App-Prozess mit Android Profiler zu profilieren? Wenn nicht, habe ich diesen Artikel speziell für Sie geschrieben!
Android Profiler ist eine Sammlung von Tools, die seit Android Studio 3.0 verfügbar sind und die vorherigen Android Monitor-Tools ersetzen. Die neue Suite ist viel fortschrittlicher bei der Diagnose von Leistungsproblemen von Apps. Sie kommt mit einer gemeinsamen Zeitachse und detaillierten CPU-, Speicher- und Netzwerk-Profilern. Durch geschickte Nutzung können wir viel Zeit sparen, die sonst mit Debugging oder dem Scrollen durch Protokolle im Logcat-Fenster verloren geht.
Um auf die Profiling-Tools zuzugreifen, klicken Sie auf View > Tool Windows > Android Profiler oder finden Sie ein entsprechendes Tool-Fenster in der Symbolleiste. Um Echtzeitdaten zu sehen, müssen Sie ein Gerät mit aktiviertem USB-Debugging anschließen oder den Android-Emulator verwenden und den App-Prozess auswählen. Ich empfehle Ihnen, das offizielle Android-Benutzerhandbuch zu lesen, um zu lernen, wie Sie alle in diesem Fenster angezeigten Daten inspizieren können.
Lernen Sie gerne anhand von Beispielen? Ich habe zwei Beispiele vorbereitet, um mit dem CPU Profiler zu üben. Dies sind kleine Apps, die auf einige Leistungsprobleme gestoßen sind. Lassen Sie uns versuchen, diese zu lösen!
Wir beginnen mit der Entwicklung einer einfachen Android-Anwendung, die eine Liste von aufeinanderfolgenden Daten anzeigt (die noch nicht passiert sind). Unter jedem Datum können wir die verbleibende Zeit in Tagen, Stunden, Minuten und Sekunden anzeigen.
Der Code beider Beispiele ist auf GitHub verfügbar, sodass Sie das Repository einfach klonen und das Projekt in Android Studio öffnen können. Überprüfen Sie vorerst die Revision mit dem Tag sample-1-before
.
Beginnen Sie mit der Definition eines Layouts, das aus einem RecyclerView
besteht, das innerhalb von SwipeRefreshLayout
platziert ist. Dies ermöglicht es, die Daten bei einer vertikalen Wischgeste zu aktualisieren.
Erstellen Sie als Nächstes eine Activity
, die unser Layout aufbläst, die Benutzerinteraktion verarbeitet und Operationen im Haupt-Thread ausführt, um aktualisierte Daten anzuzeigen:
In Zeile 9 verwenden wir den RecyclerView
-Adapter. Wir verwenden die Recycler-Bibliothek von android-commons (die in den meisten EL Passion Android-Projekten verwendet wird). Eine generische Funktion nimmt eine Liste von Elementen, einen Verweis auf die Layout-Ressource des Elements und einen Binder. Ziel ist es, den Code prägnant zu halten und den RecyclerView
-Adapter ohne Boilerplate-Code einzurichten.
Am Ende der onCreate
-Funktion setzen wir den Listener, um über Aktualisierungsaktionen, die durch SwipeRefreshLayout
ausgelöst werden, informiert zu werden. Die referenzierte refreshData
-Funktion ersetzt die Liste durch frische neue Elemente und benachrichtigt den Adapter über Änderungen der Daten.
In Zeile 25 generieren wir eine Liste von 1000 Elementen. Jedes Element setzt seine Eigenschaften in Bezug auf das aktuelle Datum und die Zeitverschiebung in Tagen. Die Zeitverschiebung reicht von 0 bis 999 und beeinflusst das Datum, das durch das Element angezeigt wird (siehe Zeile 29). Wir verwenden ThreeTenABP als unsere API für Daten und Zeiträume. Es ist ein unschätzbarer Backport des java.time.*
-Pakets, das von Jake Wharton für Android optimiert wurde.
In Zeile 36 führen wir einige Operationen durch, um die verbleibende Zeit als eine menschenlesbarere Dauer zu erhalten.
In Zeile 50 binden wir ein Element mit dem ViewHolder, um itemView
an einer bestimmten Position zu aktualisieren. Wir greifen auf Ressourcen zu, um den String mit dem Wert remainingTime
zu formatieren.
Das Element selbst hält die Werte formattedDate
und remainingTime
, die bereit sind, in den entsprechenden TextView
-Komponenten angezeigt zu werden. Lassen Sie uns das folgende Elementlayout verwenden:
Starten Sie die App und wischen Sie, um die Daten zu aktualisieren. Haben Sie ein Einfrieren bemerkt? Wahrscheinlich nicht. Das hängt stark von der CPU Ihres Geräts und anderen Prozessen ab, die CPU-Zeit verbrauchen. Starten Sie jetzt das Android Profiler Tool-Fenster und wählen Sie die entsprechende Zeitleiste aus, um den CPU Profiler zu öffnen. Verbinden Sie Ihr Gerät und wischen Sie erneut, um die Daten zu aktualisieren. Beachten Sie, dass Profiler-Threads zum Anwendungsprozess hinzugefügt werden und zusätzliche CPU-Zeit verbrauchen. Ich nehme an, dass Sie jetzt bereits Frame-Skips erlebt haben. Schauen Sie sich das Logcat an, da der Choreograf Sie bereits über eine hohe Verarbeitung warnen sollte:
I/Choreographer: Skipped 147 frames! Die Anwendung könnte zu viel Arbeit in ihrem Haupt-Thread verrichten.
Cool! Wir können mit unserer Inspektion beginnen. Schauen Sie sich die CPU Profiler-Zeitleiste an:
Über dem Diagramm gibt es eine Ansicht, die die Benutzerinteraktion mit der App darstellt. Alle Benutzereingaben werden hier als lila Kreise angezeigt. Sie können einen Kreis sehen, der den Wischvorgang darstellt, den wir durchgeführt haben, um die Daten zu aktualisieren. Etwas weiter unten finden Sie die derzeit angezeigte Sample1Activity
. Dieser Bereich wird als Ereigniszeitleiste bezeichnet.
Unter den Ereignissen befindet sich die CPU-Zeitleiste, die grafisch die CPU-Nutzung der App und anderer Prozesse im Verhältnis zur insgesamt verfügbaren CPU-Zeit anzeigt. Darüber hinaus können Sie die Anzahl der Threads beobachten, die Ihre App verwendet.
Unten sehen Sie die Thread-Aktivitätszeitleiste, die zum Anwendungsprozess gehört. Jeder Thread befindet sich in einem von drei Zuständen, die durch Farben angezeigt werden: aktiv (grün), wartend (gelb) oder schlafend (grau). Ganz oben in der Liste finden Sie den Haupt-Thread der App. Auf meinem Gerät (Nexus 5X) verwendet er ~35% der CPU-Zeit für etwa 5 Sekunden. Das ist viel! Wir können eine Methodenverfolgung aufzeichnen, um zu sehen, was dort passiert.
Klicken Sie auf die Aufnahmetaste 🔴, kurz bevor Sie wischen, um die Aktion zu aktualisieren, und stoppen Sie die Aufnahme ⏹, kurz nachdem die Datenaktualisierung abgeschlossen ist. Wenn Sie fertig sind, beachten Sie, dass der Methodenverfolgungsbereich gerade erschienen ist:
Wir beginnen unsere Analyse mit dem Call Chart, das im ersten Tab angezeigt wird. Die horizontale Achse stellt den Zeitverlauf dar. Auf der vertikalen Achse werden die Aufrufer und deren Aufgerufene (von oben nach unten) angezeigt. Methodenaufrufe werden auch farblich unterschieden, je nachdem, ob es sich um einen Aufruf an die System-API, eine Drittanbieter-API oder unsere Methode handelt. Beachten Sie, dass die Gesamtzeit für jeden Methodenaufruf die Summe der Selbstzeit der Methode und der Zeit ihrer Aufgerufenen ist. Aus diesem Diagramm können Sie ableiten, dass das Leistungsproblem irgendwo innerhalb der generateItems
-Methode liegt. Bewegen Sie die Maus über die Leiste, um weitere Details zur verstrichenen Zeit zu überprüfen. Sie können auch doppelt auf die Leiste klicken, um die Methodendeklaration im Code zu sehen. Es ist ziemlich schwierig, mehr aus diesem Tab abzuleiten, da es viel Zoomen und Scrollen erfordert, also wechseln wir zum nächsten Tab.
Das Flame Chart ist viel besser geeignet, um zu zeigen, welche Methoden unserer wertvollen CPU-Zeit in Anspruch genommen haben. Es aggregiert dieselben Aufrufstapel und kehrt das Diagramm aus dem vorherigen Tab um. Anstelle vieler kurzer horizontaler Balken wird ein einzelner längerer Balken angezeigt. Schauen Sie sich das jetzt an:
Zwei verdächtige Methoden gefunden. Würden Sie glauben, dass getRemainingTime
die gesamte Ausführungszeit der Methode über 2 Sekunden und LocalDateTime.format
über 1 Sekunde CPU-Zeit in Anspruch nehmen wird?
Beachten Sie, dass diese Zeit auch jeden Zeitraum umfasst, in dem der Thread nicht aktiv war. In der oberen rechten Ecke des Methodenverfolgungsbereichs können Sie die Zeitinformationen so umschalten, dass sie in der Thread-Zeit angezeigt werden. Wenn wir einen einzelnen Thread analysieren, könnte dies die bevorzugte Option sein, da sie den CPU-Zeitverbrauch zeigt, der nicht von anderen Threads beeinflusst wird.
Okay, lassen Sie uns weitermachen. Öffnen Sie jetzt den letzten Tab, um das Bottom Up-Diagramm zu sehen. Es zeigt eine Liste von Methodenaufrufen, die nach dem CPU-Zeitverbrauch absteigend sortiert sind. Dieses Diagramm gibt uns detaillierte Zeitinformationen (in Mikrosekunden). Durch das Erweitern der Methoden können Sie deren Aufrufer finden.
Holen Sie sich aus dem Diagramm Zeitinformationen über die Methoden, die wir beschuldigt haben, zu viel CPU-Zeit zu verbrauchen. Setzen Sie sie in Beziehung zu zwei Methoden aus ihrem Aufrufstapel:
Sie können sehen, dass getRemainingTime
und LocalDateTime.format
über 80% der aufgezeichneten Methodenverfolgung verbrauchen! Um dieses Einfrieren zu beheben, müssen wir an der Generierung der Elemente arbeiten. Das ist offensichtlich.
Was ist also zu tun? Sie haben wahrscheinlich bereits mehrere Lösungen gefunden. Wir führen eine rechenintensive Berechnung durch, um 1000 Elemente zu erstellen (keine kleine Zahl). Sie können darüber nachdenken, eine Paginierung zu implementieren, um die Daten schrittweise zu erstellen und anzuzeigen. Das ist eine großartige Idee, da sie skalierbar ist. Dieses Mal möchte ich jedoch einen anderen Weg einschlagen. Was wäre, wenn wir alle Formatierungen kurz vor der Anzeige der Daten im RecyclerView
an der angegebenen Position durchführen — wenn wir Item
mit RecyclerView.ViewHolder
binden? Dadurch würden wir die Methoden getRemainingTime
und LocalDateTime.format
nur für die wenigen aktuell angezeigten und bereit zu zeigenden Elemente aufrufen — nicht tausendmal wie zuvor. Um dies zu erreichen, müssen wir die Eigenschaften von Item
aktualisieren, um nur die notwendigen Daten zu halten, um die Formatierung später durchzuführen:
Das erfordert folgende Änderungen in den Funktionen generateItems
und bindItem
:
Lassen Sie uns sehen, dass wir die Funktion createItem
inline gesetzt haben, da jetzt alle Formatierungen innerhalb der Methode bindItem
stattfinden. Überprüfen Sie die Revision mit dem Tag sample-1-after
, um diese Änderungen zu erhalten.
Es ist Zeit, den CPU Profiler erneut zu starten und die Methodenverfolgung aufzuzeichnen, nachdem Änderungen in unserem Code vorgenommen wurden. Schauen Sie sich das Call Chart an, um zu überprüfen, ob unsere Optimierung gut verlaufen ist:
Wenn Sie die Maus über die Funktion generateItems
bewegen, werden Sie feststellen, dass sie jetzt ~0,3 Sekunden der Wand-Uhrzeit verbraucht. Das sind über 13 Mal weniger CPU-Zeit als vor der Optimierung! Bevor wir mit dem Feiern beginnen, wechseln wir zum Flame Chart, um sicherzustellen, dass unsere Änderungen keine negativen Auswirkungen auf die Gesamtdauer der Methode bindItem
haben. Glücklicherweise verbraucht sie bis zu 0,1 Sekunden.
Zusätzlich können Sie es scrollen, um sicherzustellen, dass unsere Optimierung die Gesamtleistung der App nicht beeinträchtigt. Versuchen Sie, während eines solchen Scrollens die Methodenverfolgung aufzuzeichnen. Beachten Sie, dass der Choreograf nicht mehr über das Überspringen von Frames klagt. Erfolg! Der Code ist optimiert und wir sind mit dem ersten Beispiel fertig!
Im nächsten Beispiel werden wir größtenteils den Code aus Beispiel 1 nach der Optimierung wiederverwenden. Die einzige Änderung, die wir im Aktivitätslayout vornehmen werden, ist das Hinzufügen eines ImageView
über dem RecyclerView
. Um den gesamten Inhalt scrollbar zu machen, legen Sie beide Ansichten in ein NestedScrollView
:
Um Konflikte im Scrollverhalten des RecyclerView
zu vermeiden, müssen wir das Attribut nestedScrollingEnabled
auf false
setzen. Überprüfen Sie die Revision mit dem Tag sample-2-before
, um dieses Beispiel schnell zu ziehen. Starten Sie die App und wischen Sie, um die Daten zu aktualisieren. Sie sollten ein Einfrieren bemerken, selbst ohne den Android Profiler angeschlossen.
Dieses Mal habe ich beschlossen, Ihnen die Diagnose selbst zu überlassen, um Ihren Spaß nicht zu verderben. Nach erfolgreicher Optimierung der App-Leistung sollten Sie kein Einfrieren wie im Beispiel 1 erleben. Es gibt nur eine Regel — der Bildschirm, der dem Benutzer angezeigt wird, darf sein Aussehen nicht ändern. Viel Glück!
Ich glaube, ich habe dich ermutigt, den Android Profiler häufiger zu nutzen. Ich denke, das ist eine gute Praxis, wenn uns eine reibungslose Benutzererfahrung am Herzen liegt. In diesem Artikel habe ich mich hauptsächlich auf den CPU Profiler konzentriert. Allerdings sind auch der Memory Profiler und der Network Profiler, die im Text nicht behandelt werden, einen Blick wert. Die Aufzeichnung der Speicherzuweisung hilft sehr dabei, Lecks zu finden, z.B. indem man dir vorwirft, dass Bitmaps nicht recycelt wurden. Wie auch immer, das Profiling der Netzwerkaktivität kann zu mehreren Optimierungen führen, die darauf abzielen, den Batterieverbrauch zu reduzieren.
11 März 2025 • Paweł Sierant