Tipps und Tools zur Optimierung von Android-Apps

Veröffentlicht: 2022-03-11

Android-Geräte haben viele Kerne, also ist das Schreiben reibungsloser Apps für jeden eine einfache Aufgabe, oder? Falsch. Da alles auf Android auf viele verschiedene Arten erledigt werden kann, kann es schwierig sein, die beste Option auszuwählen. Wer die effizienteste Methode wählen will, muss wissen, was unter der Haube passiert. Glücklicherweise müssen Sie sich nicht auf Ihr Gefühl oder Ihren Geruchssinn verlassen, da es viele Tools gibt, die Ihnen helfen können, Engpässe zu finden, indem sie messen und beschreiben, was vor sich geht. Richtig optimierte und reibungslose Apps verbessern die Benutzererfahrung erheblich und entladen auch weniger Batterie.

Sehen wir uns zuerst einige Zahlen an, um zu überlegen, wie wichtig die Optimierung wirklich ist. Laut einem Nimbledroid-Beitrag haben 86 % der Benutzer (mich eingeschlossen) Apps deinstalliert, nachdem sie sie nur einmal verwendet hatten, weil sie schlecht funktionierten. Wenn Sie Inhalte laden, haben Sie weniger als 11 Sekunden Zeit, um sie dem Benutzer anzuzeigen. Nur jeder dritte Nutzer schenkt Ihnen mehr Zeit. Möglicherweise erhalten Sie deshalb auch viele schlechte Bewertungen bei Google Play.

Bessere Apps erstellen: Android-Leistungsmuster

Das Testen der Geduld Ihrer Benutzer ist eine Abkürzung zur Deinstallation.
Twittern

Das erste, was jedem Benutzer immer wieder auffällt, ist die Startzeit der App. Laut einem anderen Nimbledroid-Beitrag starten von den 100 Top-Apps 40 in weniger als 2 Sekunden und 70 in weniger als 3 Sekunden. Wenn möglich, sollten Sie daher einige Inhalte generell so schnell wie möglich anzeigen und die Hintergrundprüfungen und Aktualisierungen etwas hinauszögern.

Denken Sie immer daran, dass vorzeitige Optimierung die Wurzel allen Übels ist. Auch mit der Mikrooptimierung sollten Sie nicht zu viel Zeit verschwenden. Sie werden den größten Vorteil der Optimierung von Code sehen, der häufig ausgeführt wird. Dazu gehört zum Beispiel die Funktion onDraw() , die jeden Frame ausführt, idealerweise 60 Mal pro Sekunde. Das Zeichnen ist die langsamste Operation auf dem Markt, also versuchen Sie nur, das neu zu zeichnen, was Sie müssen. Mehr dazu kommt später.

Leistungstipps

Genug Theorie, hier ist eine Liste mit einigen Dingen, die Sie beachten sollten, wenn Leistung für Sie wichtig ist.

1. String vs. StringBuilder

Angenommen, Sie haben einen String und möchten aus irgendeinem Grund 10.000 Mal weitere Strings anhängen. Der Code könnte etwa so aussehen.

 String string = "hello"; for (int i = 0; i < 10000; i++) { string += " world"; }

Sie können auf den Android Studio-Monitoren sehen, wie ineffizient manche String-Verkettung sein kann. Es gibt Tonnen von Garbage Collections (GC).

String vs. StringBuilder

Dieser Vorgang dauert auf meinem recht guten Gerät mit Android 5.1.1 etwa 8 Sekunden. Der effizientere Weg, dasselbe Ziel zu erreichen, ist die Verwendung eines StringBuilder wie diesem.

 StringBuilder sb = new StringBuilder("hello"); for (int i = 0; i < 10000; i++) { sb.append(" world"); } String string = sb.toString();

Auf demselben Gerät geschieht dies fast sofort, in weniger als 5 ms. Die CPU- und Speicher-Visualisierungen sind fast völlig flach, sodass Sie sich vorstellen können, wie groß diese Verbesserung ist. Beachten Sie jedoch, dass wir, um diesen Unterschied zu erreichen, 10.000 Zeichenfolgen anhängen mussten, was Sie wahrscheinlich nicht oft tun. Wenn Sie also nur einmal ein paar Strings hinzufügen, werden Sie keine Verbesserung feststellen. Übrigens, falls doch:

 String string = "hello" + " world";

Es wird intern in einen StringBuilder konvertiert, sodass es problemlos funktioniert.

Sie fragen sich vielleicht, warum das Verketten von Strings auf die erste Art so langsam ist? Dies wird durch die Tatsache verursacht, dass Strings unveränderlich sind, sodass sie nach ihrer Erstellung nicht mehr geändert werden können. Selbst wenn Sie denken, dass Sie den Wert eines Strings ändern, erstellen Sie tatsächlich einen neuen String mit dem neuen Wert. In einem Beispiel wie:

 String myString = "hello"; myString += " world";

Was Sie in Erinnerung bekommen, ist nicht 1 Saite „Hallo Welt“, sondern tatsächlich 2 Saiten. Der String myString enthält „hello world“, wie Sie es erwarten würden. Der ursprüngliche String mit dem Wert „hello“ ist jedoch immer noch am Leben, ohne Verweis darauf, und wartet darauf, von der Garbage Collection erfasst zu werden. Das ist auch der Grund, warum Sie Passwörter in einem Char-Array statt in einem String speichern sollten. Wenn Sie ein Passwort als String speichern, bleibt es bis zum nächsten GC für eine unvorhersehbare Zeit in einem für Menschen lesbaren Format im Speicher. Zurück zur oben beschriebenen Unveränderlichkeit bleibt der String im Speicher, auch wenn Sie ihm nach der Verwendung einen anderen Wert zuweisen. Wenn Sie jedoch das Char-Array leeren, nachdem Sie das Passwort verwendet haben, verschwindet es von überall.

2. Auswählen des richtigen Datentyps

Bevor Sie mit dem Schreiben von Code beginnen, sollten Sie entscheiden, welche Datentypen Sie für Ihre Sammlung verwenden möchten. Sollten Sie beispielsweise einen Vector oder eine ArrayList verwenden? Nun, es hängt von Ihrem Anwendungsfall ab. Wenn Sie eine Thread-sichere Sammlung benötigen, mit der nur ein Thread gleichzeitig arbeiten kann, sollten Sie einen Vector auswählen, da er synchronisiert ist. In anderen Fällen sollten Sie sich wahrscheinlich an eine ArrayList halten, es sei denn, Sie haben wirklich einen bestimmten Grund, Vektoren zu verwenden.

Wie wäre es mit dem Fall, wenn Sie eine Sammlung mit einzigartigen Objekten wünschen? Nun, Sie sollten wahrscheinlich ein Set auswählen. Sie können absichtlich keine Duplikate enthalten, sodass Sie sich nicht selbst darum kümmern müssen. Es gibt mehrere Arten von Sets, also wählen Sie eines aus, das zu Ihrem Anwendungsfall passt. Für eine einfache Gruppe eindeutiger Elemente können Sie ein HashSet verwenden. Wenn Sie die Reihenfolge der eingefügten Elemente beibehalten möchten, wählen Sie ein LinkedHashSet . Ein TreeSet sortiert Elemente automatisch, sodass Sie keine Sortiermethoden darauf aufrufen müssen. Es sollte die Artikel auch effizient sortieren, ohne dass Sie an Sortieralgorithmen denken müssen.

Daten dominieren. Wenn Sie die richtigen Datenstrukturen gewählt und die Dinge gut organisiert haben, werden die Algorithmen fast immer selbstverständlich sein. Datenstrukturen, nicht Algorithmen, sind von zentraler Bedeutung für die Programmierung.
— Die 5 Programmierregeln von Rob Pike

Das Sortieren von Ganzzahlen oder Zeichenfolgen ist ziemlich einfach. Was aber, wenn Sie eine Klasse nach einer Eigenschaft sortieren möchten? Angenommen, Sie schreiben eine Liste mit Mahlzeiten, die Sie essen, und speichern ihre Namen und Zeitstempel. Wie würden Sie die Mahlzeiten nach Zeitstempel vom niedrigsten zum höchsten sortieren? Zum Glück ist es ziemlich einfach. Es reicht aus, das Comparable -Interface in der Meal -Klasse zu implementieren und die compareTo() Funktion zu überschreiben. Um die Mahlzeiten vom niedrigsten zum höchsten Zeitstempel zu sortieren, könnten wir so etwas schreiben.

 @Override public int compareTo(Object object) { Meal meal = (Meal) object; if (this.timestamp < meal.getTimestamp()) { return -1; } else if (this.timestamp > meal.getTimestamp()) { return 1; } return 0; }

3. Standortaktualisierungen

Es gibt viele Apps, die den Standort des Benutzers erfassen. Verwenden Sie dazu die Google Location Services API, die viele nützliche Funktionen enthält. Es gibt einen separaten Artikel über die Verwendung, daher werde ich ihn nicht wiederholen.

Ich möchte nur einige wichtige Punkte aus Performance-Sicht hervorheben.

Verwenden Sie zunächst nur den genauesten Standort, den Sie benötigen. Wenn Sie beispielsweise Wettervorhersagen erstellen, benötigen Sie nicht den genauesten Standort. Es ist schneller und batterieeffizienter, nur ein sehr raues Gebiet basierend auf dem Netzwerk zu erhalten. Sie können dies erreichen, indem Sie die Priorität auf LocationRequest.PRIORITY_LOW_POWER setzen.

Sie können auch eine Funktion von LocationRequest namens setSmallestDisplacement() . Wenn Sie dies in Metern festlegen, wird Ihre App nicht über Standortänderungen benachrichtigt, wenn diese kleiner als der angegebene Wert waren. Wenn Sie beispielsweise eine Karte mit nahe gelegenen Restaurants in Ihrer Nähe haben und die kleinste Verschiebung auf 20 Meter festlegen, stellt die App keine Anfragen zum Überprüfen von Restaurants, wenn der Benutzer nur in einem Raum herumläuft. Die Anfragen wären nutzlos, da es sowieso kein neues Restaurant in der Nähe gäbe.

Die zweite Regel fordert Standortaktualisierungen nur so oft an, wie Sie sie benötigen. Das ist ziemlich selbsterklärend. Wenn Sie wirklich diese Wettervorhersage-App erstellen, müssen Sie den Standort nicht alle paar Sekunden anfordern, da Sie wahrscheinlich keine so genauen Vorhersagen haben (kontaktieren Sie mich, wenn Sie eine haben). Sie können die Funktion setInterval() verwenden, um das erforderliche Intervall festzulegen, in dem das Gerät Ihre App über den Standort aktualisiert. Wenn mehrere Apps weiterhin den Standort des Benutzers anfordern, wird jede App bei jeder neuen Standortaktualisierung benachrichtigt, auch wenn Sie ein höheres setInterval() eingestellt haben. Um zu verhindern, dass Ihre App zu oft benachrichtigt wird, stellen Sie sicher, dass Sie mit setFastestInterval() immer ein schnellstes Aktualisierungsintervall festlegen.

Und schließlich fordert die dritte Regel Standortaktualisierungen nur dann an, wenn Sie sie benötigen. Wenn Sie alle x Sekunden einige Objekte in der Nähe auf der Karte anzeigen und die App in den Hintergrund geht, müssen Sie den neuen Standort nicht kennen. Es gibt keinen Grund, die Karte zu aktualisieren, wenn der Benutzer sie sowieso nicht sehen kann. Stellen Sie sicher, dass Sie das Abhören von Standortaktualisierungen gegebenenfalls beenden, vorzugsweise in onPause() . Sie können die Aktualisierungen dann in onResume() .

4. Netzwerkanfragen

Es besteht eine hohe Wahrscheinlichkeit, dass Ihre App das Internet zum Herunterladen oder Hochladen von Daten verwendet. Wenn dies der Fall ist, haben Sie mehrere Gründe, der Behandlung von Netzwerkanfragen Aufmerksamkeit zu schenken. Eine davon sind mobile Daten, die auf viele Menschen sehr begrenzt sind und die Sie nicht verschwenden sollten.

Der zweite ist die Batterie. Sowohl WLAN als auch Mobilfunknetze können ziemlich viel davon verbrauchen, wenn sie zu viel genutzt werden. Angenommen, Sie möchten 1 KB herunterladen. Um eine Netzwerkanfrage zu stellen, müssen Sie das Mobilfunk- oder WLAN-Radio aufwecken, dann können Sie Ihre Daten herunterladen. Das Radio schläft jedoch nicht sofort nach der Operation ein. Je nach Gerät und Netzbetreiber bleibt es noch etwa 20 bis 40 Sekunden in einem ziemlich aktiven Zustand.

Netzwerkanfragen

Also, was können Sie dagegen tun? Charge. Um zu vermeiden, dass das Radio alle paar Sekunden aktiviert wird, sollten Sie Dinge vorab abrufen, die der Benutzer möglicherweise in den kommenden Minuten benötigt. Die richtige Art der Stapelverarbeitung ist abhängig von Ihrer App sehr dynamisch, aber wenn es möglich ist, sollten Sie die Daten herunterladen, die der Benutzer möglicherweise in den nächsten 3-4 Minuten benötigt. Man könnte die Stapelparameter auch basierend auf dem Internettyp des Benutzers oder dem Ladestatus bearbeiten. Wenn der Benutzer beispielsweise während des Ladevorgangs im WLAN ist, können Sie viel mehr Daten vorab abrufen, als wenn der Benutzer mit schwachem Akku im mobilen Internet ist. Die Berücksichtigung all dieser Variablen kann eine schwierige Sache sein, die nur wenige Menschen tun würden. Glücklicherweise hilft Ihnen der GCM Network Manager!

GCM Network Manager ist eine wirklich hilfreiche Klasse mit vielen anpassbaren Attributen. Sie können sowohl wiederkehrende als auch einmalige Aufgaben einfach planen. Bei sich wiederholenden Aufgaben können Sie sowohl das niedrigste als auch das höchste Wiederholungsintervall einstellen. Dadurch können nicht nur Ihre Anfragen, sondern auch Anfragen von anderen Apps gestapelt werden. Das Radio muss nur einmal pro Zeitraum aufgeweckt werden, und während es aktiv ist, laden alle Apps in der Warteschlange herunter und laden hoch, was sie sollen. Dieser Manager kennt auch den Netzwerktyp und den Ladezustand des Geräts, sodass Sie ihn entsprechend anpassen können. Weitere Details und Beispiele finden Sie in diesem Artikel. Ich fordere Sie auf, es sich anzusehen. Eine Beispielaufgabe sieht so aus:

 Task task = new OneoffTask.Builder() .setService(CustomService.class) .setExecutionWindow(0, 30) .setTag(LogService.TAG_TASK_ONEOFF_LOG) .setUpdateCurrent(false) .setRequiredNetwork(Task.NETWORK_STATE_CONNECTED) .setRequiresCharging(false) .build();

Übrigens, seit Android 3.0 erhalten Sie eine NetworkOnMainThreadException , wenn Sie eine Netzwerkanfrage im Haupt-Thread stellen. Das wird Sie definitiv warnen, das nicht noch einmal zu tun.

5. Reflexion

Reflexion ist die Fähigkeit von Klassen und Objekten, ihre eigenen Konstruktoren, Felder, Methoden usw. zu untersuchen. Es wird normalerweise aus Gründen der Abwärtskompatibilität verwendet, um zu überprüfen, ob eine bestimmte Methode für eine bestimmte Betriebssystemversion verfügbar ist. Wenn Sie zu diesem Zweck Reflektion verwenden müssen, achten Sie darauf, die Antwort zwischenzuspeichern, da die Verwendung von Reflektion ziemlich langsam ist. Einige weit verbreitete Bibliotheken verwenden auch Reflection, wie Roboguice für die Abhängigkeitsinjektion. Aus diesem Grund sollten Sie Dagger 2 bevorzugen. Weitere Details zur Reflexion finden Sie in einem separaten Beitrag.

6. Autoboxen

Autoboxing und Unboxing sind Prozesse zum Konvertieren eines primitiven Typs in einen Objekttyp oder umgekehrt. In der Praxis bedeutet dies, einen int in einen Integer umzuwandeln. Um dies zu erreichen, verwendet der Compiler intern die Funktion Integer.valueOf() . Das Konvertieren ist nicht nur langsam, Objekte benötigen auch viel mehr Speicher als ihre primitiven Äquivalente. Schauen wir uns etwas Code an.

 Integer total = 0; for (int i = 0; i < 1000000; i++) { total += i; }

Während dies im Durchschnitt 500 ms dauert, wird es durch Umschreiben, um Autoboxing zu vermeiden, drastisch beschleunigt.

 int total = 0; for (int i = 0; i < 1000000; i++) { total += i; }

Diese Lösung läuft mit etwa 2 ms, was 25-mal schneller ist. Wenn Sie mir nicht glauben, testen Sie es aus. Die Zahlen werden natürlich pro Gerät unterschiedlich sein, aber es sollte immer noch viel schneller sein. Und es ist auch ein wirklich einfacher Schritt zur Optimierung.

Okay, Sie erstellen wahrscheinlich nicht oft eine Variable vom Typ Integer. Aber was ist mit den Fällen, in denen es schwieriger zu vermeiden ist? Wie in einer Karte, wo Sie Objekte verwenden müssen, wie Map<Integer, Integer> ? Schauen Sie sich die Lösung an, die viele Leute verwenden.

 Map<Integer, Integer> myMap = new HashMap<>(); for (int i = 0; i < 100000; i++) { myMap.put(i, random.nextInt()); }

Das Einfügen von 100.000 zufälligen Ints in die Karte dauert etwa 250 ms. Betrachten Sie nun die Lösung mit SparseIntArray.

 SparseIntArray myArray = new SparseIntArray(); for (int i = 0; i < 100000; i++) { myArray.put(i, random.nextInt()); }

Dies dauert viel weniger, ungefähr 50 ms. Es ist auch eine der einfacheren Methoden zur Verbesserung der Leistung, da nichts Kompliziertes getan werden muss und der Code auch lesbar bleibt. Während das Ausführen einer klaren App mit der ersten Lösung 13 MB meines Speichers beanspruchte, nahm die Verwendung von primitiven ints etwas weniger als 7 MB in Anspruch, also nur die Hälfte davon.

SparseIntArray ist nur eine der coolen Sammlungen, die Ihnen helfen können, Autoboxing zu vermeiden. Eine Map wie Map<Integer, Long> könnte durch SparseLongArray ersetzt werden, da der Wert der Map vom Typ Long ist. Wenn Sie sich den Quellcode von SparseLongArray , werden Sie etwas ziemlich Interessantes sehen. Unter der Haube handelt es sich im Grunde nur um ein Paar Arrays. Sie können auch ein SparseBooleanArray ähnliche Weise verwenden.

Wenn Sie den Quellcode gelesen haben, ist Ihnen vielleicht ein Hinweis aufgefallen, der besagt, dass SparseIntArray langsamer sein kann als HashMap . Ich habe viel experimentiert, aber für mich war SparseIntArray sowohl in Bezug auf den Speicher als auch auf die Leistung immer besser. Ich denke, es liegt immer noch an Ihnen, welche Sie wählen, experimentieren Sie mit Ihren Anwendungsfällen und sehen Sie, welche am besten zu Ihnen passt. Haben Sie auf jeden Fall die SparseArrays im Kopf, wenn Sie Karten verwenden.

7. OnDraw

Wie ich oben gesagt habe, werden Sie beim Optimieren der Leistung wahrscheinlich den größten Nutzen darin sehen, Code zu optimieren, der häufig ausgeführt wird. Eine der häufig ausgeführten Funktionen ist onDraw() . Es überrascht Sie vielleicht nicht, dass es für das Zeichnen von Ansichten auf dem Bildschirm verantwortlich ist. Da die Geräte in der Regel mit 60 fps laufen, wird die Funktion 60 Mal pro Sekunde ausgeführt. Jeder Frame hat 16 ms Zeit, um vollständig bearbeitet zu werden, einschließlich seiner Vorbereitung und seines Zeichnens, daher sollten Sie langsame Funktionen wirklich vermeiden. Nur der Haupt-Thread kann auf dem Bildschirm zeichnen, daher sollten Sie es vermeiden, teure Operationen darauf durchzuführen. Wenn Sie den Haupt-Thread für einige Sekunden einfrieren, erhalten Sie möglicherweise den berüchtigten Application Not Responding (ANR)-Dialog. Verwenden Sie für die Größenänderung von Bildern, Datenbankarbeiten usw. einen Hintergrund-Thread.

Wenn Sie glauben, dass Ihre Benutzer diesen Rückgang der Bildrate nicht bemerken, liegen Sie falsch!
Twittern

Ich habe einige Leute gesehen, die versuchten, ihren Code zu kürzen, weil sie dachten, dass es auf diese Weise effizienter wäre. Das ist definitiv nicht der richtige Weg, da kürzerer Code absolut nicht schneller Code bedeutet. Auf keinen Fall sollten Sie die Qualität des Codes an der Anzahl der Zeilen messen.

Eines der Dinge, die Sie in onDraw() vermeiden sollten, ist die Zuweisung von Objekten wie Paint. Bereiten Sie alles im Konstruktor vor, damit es beim Zeichnen fertig ist. Auch wenn Sie onDraw() optimiert haben, sollten Sie es nur so oft wie nötig aufrufen. Was ist besser als der Aufruf einer optimierten Funktion? Nun, überhaupt keine Funktion aufrufen. Falls Sie Text zeichnen möchten, gibt es eine hübsche Hilfsfunktion namens drawText() , mit der Sie Dinge wie den Text, die Koordinaten und die Textfarbe angeben können.

8. ViewHolder

Sie kennen das wahrscheinlich, aber ich kann es nicht überspringen. Das Viewholder-Entwurfsmuster ist eine Möglichkeit, das Scrollen von Listen reibungsloser zu gestalten. Es ist eine Art View-Caching, das die Aufrufe von findViewById() und das Aufblähen von Views durch Speichern erheblich reduzieren kann. So oder so ähnlich kann es aussehen.

 static class ViewHolder { TextView title; TextView text; public ViewHolder(View view) { title = (TextView) view.findViewById(R.id.title); text = (TextView) view.findViewById(R.id.text); } }

Dann können Sie in der Funktion getView() Ihres Adapters überprüfen, ob Sie eine verwendbare Ansicht haben. Wenn nicht, erstellen Sie eine.

 ViewHolder viewHolder; if (convertView == null) { convertView = inflater.inflate(R.layout.list_item, viewGroup, false); viewHolder = new ViewHolder(convertView); convertView.setTag(viewHolder); } else { viewHolder = (ViewHolder) convertView.getTag(); } viewHolder.title.setText("Hello World");

Im Internet finden Sie viele nützliche Informationen zu diesem Muster. Es kann auch in Fällen verwendet werden, in denen Ihre Listenansicht mehrere verschiedene Arten von Elementen enthält, wie z. B. einige Abschnittsüberschriften.

9. Bildgröße ändern

Die Chancen stehen gut, dass Ihre App einige Bilder enthält. Falls Sie einige JPGs aus dem Internet herunterladen, können diese sehr große Auflösungen haben. Die Geräte, auf denen sie angezeigt werden, werden jedoch viel kleiner sein. Selbst wenn Sie ein Foto mit der Kamera Ihres Geräts aufnehmen, muss es vor der Anzeige verkleinert werden, da die Fotoauflösung viel größer ist als die Auflösung des Displays. Das Ändern der Größe von Bildern vor dem Anzeigen ist von entscheidender Bedeutung. Wenn Sie versuchen würden, sie in voller Auflösung anzuzeigen, würde Ihnen ziemlich schnell der Speicherplatz ausgehen. In den Android-Dokumenten wird viel über die effiziente Anzeige von Bitmaps geschrieben, ich werde versuchen, es zusammenzufassen.

Sie haben also eine Bitmap, wissen aber nichts darüber. Es gibt ein nützliches Bitmap-Flag namens inJustDecodeBounds, mit dem Sie die Auflösung der Bitmap herausfinden können. Nehmen wir an, Ihre Bitmap hat eine Größe von 1024 x 768 und die zur Anzeige verwendete ImageView hat nur 400 x 300. Sie sollten die Auflösung der Bitmap so lange durch 2 teilen, bis sie immer noch größer als die angegebene ImageView ist. Wenn Sie dies tun, wird die Bitmap um den Faktor 2 heruntergerechnet, sodass Sie eine Bitmap von 512 x 384 erhalten. Die heruntergesampelte Bitmap benötigt 4x weniger Speicher, was Ihnen sehr dabei hilft, den berühmten OutOfMemory-Fehler zu vermeiden.

Jetzt, da Sie wissen, wie es geht, sollten Sie es nicht tun. … Zumindest nicht, wenn Ihre App stark auf Bilder angewiesen ist und es nicht nur 1-2 Bilder sind. Vermeiden Sie auf jeden Fall Dinge wie das manuelle Ändern der Größe und das Recycling von Bildern, verwenden Sie dafür einige Bibliotheken von Drittanbietern. Die beliebtesten sind Picasso von Square, Universal Image Loader, Fresco von Facebook oder mein Favorit Glide. Es gibt eine riesige aktive Community von Entwicklern, sodass Sie auch im Issues-Bereich auf GitHub viele hilfreiche Leute finden können.

10. Strikter Modus

Der strikte Modus ist ein ziemlich nützliches Entwicklertool, das viele Leute nicht kennen. Es wird normalerweise zum Erkennen von Netzwerkanforderungen oder Festplattenzugriffen vom Haupt-Thread verwendet. Sie können festlegen, nach welchen Problemen der strenge Modus suchen und welche Strafe er auslösen soll. Ein Google-Beispiel sieht so aus:

 public void onCreate() { if (DEVELOPER_MODE) { StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder() .detectDiskReads() .detectDiskWrites() .detectNetwork() .penaltyLog() .build()); StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder() .detectLeakedSqlLiteObjects() .detectLeakedClosableObjects() .penaltyLog() .penaltyDeath() .build()); } super.onCreate(); }

Wenn Sie jedes Problem erkennen möchten, das der strenge Modus finden kann, können Sie auch detectAll() verwenden. Wie bei vielen Leistungstipps sollten Sie nicht blind versuchen, alle Berichte des strengen Modus zu reparieren. Untersuchen Sie es einfach, und wenn Sie sicher sind, dass es kein Problem ist, lassen Sie es in Ruhe. Stellen Sie außerdem sicher, dass Sie den Strict Mode nur zum Debuggen verwenden und ihn bei Produktions-Builds immer deaktiviert haben.

Debugging-Leistung: Der Profi-Weg

Sehen wir uns nun einige Tools an, die Ihnen helfen können, Engpässe zu finden oder zumindest zu zeigen, dass etwas nicht stimmt.

1. Android-Monitor

Dies ist ein in Android Studio integriertes Tool. Standardmäßig finden Sie den Android-Monitor in der linken unteren Ecke und können dort zwischen 2 Registerkarten wechseln. Logcat und Monitore. Der Abschnitt Monitore enthält 4 verschiedene Diagramme. Netzwerk, CPU, GPU und Speicher. Sie sind ziemlich selbsterklärend, also werde ich sie nur schnell durchgehen. Hier ist ein Screenshot der Grafiken, die beim Parsen von JSON beim Herunterladen aufgenommen wurden.

Android-Monitor

Der Netzwerkteil zeigt den eingehenden und ausgehenden Datenverkehr in KB/s. Der CPU-Teil zeigt die CPU-Auslastung in Prozent an. Der GPU-Monitor zeigt an, wie viel Zeit zum Rendern der Frames eines UI-Fensters benötigt wird. Dies ist der detaillierteste dieser 4 Monitore. Wenn Sie also weitere Details dazu wünschen, lesen Sie dies.

Schließlich haben wir den Speichermonitor, den Sie wahrscheinlich am häufigsten verwenden werden. Standardmäßig wird die aktuelle Menge an freiem und zugewiesenem Speicher angezeigt. Sie können damit auch eine Garbage Collection erzwingen, um zu testen, ob die Menge des verwendeten Speichers sinkt. Es hat eine nützliche Funktion namens Dump Java Heap, die eine HPROF-Datei erstellt, die mit dem HPROF Viewer und Analyzer geöffnet werden kann. Auf diese Weise können Sie sehen, wie viele Objekte Sie zugewiesen haben, wie viel Speicher von was belegt wird und welche Objekte möglicherweise Speicherlecks verursachen. Zu lernen, wie man diesen Analysator benutzt, ist nicht die einfachste Aufgabe da draußen, aber es lohnt sich. Das nächste, was Sie mit dem Speichermonitor tun können, ist eine zeitgesteuerte Zuordnungsverfolgung, die Sie nach Belieben starten und stoppen können. Dies kann in vielen Fällen nützlich sein, beispielsweise beim Scrollen oder Drehen des Geräts.

2. GPU-Überziehung

Dies ist ein einfaches Hilfstool, das Sie in den Entwickleroptionen aktivieren können, sobald Sie den Entwicklermodus aktiviert haben. Wählen Sie „GPU-Überzeichnung debuggen“, „Überzeichnungsbereiche anzeigen“ und Ihr Bildschirm bekommt einige seltsame Farben. Es ist ok, das soll passieren. Die Farben geben an, wie oft ein bestimmter Bereich überzeichnet wurde. Echte Farbe bedeutet, dass es keine Überziehung gab, das sollten Sie anstreben. Blau bedeutet eine Überziehung, Grün bedeutet zwei, Rosa drei, Rot vier.

GPU-Überziehung

Obwohl es am besten ist, Echtfarben zu sehen, werden Sie immer einige Überzeichnungen sehen, insbesondere bei Texten, Navigationsleisten, Dialogen und mehr. Versuchen Sie also nicht, es vollständig loszuwerden. Wenn Ihre App bläulich oder grünlich ist, ist das wahrscheinlich in Ordnung. Wenn Sie jedoch auf einigen einfachen Bildschirmen zu viel Rot sehen, sollten Sie untersuchen, was los ist. Es könnten zu viele Fragmente übereinander gestapelt sein, wenn Sie sie immer wieder hinzufügen, anstatt sie zu ersetzen. Wie ich oben erwähnt habe, ist das Zeichnen der langsamste Teil von Apps, daher macht es keinen Sinn, etwas zu zeichnen, wenn mehr als 3 Ebenen darauf gezeichnet werden. Fühlen Sie sich frei, Ihre Lieblings-Apps damit auszuprobieren. Sie werden sehen, dass sogar Apps mit über einer Milliarde Downloads rote Bereiche haben, also lassen Sie es beim Optimieren ruhig angehen.

3. GPU-Rendering

Dies ist ein weiteres Tool aus den Entwickleroptionen, das als Profil-GPU-Rendering bezeichnet wird. Wählen Sie nach der Auswahl "Auf dem Bildschirm als Balken". Sie werden feststellen, dass einige farbige Balken auf Ihrem Bildschirm erscheinen. Da jede Anwendung separate Balken hat, hat die Statusleiste seltsamerweise ihre eigenen, und falls Sie Software-Navigationsschaltflächen haben, haben sie auch ihre eigenen Balken. Wie auch immer, die Balken werden aktualisiert, wenn Sie mit dem Bildschirm interagieren.

GPU-Rendering

Die Balken bestehen aus 3-4 Farben, und laut der Android-Dokumentation spielt ihre Größe tatsächlich eine Rolle. Je kleiner, desto besser. Unten haben Sie Blau, das die Zeit darstellt, die zum Erstellen und Aktualisieren der Anzeigelisten der Ansicht verwendet wurde. Wenn dieser Teil zu groß ist, bedeutet dies, dass viele benutzerdefinierte Ansichten gezeichnet werden oder viel Arbeit in den onDraw() Funktionen geleistet wird. Wenn Sie Android 4.0 oder höher haben, sehen Sie einen violetten Balken über dem blauen. Dies stellt die Zeit dar, die für die Übertragung von Ressourcen an den Render-Thread aufgewendet wurde. Dann kommt der rote Teil, der die Zeit darstellt, die der 2D-Renderer von Android damit verbracht hat, Befehle an OpenGL auszugeben, um Anzeigelisten zu zeichnen und neu zu zeichnen. Oben befindet sich der orangefarbene Balken, der die Zeit darstellt, die die CPU darauf wartet, dass die GPU ihre Arbeit beendet. Wenn es zu groß ist, erledigt die App zu viel Arbeit auf der GPU.

Wenn Sie gut genug sind, gibt es eine weitere Farbe über dem Orange. Es ist eine grüne Linie, die den Schwellenwert von 16 ms darstellt. Da Ihr Ziel darin bestehen sollte, Ihre App mit 60 fps auszuführen, haben Sie 16 ms Zeit, um jeden Frame zu zeichnen. Wenn Sie es nicht schaffen, könnten einige Frames übersprungen werden, die App könnte ruckeln, und der Benutzer würde es definitiv bemerken. Achten Sie besonders auf Animationen und Scrollen, da ist die Glätte am wichtigsten. Auch wenn Sie mit diesem Tool einige übersprungene Frames erkennen können, hilft es Ihnen nicht wirklich herauszufinden, wo genau das Problem liegt.

4. Hierarchie-Viewer

Dies ist eines meiner Lieblingstools da draußen, da es wirklich mächtig ist. Sie können es von Android Studio über Tools -> Android -> Android Device Monitor starten, oder es befindet sich auch in Ihrem sdk/tools-Ordner als „Monitor“. Sie können dort auch eine eigenständige ausführbare Hierarchyviewer-Datei finden, aber da sie veraltet ist, sollten Sie den Monitor öffnen. Wie auch immer Sie den Android Device Monitor öffnen, wechseln Sie zur Hierarchy Viewer-Perspektive. Wenn Sie keine laufenden Apps sehen, die Ihrem Gerät zugewiesen sind, gibt es ein paar Dinge, die Sie tun können, um das Problem zu beheben. Schauen Sie sich auch diesen Problemthread an, es gibt Leute mit allen Arten von Problemen und allen Arten von Lösungen. Etwas sollte auch für Sie funktionieren.

Mit Hierarchy Viewer können Sie sich (offensichtlich) einen wirklich übersichtlichen Überblick über Ihre Ansichtshierarchien verschaffen. Wenn Sie jedes Layout in einem separaten XML sehen, können Sie leicht nutzlose Ansichten erkennen. Wenn Sie die Layouts jedoch immer wieder kombinieren, kann es schnell unübersichtlich werden. Ein Tool wie dieses macht es einfach, zum Beispiel ein RelativeLayout, das nur ein untergeordnetes Element hat, ein anderes RelativeLayout zu erkennen. Das macht einen von ihnen entfernbar.

Vermeiden Sie den Aufruf requestLayout() , da dies dazu führt, dass die gesamte Ansichtshierarchie durchlaufen wird, um herauszufinden, wie groß jede Ansicht sein sollte. Wenn es einen Konflikt mit den Messungen gibt, kann die Hierarchie mehrmals durchlaufen werden, was, wenn dies während einer Animation passiert, definitiv dazu führt, dass einige Frames übersprungen werden. Wenn Sie mehr darüber erfahren möchten, wie Android seine Ansichten zeichnet, können Sie dies lesen. Sehen wir uns eine Ansicht aus dem Hierarchy Viewer an.

Hierarchie-Viewer

Die obere rechte Ecke enthält eine Schaltfläche zum Maximieren der Vorschau der jeweiligen Ansicht in einem eigenständigen Fenster. Darunter sehen Sie auch die aktuelle Vorschau der Ansicht in der App. Das nächste Element ist eine Zahl, die angibt, wie viele untergeordnete Elemente die angegebene Ansicht hat, einschließlich der Ansicht selbst. Wenn Sie einen Knoten auswählen (vorzugsweise den Stammknoten) und auf „Layoutzeiten abrufen“ (3 farbige Kreise) klicken, werden 3 weitere Werte ausgefüllt, zusammen mit farbigen Kreisen, die mit Maß, Layout und Zeichnung gekennzeichnet sind. Es ist vielleicht nicht überraschend, dass die Messphase die Zeit darstellt, die zum Messen der gegebenen Ansicht benötigt wurde. In der Layoutphase geht es um die Renderzeit, während das Zeichnen der eigentliche Zeichenvorgang ist. Diese Werte und Farben sind relativ zueinander. Grün bedeutet, dass die Ansicht in den oberen 50 % aller Ansichten in der Struktur gerendert wird. Gelb bedeutet, dass in den langsameren 50 % aller Ansichten im Baum gerendert wird, rot bedeutet, dass die angegebene Ansicht eine der langsamsten ist. Da diese Werte relativ sind, gibt es immer rote. Sie können sie einfach nicht vermeiden.

Unter den Werten haben Sie den Klassennamen, z. B. „TextView“, eine interne Ansichts-ID des Objekts und die android:id der Ansicht, die Sie in den XML-Dateien festlegen. Ich fordere Sie dringend auf, es sich zur Gewohnheit zu machen, allen Ansichten IDs hinzuzufügen, auch wenn Sie im Code nicht darauf verweisen. Dadurch wird das Identifizieren der Ansichten im Hierarchy Viewer wirklich einfach, und falls Sie automatisierte Tests in Ihrem Projekt haben, wird es auch das Targeting der Elemente viel schneller machen. Das spart Zeit für Sie und Ihre Kollegen beim Schreiben. Das Hinzufügen von IDs zu Elementen, die in XML-Dateien hinzugefügt wurden, ist ziemlich einfach. Aber was ist mit den dynamisch hinzugefügten Elementen? Nun, es stellt sich heraus, dass es auch wirklich einfach ist. Erstellen Sie einfach eine ids.xml-Datei in Ihrem Werteordner und geben Sie die erforderlichen Felder ein. Es kann so aussehen:

 <resources> <item name="item_title" type="id"/> <item name="item_body" type="id"/> </resources>

Dann können Sie im Code setId(R.id.item_title) . Es könnte nicht einfacher sein.

Es gibt noch ein paar Dinge, auf die Sie bei der Optimierung der Benutzeroberfläche achten sollten. Grundsätzlich sollten Sie tiefe Hierarchien vermeiden und flache, vielleicht breite Hierarchien bevorzugen. Verwenden Sie keine Layouts, die Sie nicht benötigen. Beispielsweise können Sie wahrscheinlich eine Gruppe verschachtelter LinearLayouts entweder durch ein RelativeLayout oder ein TableLayout . Fühlen Sie sich frei, mit verschiedenen Layouts zu experimentieren, verwenden Sie nicht immer nur LinearLayout und RelativeLayout . Versuchen Sie auch, bei Bedarf einige benutzerdefinierte Ansichten zu erstellen. Dies kann die Leistung erheblich verbessern, wenn es gut gemacht wird. Wussten Sie zum Beispiel, dass Instagram keine TextViews zum Anzeigen von Kommentaren verwendet?

Weitere Informationen zum Hierarchy Viewer finden Sie auf der Android-Entwickler-Website mit Beschreibungen der verschiedenen Bereiche, der Verwendung des Pixel Perfect-Tools usw. Eine weitere Sache, auf die ich hinweisen möchte, ist das Erfassen der Ansichten in einer .psd-Datei, was mit durchgeführt werden kann die Schaltfläche „Fensterebenen erfassen“. Jede Ansicht befindet sich in einer separaten Ebene, sodass es wirklich einfach ist, sie in Photoshop oder GIMP auszublenden oder zu ändern. Oh, das ist ein weiterer Grund, jeder möglichen Ansicht eine ID hinzuzufügen. Dadurch erhalten die Ebenen Namen, die tatsächlich Sinn machen.

In den Entwickleroptionen finden Sie viel mehr Debugging-Tools, daher rate ich Ihnen, sie zu aktivieren und zu sehen, was sie tun. Was könnte möglicherweise falsch laufen?

Die Website für Android-Entwickler enthält eine Reihe von Best Practices für die Leistung. Sie decken viele verschiedene Bereiche ab, einschließlich der Speicherverwaltung, über die ich nicht wirklich gesprochen habe. Ich habe es stillschweigend ignoriert, weil der Umgang mit Speicher und das Verfolgen von Speicherlecks eine ganz andere Geschichte sind. Die Verwendung einer Bibliothek eines Drittanbieters zur effizienten Anzeige von Bildern ist sehr hilfreich, aber wenn Sie immer noch Speicherprobleme haben, sehen Sie sich Leak Canary von Square an oder lesen Sie dies.

Einpacken

Das war also die gute Nachricht. Die schlechte Nachricht ist, dass die Optimierung von Android-Apps viel komplizierter ist. Es gibt viele Möglichkeiten, alles zu tun, also sollten Sie mit den Vor- und Nachteilen vertraut sein. Normalerweise gibt es keine Wunderlösung, die nur Vorteile hat. Nur wenn Sie verstehen, was hinter den Kulissen passiert, können Sie die für Sie beste Lösung auswählen. Nur weil Ihr Lieblingsentwickler sagt, dass etwas gut ist, bedeutet das nicht unbedingt, dass es die beste Lösung für Sie ist. Es gibt viel mehr Bereiche zu besprechen und mehr Profiling-Tools, die fortgeschrittener sind, also kommen wir beim nächsten Mal vielleicht dazu.

Stellen Sie sicher, dass Sie von den Top-Entwicklern und Top-Unternehmen lernen. Unter diesem Link finden Sie ein paar hundert Engineering-Blogs. Es geht offensichtlich nicht nur um Android-bezogene Sachen, wenn Sie also nur an Android interessiert sind, müssen Sie den jeweiligen Blog filtern. Ich kann die Blogs von Facebook und Instagram sehr empfehlen. Obwohl die Instagram-Benutzeroberfläche auf Android fragwürdig ist, enthält ihr Engineering-Blog einige wirklich coole Artikel. Für mich ist es großartig, dass es so einfach ist zu sehen, wie Dinge in Unternehmen gemacht werden, die täglich Hunderte von Millionen von Benutzern verwalten, sodass es verrückt erscheint, ihre Blogs nicht zu lesen. Die Welt verändert sich sehr schnell. Wenn Sie also nicht ständig versuchen, sich zu verbessern, von anderen zu lernen und neue Tools zu verwenden, werden Sie zurückgelassen. Wie Mark Twain sagte, hat eine Person, die nicht lesen kann, keinen Vorteil gegenüber einer, die nicht lesen kann.