Auf dieser Seite wird erläutert, wie Sie die Speichernutzung in Ihrer App proaktiv reduzieren können. Informationen dazu, wie das Android-Betriebssystem den Speicher verwaltet, finden Sie im Überblick zur Speicherverwaltung.
Der Arbeitsspeicher (RAM) ist eine wertvolle Ressource für jede Softwareentwicklungsumgebung und noch wertvoller für ein mobiles Betriebssystem, in dem der physische Speicher oft begrenzt ist. Sowohl die Android-Laufzeit (Android Runtime, ART) als auch die virtuelle Dalvik-Maschine führen routinemäßige automatische Speicherbereinigung durch. Das bedeutet jedoch nicht, dass Sie ignorieren können, wann und wo Ihre App Speicher zuweist und freigibt. Sie müssen weiterhin Speicherlecks vermeiden, die in der Regel durch das Beibehalten von Objektverweisen in statischen Mitgliedsvariablen verursacht werden. Außerdem müssen Sie alle Reference-Objekte zum richtigen Zeitpunkt freigeben, wie durch Lebenszyklus-Callbacks definiert.
Code und Ressourcen der App reduzieren
Einige Ressourcen und Bibliotheken in Ihrem Code können Arbeitsspeicher belegen, ohne dass Sie es bemerken. Die Gesamtgröße Ihrer App, einschließlich Bibliotheken von Drittanbietern oder eingebetteter Ressourcen, kann sich darauf auswirken, wie viel Arbeitsspeicher Ihre App verbraucht. Sie können den Arbeitsspeicherverbrauch Ihrer App verbessern, indem Sie redundante, unnötige oder aufgeblähte Komponenten, Ressourcen und Bibliotheken aus Ihrem Code entfernen.
Gesamtgröße der App durch Aktivieren von R8 reduzieren
Der kompilierte Anwendungscode ist ein aktiver Teil des Laufzeitspeicherbedarfs. Jede Klasse, Methode, Bibliotheksabhängigkeit und Stringkonstante muss beim Ausführen in den RAM geladen werden. Je größer Ihre kompilierte Codebasis ist, desto mehr physischen RAM benötigt Ihre App.
Sie können R8 verwenden, um den Speicherbedarf Ihrer App zu reduzieren. R8 ist zwar traditionell für die Verkleinerung der APK-Größe bekannt, hat aber auch einen direkten, positiven Einfluss auf den Laufzeitspeicher (RAM). R8 analysiert den Bytecode Ihrer App, um nicht verwendeten Code zu entfernen, redundante Klassen zusammenzuführen, Methoden zu inline zu setzen und Kennungen zu minimieren. Da weniger kompilierter Bytecode aus der APK in den RAM geladen wird, verringert sich der gesamte grundlegende Speicherbedarf der App. Außerdem wird der RAM-Overhead durch die Minimierung von Klassen-, Methoden- und Feldnamen in kürzere Kennungen direkt reduziert. Durch Optimierungen wie das Zusammenführen von Klassen und das Inlining umfangreicher Methoden werden auch kostspielige Laufzeit-Lookups und Zuweisungsmuster ersetzt, was zu einem optimierten Heap- und Stapelspeicher führt.
Aufbewahrungsregeln verstehen
Keep-Regeln sind Konfigurationsanweisungen, die R8 mitteilen, welche Teile Ihres Codes bei der Optimierung beibehalten werden sollen. So wird verhindert, dass Code entfernt oder minimiert wird, auf den Ihre App angewiesen ist. Weitere Informationen finden Sie unter Keep-Regeln – Übersicht.
Schlecht geschriebene Keep-Regeln verhindern, dass R8 große Teile Ihrer Codebasis optimiert. Vermeiden Sie zu allgemeine Aufbewahrungsregeln und halten Sie sich an die folgenden Best Practices:
- Globale Regeln, die Sie vermeiden sollten:
-dontoptimize: Deaktiviert die Optimierung für die gesamte App vollständig, was zu größeren, langsameren ausführbaren Dateien führt.-dontshrink: Verhindert das Entfernen von ungenutztem Code und ungenutzten Ressourcen.-dontobfuscate: Verhindert die Namensminimierung, wodurch wertvolle Speicherplatzersparnisse (insbesondere bei großen Apps) nicht genutzt werden können.
Paketweite Platzhalter vermeiden:Allgemeine Regeln wie
-keep class com.example.package.** { *; }zwingen R8, jede Klasse, jedes Feld und jede Methode in diesem Paket beizubehalten. Dadurch wird die Möglichkeit von R8, Code in diesem Paket zu entfernen, zu optimieren oder zu minimieren, vollständig unterbunden.Standard-R8-Konfigurationsdatei verwenden:Verwenden Sie immer
proguard-android-optimize.txt.
Weitere Informationen zum Schreiben von Aufbewahrungsregeln finden Sie in der Übersicht zu Aufbewahrungsregeln. Best Practices für die Verwendung von Regeln
Der R8 Configuration Analyzer bietet Einblicke in Ihre R8-Konfiguration und zeigt, wie sich jede Keep-Regel auf Ihre App auswirkt. Weitere Informationen dazu, wie Sie Regeln identifizieren, die die Optimierung blockieren, finden Sie unter R8 Configuration Analyzer.
Vorsicht bei der Verwendung externer Bibliotheken
Externer Bibliothekscode ist oft nicht für mobile Umgebungen geschrieben und kann ineffizient für die Arbeit an einem mobilen Client sein. Wenn Sie eine externe Bibliothek verwenden, müssen Sie sie möglicherweise für Mobilgeräte optimieren. Planen Sie diese Arbeit im Voraus und analysieren Sie die Bibliothek hinsichtlich der Codegröße und des RAM-Bedarfs, bevor Sie sie verwenden.
Auch einige für Mobilgeräte optimierte Bibliotheken können aufgrund unterschiedlicher Implementierungen Probleme verursachen. So kann es beispielsweise sein, dass eine Bibliothek Lite-Protobufs verwendet, während eine andere Micro-Protobufs nutzt. Das führt zu zwei verschiedenen Protobuf-Implementierungen in Ihrer App. Das kann bei verschiedenen Implementierungen von Logging, Analysen, Frameworks zum Laden von Bildern, Caching und vielen anderen Dingen passieren, die Sie nicht erwarten.
Durch die Optimierung Ihrer App mit R8 kann zwar ungenutzter Code aus Abhängigkeiten entfernt werden, die Effektivität ist jedoch oft durch die interne Konfiguration der Bibliothek begrenzt. Beispielsweise können umfassende Keep-Regeln oder die Verwendung von Reflection in einer Bibliothek verhindern, dass R8 den Code verkleinert, was zu einem größeren Speicherbedarf führt. Strategien zur Auswahl effizienter Bibliotheken finden Sie unter Bibliotheken mit Bedacht auswählen.
Verwenden Sie eine gemeinsam genutzte Bibliothek nicht für nur ein oder zwei von Dutzenden von Funktionen. Sie sollten nicht unnötig viel Code und Overhead einbinden. Wenn Sie überlegen, ob Sie eine Bibliothek verwenden sollten, suchen Sie nach einer Implementierung, die Ihren Anforderungen weitgehend entspricht. Andernfalls können Sie auch eine eigene Implementierung erstellen.
Hilt oder Dagger 2 für die Abhängigkeitsinjektion verwenden
Frameworks für die Abhängigkeitsinjektion können den Code, den Sie schreiben, vereinfachen und eine adaptive Umgebung bereitstellen, die für Tests und andere Konfigurationsänderungen nützlich ist.
Wenn Sie ein Framework für die Abhängigkeitsinjektion in Ihrer App verwenden möchten, sollten Sie Hilt oder Dagger in Betracht ziehen. Hilt ist eine Dependency Injection-Bibliothek für Android, die auf Dagger basiert. Dagger verwendet keine Reflexion, um den Code Ihrer App zu scannen. Sie können die statische Implementierung von Dagger zur Kompilierzeit in Android-Apps verwenden, ohne unnötige Laufzeitkosten oder Arbeitsspeichernutzung.
Andere Frameworks für die Abhängigkeitsinjektion, die Reflection verwenden, initialisieren Prozesse, indem sie Ihren Code nach Anmerkungen durchsuchen. Dieser Prozess kann deutlich mehr CPU-Zyklen und RAM erfordern und beim Starten der App zu einer spürbaren Verzögerung führen.
Achten Sie bei der Verwendung von Dependency Injection darauf, Speicherlecks zu vermeiden, indem Sie dafür sorgen, dass Objekte entsprechend ihrem Umfang definiert werden. Wenn Objekte länger als nötig beibehalten werden, indem sie an den falschen Lebenszyklus gebunden werden, kann dies zu Speicherlecks führen.
Bilder bewusst laden
Grafik-Bitmaps sind in der Regel die größten gängigen Objekte im Speicher Ihrer App. Auch wenn Sie mit komprimierten Dateien wie JPEGs arbeiten, muss die Datei in eine unkomprimierte Bitmap umgewandelt werden, um auf dem Bildschirm angezeigt zu werden. Eine kleine komprimierte Bilddatei kann zu einer sehr großen Bitmap werden.
Die meisten Bitmaps verwenden beispielsweise die Konfiguration ARGB_8888. Das bedeutet, dass für jedes Pixel 4 Byte Speicherplatz erforderlich sind – jeweils ein Byte für Rot, Grün, Blau und Alpha (Transparenz). Wenn Sie ein 100 KB großes JPEG in einer Ansicht mit 1.000 × 1.000 Pixeln anzeigen, benötigt die Bitmap 4 Byte für jedes der 1.000.000 Pixel, also insgesamt 4 MB Arbeitsspeicher.
Es gibt verschiedene Möglichkeiten, die Verwendung von Bildern zu optimieren. Wenn Sie beispielsweise Bibliotheken zum Laden von Bildern verwenden, können Sie Speicher freigeben, wenn er nicht benötigt wird. Informationen zum effizienten Umgang mit Bildern finden Sie unter Bitmap-Bilder optimieren.
Verfügbaren Arbeitsspeicher und Arbeitsspeichernutzung überwachen
Sie müssen die Probleme mit der Arbeitsspeichernutzung Ihrer App finden, bevor Sie sie beheben können. Der Memory Profiler in Android Studio hilft Ihnen, Speicherprobleme auf folgende Weise zu finden und zu diagnostizieren:
- Sehen Sie sich an, wie Ihre App im Laufe der Zeit Arbeitsspeicher zuweist. Der Speicher-Profiler zeigt ein Echtzeitdiagramm der Arbeitsspeichernutzung Ihrer App, der Anzahl der zugewiesenen Java-Objekte und des Zeitpunkts der automatischen Speicherbereinigung.
- Sie können automatische Speicherbereinigung-Ereignisse initiieren und einen Snapshot des Java-Heaps erstellen, während Ihre App ausgeführt wird.
- Arbeitsspeicherzuweisungen Ihrer App aufzeichnen, alle zugewiesenen Objekte prüfen, den Stacktrace für jede Zuweisung ansehen und zum entsprechenden Code im Android Studio-Editor springen.
Der Memory Profiler ist auch in die LeakCanary-Bibliothek zur Speicherleckerkennung eingebunden. Mit LeakCanary können Sie die Analyse von Speicherlecks vom Testgerät auf Ihren Entwicklungscomputer verlagern, was Ihren Workflow erheblich beschleunigen kann. Weitere Informationen finden Sie in den Versionshinweisen zu Android Studio.
Es gibt weitere Tools, mit denen Sie Speicherprobleme anhand von Daten von Nutzern diagnostizieren können, die Ihre Produktions-App ausführen:
- Mit Android Vitals LMK-Ereignisse (Low-Memory-Kill) erfassen
- Verwenden Sie den Profiling Manager, um Out of Memory-Fehler sowie anomales App-Verhalten zu verfolgen, das durch Speicherlecks verursacht werden kann.
Speicher als Reaktion auf Ereignisse freigeben
Android kann Speicherplatz von Ihrer App zurückfordern oder Ihre App bei Bedarf vollständig beenden, um Speicherplatz für kritische Aufgaben freizugeben. Weitere Informationen finden Sie unter Speicherverwaltung – Übersicht. Um den Systemspeicher weiter auszugleichen und zu vermeiden, dass der Systemprozess Ihrer App beendet werden muss, können Sie die Schnittstelle ComponentCallbacks2 in Ihren Activity-Klassen implementieren. Die bereitgestellte Callback-Methode onTrimMemory() benachrichtigt Ihre App über Lebenszyklus- oder Arbeitsspeicher-bezogene Ereignisse, die eine gute Gelegenheit für Ihre App darstellen, die Arbeitsspeichernutzung freiwillig zu reduzieren.
Wenn Sie Arbeitsspeicher freigeben, wird Ihre App möglicherweise seltener vom Low-Memory-Killer beendet.
Ihre Implementierung von onTrimMemory() sollte sich ausschließlich auf die Ereignisse TRIM_MEMORY_UI_HIDDEN und TRIM_MEMORY_BACKGROUND konzentrieren. Ab Android 14 liefert das System keine Benachrichtigungen mehr für die anderen, alten Konstanten. Diese Konstanten wurden in Android 15 offiziell eingestellt.)
TRIM_MEMORY_UI_HIDDEN: Dieses Signal gibt an, dass die Benutzeroberfläche Ihrer App nicht mehr im Blickfeld des Nutzers ist. Dieser Übergang bietet die Möglichkeit, erhebliche Speicherzuweisungen freizugeben, die ausschließlich an die Benutzeroberfläche gebunden sind, z. B. Bitmaps, Videowiedergabepuffer oder komplexe Animationsressourcen.TRIM_MEMORY_BACKGROUND: Dieses Signal gibt an, dass Ihr Prozess im Hintergrund ausgeführt wird und nun infrage kommt, beendet zu werden, um den globalen Arbeitsspeicherbedarf des Systems zu decken. Wenn Sie die Dauer verlängern möchten, in der sich Ihr Prozess im Cache-Zustand befindet, und die Anzahl der Kaltstarts der App reduzieren möchten, sollten Sie alle Ressourcen, die leicht rekonstruiert werden können, wenn der Nutzer seine Sitzung fortsetzt, aggressiv freigeben.
In diesem Codebeispiel wird gezeigt, wie Sie den onTrimMemory()-Callback implementieren, um auf verschiedene speicherbezogene Ereignisse zu reagieren:
Kotlin
import android.content.ComponentCallbacks2
// Other import statements.
class MainActivity : AppCompatActivity(), ComponentCallbacks2 {
// Other activity code.
/**
* Release memory when the UI becomes hidden or when system resources become low.
* @param level the memory-related event that is raised.
*/
override fun onTrimMemory(level: Int) {
if (level >= ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN) {
// Release memory related to UI elements, such as bitmap caches.
}
if (level >= ComponentCallbacks2.TRIM_MEMORY_BACKGROUND) {
// Release memory related to background processing, such as by
// closing a database connection.
}
}
}
Java
import android.content.ComponentCallbacks2;
// Other import statements.
public class MainActivity extends AppCompatActivity
implements ComponentCallbacks2 {
// Other activity code.
/**
* Release memory when the UI becomes hidden or when system resources become low.
* @param level the memory-related event that is raised.
*/
public void onTrimMemory(int level) {
if (level >= ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN) {
// Release memory related to UI elements, such as bitmap caches.
}
if (level >= ComponentCallbacks2.TRIM_MEMORY_BACKGROUND) {
// Release memory related to background processing, such as by
// closing a database connection.
}
}
}
Prüfen, wie viel Speicherplatz Sie benötigen
Damit mehrere Prozesse gleichzeitig ausgeführt werden können, legt Android ein festes Limit für die Heap-Größe fest, die jeder App zugewiesen wird. Das genaue Limit für die Heap-Größe variiert je nach Gerät und hängt davon ab, wie viel RAM insgesamt auf dem Gerät verfügbar ist. Wenn Ihre App die Heap-Kapazität erreicht und versucht, mehr Arbeitsspeicher zuzuweisen, löst das System eine OutOfMemoryError aus.
Um zu vermeiden, dass der Speicherplatz ausgeht, können Sie das System abfragen, um zu ermitteln, wie viel Heapspeicher auf dem aktuellen Gerät verfügbar ist. Sie können das System nach diesem Wert fragen, indem Sie getMemoryInfo() aufrufen. Dadurch wird ein ActivityManager.MemoryInfo-Objekt zurückgegeben, das Informationen zum aktuellen Speicherstatus des Geräts enthält, einschließlich des verfügbaren Speichers, des Gesamtspeichers und des Speicherschwellenwerts – des Speicherniveaus, bei dem das System beginnt, Prozesse zu beenden. Das ActivityManager.MemoryInfo-Objekt macht auch lowMemory verfügbar. Das ist ein einfacher boolescher Wert, der angibt, ob auf dem Gerät nur noch wenig Arbeitsspeicher verfügbar ist.
Das folgende Beispielcode-Snippet zeigt, wie Sie die Methode getMemoryInfo() in Ihrer App verwenden.
Kotlin
fun doSomethingMemoryIntensive() {
// Before doing something that requires a lot of memory,
// check whether the device is in a low memory state.
if (!getAvailableMemory().lowMemory) {
// Do memory intensive work.
}
}
// Get a MemoryInfo object for the device's current memory status.
private fun getAvailableMemory(): ActivityManager.MemoryInfo {
val activityManager = getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
return ActivityManager.MemoryInfo().also { memoryInfo ->
activityManager.getMemoryInfo(memoryInfo)
}
}
Java
public void doSomethingMemoryIntensive() {
// Before doing something that requires a lot of memory,
// check whether the device is in a low memory state.
ActivityManager.MemoryInfo memoryInfo = getAvailableMemory();
if (!memoryInfo.lowMemory) {
// Do memory intensive work.
}
}
// Get a MemoryInfo object for the device's current memory status.
private ActivityManager.MemoryInfo getAvailableMemory() {
ActivityManager activityManager = (ActivityManager) this.getSystemService(ACTIVITY_SERVICE);
ActivityManager.MemoryInfo memoryInfo = new ActivityManager.MemoryInfo();
activityManager.getMemoryInfo(memoryInfo);
return memoryInfo;
}
Beenden von Apps aufgrund von wenig Arbeitsspeicher überwachen
Vom Nutzer wahrgenommene Low-Memory-Kills (LMKs) treten auf, wenn der Systemspeicher kritisch niedrig wird. Wenn der Arbeitsspeicher knapp wird, beendet der Daemon lmkd (Low Memory Killer) Prozesse basierend auf ihrer oom_adj_score. Apps, die im Cache gespeichert sind oder einen Dienst ohne zugehörige Benutzeroberfläche (z. B. einen Job) ausführen, haben die höchsten Werte und werden zuerst beendet. Wenn der Arbeitsspeicher weiterhin kritisch niedrig ist, muss der Daemon Arbeitsspeicher von Prozessen mit einem oom_adj_score von 0 freigeben.
Da diese Punktzahl für sichtbare Apps reserviert ist, führt die Beendigung zu einem sofortigen, nicht sanften Prozessende. Für den Endnutzer sieht es so aus, als wäre die App abgestürzt. Oft werden dabei die standardmäßigen Mechanismen zum Speichern des Lebenszyklusstatus umgangen, was zu einem Verlust des Nutzerfortschritts führt.
Das Beenden von Vordergrundprozessen ist ein wichtiger Schwerpunkt bei Android Vitals, da es ein guter Indikator für Probleme mit der Speicherverwaltung ist. Eine LMK-Rate von über 1% deutet auf einen kritischen Bedarf an sofortigen Maßnahmen hin. Eine niedrige Rate ist jedoch nicht unbedingt ein Indikator für einen guten Zustand. Eine niedrige vom Nutzer wahrgenommene LMK-Rate kann bedeuten, dass der LMK-Daemon häufig Prozesse beendet, während sie im Hintergrund ausgeführt werden. Dies beeinträchtigt die Leistung beim Warmstart und die flüssige Ausführung von Multitasking. Daher empfehlen wir, die Best Practices für den Arbeitsspeicher unabhängig von Ihrem aktuellen LMK-Wert zu befolgen, um langfristige Stabilität und Geräteleistung zu gewährleisten.
ProfilingManager zum Erfassen von Arbeitsspeicherproblemen verwenden
Die Android-Plattform bietet ProfilingManager, eine erweiterte Observability-API, mit der Sie Nutzerdaten in der Produktionsumgebung basierend auf von Ihnen festgelegten Triggern erfassen können. So können Sie schwer reproduzierbare Speicherprobleme erkennen.
Zwei neue Trigger, die mit Android 17 eingeführt wurden, sind besonders nützlich, um Arbeitsspeicherprobleme zu erkennen:
TRIGGER_TYPE_OOMgibt an, dass die App eineOutOfMemoryErrorausgelöst hat. Sie wird beim nächsten Start der App nach dem Absturz ausgelöst, wenn sich die App für Profiling-Trigger registriert.TRIGGER_TYPE_ANOMALYwird ausgelöst, wenn das System ein anomales Verhalten der App erkennt. Dies kann unter anderem durch eine übermäßige Arbeitsspeichernutzung ausgelöst werden. Sie wird ausgelöst, nachdem die App eine übermäßige Arbeitsspeichernutzung aufweist, bevor das System Maßnahmen ergreift, um den betreffenden Prozess zu beenden. Wenn die App beispielsweise die in Android 17 eingeführten Arbeitsspeicherlimits überschreitet, werdenTRIGGER_TYPE_ANOMALYausgelöst, bevor das System die App beendet.
Weitere Informationen zur programmatischen Registrierung und zum Abrufen von Triggern mit ProfilingManager finden Sie in der Dokumentation zum triggerbasierten Profiling. Firebase Crashlytics unterstützt sowohl die Trigger TRIGGER_TYPE_OOM als auch TRIGGER_TYPE_ANOMALY und korreliert sie mit anderen diagnostischen Metadaten.
Sie können auch app-gesteuerte Profilerstellung verwenden, um Start- und End-Trace-Punkte manuell zu definieren. Wir empfehlen, dies zu tun, um Heap-Dumps oder Heap-Profile manuell in Bereichen zu erfassen, in denen Sie vermuten, dass es zu Speicherlecks oder übermäßiger Arbeitsspeichernutzung kommt.
Prozessstatus und Arbeitsspeicher im Blick behalten
Zusätzlich zu Speicher-Callbacks und ‑Ereignissen können Sie den Status und die Priorität der Prozesse Ihrer App explizit abfragen. Das ist besonders nützlich, um zu verstehen, wie das Betriebssystem den aktuellen Status Ihrer App klassifiziert (z. B. ob Ihre App im Vordergrund, für den Nutzer sichtbar oder im Cache ist) und ob sie Gefahr läuft, vom Low-Memory Killer beendet zu werden.
Mit einem Python-Dashboard für die kontinuierliche Überwachung
Verwenden Sie das Python-Skript Android Memory Monitor, während Ihre App ausgeführt wird, um die Prozesse und den Speicherbedarf Ihrer App kontinuierlich zu überwachen. Dieses Befehlszeilentool verwendet adb-Befehle, um Terminal- und Web-Dashboards anzuzeigen, mit denen Sie den Zustand und die Arbeitsspeichernutzung der Prozesse Ihrer App nachvollziehen können. Führen Sie das Script mit dem Paketnamen Ihrer App aus, um die Dashboards zu starten, z. B.:
python3 android_mem_monitor.py your.package.name
Eine detaillierte Anleitung zu den Dashboardfunktionen und zur Verwendung finden Sie in der README-Datei oder in den Inline-Kommentaren des Skripts.
Zur Laufzeit mit ActivityManager
Wenn Sie Informationen zu Ihren laufenden Prozessen zur Laufzeit abrufen möchten, können Sie ActivityManager.getRunningAppProcesses() abfragen. Dadurch wird eine Liste von Objekten mit grundlegenden Informationen zu den einzelnen App-Prozessen zurückgegeben. Das Feld importance gibt die relative Bedeutung an, die das System dem Prozess beimisst, und wird Konstanten wie den folgenden zugeordnet:
IMPORTANCE_FOREGROUND: Der Prozess führt die Benutzeroberfläche im Vordergrund aktiv aus.IMPORTANCE_FOREGROUND_SERVICE: Der Prozess führt einen Vordergrunddienst aus (z. B. aktive Hintergrundmusik oder große Downloads), was ein Beispiel für einen vom Nutzer wahrgenommenen Dienst ist.IMPORTANCE_CACHED: Dieser Prozess enthält im Cache gespeicherten Code. Der Prozess wird eingefroren und der nicht verwendete Arbeitsspeicher wird von Android zurückgefordert. In diesem Status beendet das BetriebssystemMemoryLimiterdie App nicht, wenn sie den app-spezifischen Arbeitsspeicher-Grenzwert überschreitet. Wenn jedoch der Arbeitsspeicher des Gesamtsystems knapp wird, ist die App ein guter Kandidat für den Low-Memory-Killer.
Debugging mit adb
Wenn Sie das Speicherverhalten und die Prozesslebenszyklen debuggen, können Sie auch adb-Befehle verwenden, um die OOM-Scores und den Speicherbedarf Ihrer App in Echtzeit zu untersuchen. Mit den folgenden Befehlen können Sie die Prozesse und Speicherstatus Ihrer App überwachen:
- Aktive Prozess-IDs (PIDs) finden: Suchen Sie zuerst mit dem Befehl
ps, der nach Ihrem Paketnamen gefiltert wird, nach allen Prozessen, die derzeit von Ihrer App ausgeführt werden.
shell adb shell ps -A | grep your.package.name - Aktuellen OOM-Score prüfen:Verwenden Sie die PID eines Prozesses, um seinen OOM-Score aus dem Prozessdateisystem zu lesen. Ein Wert zwischen 0 und 199 weist in der Regel auf eine App im Vordergrund hin, 200 bis 249 auf einen wahrnehmbaren Dienst, 250 bis 899 auf einen Hintergrunddienst und Werte ab 900 darauf, dass der Prozess im Cache gespeichert ist und wahrscheinlich beendet wird.
shell adb shell cat /proc/pid/oom_score_adj - OOM-Scores kontinuierlich überwachen:Mit dem Befehl
watch-uids
können Sie Änderungen am OOM-Score einer App (dem niedrigsten OOM-Score aller zugehörigen Prozesse) kontinuierlich überwachen.shell adb shell am watch-uids - Speicherbedarf messen:Wenn Sie schnell den wichtigsten Speicherbedarf eines Prozesses ermitteln möchten, z. B. Anonymous RSS und Swap-Speichernutzung, lesen Sie den Status direkt aus dem Linux-Prozessdateisystem.
shell adb shell cat /proc/pid/status | grep -E "VmRSS|RssAnon|VmSwap"
Eine detailliertere und umfassendere Aufschlüsselung der Speicherzuweisungen erhalten Sie mit dem Befehldumpsys meminfo:
shell adb shell dumpsys meminfo pid - Aktive Komponenten untersuchen:Wenn Ihr Prozess eine unerwartet hohe Priorität beibehält (z. B. einen OOM-Score von
200beibehält, anstatt in den Cache zu wechseln), können Sie genau untersuchen, was der Prozess ausführt. Mit dem Befehldumpsys activity processeswerden die spezifischen Aktivitäten, Dienste, Anbieter und Empfänger angezeigt, die den Prozess am Laufen halten.
shell adb shell dumpsys activity processes your.package.name
Speichereffizientere Codekonstrukte verwenden
Einige Android-Funktionen, Java-Klassen und Codekonstrukte benötigen mehr Arbeitsspeicher als andere. Sie können den von Ihrer App verwendeten Speicherplatz minimieren, indem Sie in Ihrem Code effizientere Alternativen auswählen.
Dienste nur sparsam nutzen
Wir empfehlen dringend, Dienste nicht unnötig im Hintergrund laufen zu lassen. Das Ausführen unnötiger Dienste ist einer der schlimmsten Fehler, die eine Android-App in Bezug auf die Speicherverwaltung machen kann. Wenn Ihre App einen Dienst benötigt, um im Hintergrund zu arbeiten, lassen Sie ihn nicht laufen, es sei denn, er muss eine Aufgabe ausführen. Beenden Sie den Dienst, wenn er seine Aufgabe abgeschlossen hat. Andernfalls kann es zu einem Speicherleck kommen.
Wenn Sie einen Dienst starten, versucht das System, den Prozess für diesen Dienst am Laufen zu halten. Dieses Verhalten macht Dienstprozesse sehr teuer, da der von einem Dienst verwendete RAM für andere Prozesse nicht verfügbar ist. Dadurch wird die Anzahl der im LRU-Cache gespeicherten Prozesse reduziert, die das System im Cache behalten kann, wodurch das Wechseln zwischen Apps weniger effizient wird. Bei wenig Arbeitsspeicher kann es sogar zu einem Thrashing des Systems kommen, wenn das System nicht genügend Prozesse für alle aktuell ausgeführten Dienste aufrechterhalten kann.
Im Allgemeinen sollten Sie persistente Dienste vermeiden, da sie den verfügbaren Speicher ständig belasten. Wir empfehlen stattdessen eine alternative Implementierung wie WorkManager.
Weitere Informationen zur Verwendung von WorkManager zum Planen von Hintergrundprozessen finden Sie unter Persistent work.
Optimierte Datencontainer verwenden
Einige der von der Programmiersprache bereitgestellten Klassen sind nicht für die Verwendung auf Mobilgeräten optimiert. Die generische Implementierung HashMap kann beispielsweise speicherineffizient sein, da für jede Zuordnung ein separates Eintragsobjekt erforderlich ist.
Das Android-Framework enthält mehrere optimierte Datencontainer, darunter SparseArray, SparseBooleanArray und LongSparseArray. Die SparseArray-Klassen sind beispielsweise effizienter, da das System den Schlüssel und manchmal auch den Wert nicht automatisch in ein Objekt verpacken muss. Dadurch werden ein oder zwei weitere Objekte pro Eintrag erstellt.
Bei Bedarf können Sie jederzeit zu Roh-Arrays wechseln, um eine schlanke Datenstruktur zu erhalten.
Vorsicht bei Code-Abstraktionen
Entwickler verwenden Abstraktionen häufig als gute Programmierpraxis, da sie die Flexibilität und Wartung des Codes verbessern können. Abstraktionen erfordern jedoch in der Regel mehr Code. Wie unter Code- und Ressourcen-Footprint Ihrer App reduzieren beschrieben, führt eine größere kompilierte Codebasis direkt zu einem höheren physischen RAM-Bedarf Ihrer App. Wenn Ihre Abstraktionen keinen wesentlichen Vorteil bieten, sollten Sie sie vermeiden.
Lite-Protokollpuffer für serialisierte Daten verwenden
Protocol Buffers (Protobufs) sind ein sprach- und plattformneutraler, erweiterbarer Mechanismus, der von Google für die Serialisierung strukturierter Daten entwickelt wurde – ähnlich wie XML, aber kleiner, schneller und einfacher. Wenn Sie Protobufs für Ihre Daten verwenden, sollten Sie immer Lite-Protobufs in Ihrem clientseitigen Code verwenden. Reguläre Protobufs generieren sehr ausführlichen Code, wodurch der Code-Footprint Ihrer App im RAM erhöht wird (siehe Code- und Ressourcen-Footprint Ihrer App reduzieren). Außerdem wird dadurch die APK-Größe erhöht.
Weitere Informationen finden Sie in der Protobuf-Readme-Datei.
Vorsicht vor Speicherlecks
Eine unsachgemäße Referenzverwaltung kann zu Speicherlecks führen, bei denen Objekte ihre Nutzungsdauer überschreiten und der Garbage Collector den Speicher des geleakten Objekts nicht freigeben kann. Um Speicherlecks zu vermeiden, sollten Sie ein lebenszyklusbewusstes Design implementieren.
Memory Churn vermeiden
Ereignisse der automatischen Speicherbereinigung wirken sich nicht auf die Leistung Ihrer App aus. Viele Garbage Collection-Ereignisse, die in kurzer Zeit auftreten, können jedoch schnell den Akku entladen und die Zeit zum Einrichten von Frames aufgrund der erforderlichen Interaktionen zwischen dem Garbage Collector und den App-Threads geringfügig verlängern. Je mehr Zeit das System mit der automatischen Speicherbereinigung verbringt, desto schneller entlädt sich der Akku.
Häufig kann Memory Churn dazu führen, dass eine große Anzahl von automatischen Speicherbereinigungen erfolgt. In der Praxis beschreibt der Arbeitsspeicher-Churn die Anzahl der zugewiesenen temporären Objekte, die in einem bestimmten Zeitraum auftreten.
Beispielsweise können Sie mehrere temporäre Objekte in einer for-Schleife zuweisen.
Alternativ können Sie neue Paint- oder Bitmap-Objekte in der Funktion onDraw() einer Ansicht erstellen. In beiden Fällen erstellt die App schnell eine große Anzahl von Objekten. Diese können schnell den gesamten verfügbaren Speicher in der jungen Generation belegen und eine automatische Speicherbereinigung erzwingen.
Mit dem Speicher-Profiler können Sie die Stellen in Ihrem Code finden, an denen die Memory Churn hoch ist, bevor Sie sie beheben.
Nachdem Sie die Problembereiche in Ihrem Code identifiziert haben, sollten Sie versuchen, die Anzahl der Zuweisungen in leistungsrelevanten Bereichen zu reduzieren. Verschieben Sie Elemente aus inneren Schleifen oder in eine fabrikbasierte Zuweisungsstruktur.
Sie können auch prüfen, ob Objektpools für den Anwendungsfall von Vorteil sind. Bei einem Objektpool wird eine Objektinstanz nicht auf den Boden fallen gelassen, sondern in einen Pool freigegeben, wenn sie nicht mehr benötigt wird. Wenn das nächste Mal eine Objektinstanz dieses Typs benötigt wird, können Sie sie aus dem Pool abrufen, anstatt sie zuzuweisen.
Bewerten Sie die Leistung gründlich, um festzustellen, ob ein Objektpool in einer bestimmten Situation geeignet ist. Es gibt Fälle, in denen sich Objektpools negativ auf die Leistung auswirken können. Auch wenn Pools Zuweisungen vermeiden, führen sie zu anderen Mehraufwendungen. Die Verwaltung des Pools erfordert beispielsweise in der Regel eine Synchronisierung, die einen nicht unerheblichen Overhead verursacht. Außerdem kann das Löschen der gepoolten Objektinstanz, um Speicherlecks während der Freigabe zu vermeiden, und die anschließende Initialisierung während der Erfassung einen gewissen Overhead verursachen.
Wenn Sie mehr Objektinstanzen im Pool zurückhalten als nötig, wird auch die automatische Speicherbereinigung belastet. Objektpools reduzieren zwar die Anzahl der Garbage Collection-Aufrufe, erhöhen aber den Aufwand für jeden Aufruf, da er proportional zur Anzahl der aktiven (erreichbaren) Byte ist.