Android Coden
Android 7 min lesen

Speicherverwaltung in Android

Du lernst, wie Android-Speicher funktioniert und wie du Leaks, unnötige Allocations und Heap-Probleme vermeidest.

Memory Management klingt zuerst nach einem Detail für Systemprogrammierer. In Android ist es aber Alltag: Jede Activity, jedes ViewModel, jede Liste, jedes Bild und jede Compose-UI belegt Speicher. Wenn du alte Objekte festhältst oder zu viele Daten auf einmal lädst, wird deine App langsam, ruckelig oder sie stürzt mit einem OutOfMemoryError ab. Gute Speicherverwaltung heißt deshalb nicht, dass du manuell Speicher freigibst. Es heißt, dass du Referenzen bewusst setzt, Datenmengen begrenzt und den Lebenszyklus deiner App respektierst.

Was ist das?

Memory Management beschreibt, wie deine App mit Arbeitsspeicher umgeht. In Kotlin auf Android erzeugst du Objekte meist durch Klassen, Datenklassen, Collections, Lambdas, Bilder oder UI-Zustand. Diese Objekte landen auf dem Heap, also in dem Speicherbereich, den die Laufzeitumgebung für dynamische Objekte nutzt. Die Garbage Collection räumt Objekte automatisch auf, sobald sie nicht mehr erreichbar sind.

Der wichtige Punkt ist: Automatisch bedeutet nicht fehlerfrei aus Entwicklersicht. Wenn du eine Referenz auf ein Objekt behältst, gilt es weiterhin als erreichbar. Dann kann die Garbage Collection es nicht entfernen, auch wenn es fachlich längst veraltet ist. Genau hier entstehen Leaks. Ein Leak ist kein sichtbares Loch im Speicher, sondern eine unnötig lebende Referenzkette.

Im Android-Kontext ist das besonders relevant, weil Komponenten kurze und wechselnde Lebenszyklen haben. Eine Activity kann durch Rotation neu erstellt werden. Eine Composable kann häufig neu zusammengesetzt werden. Ein Fragment-View kann zerstört sein, während das Fragment selbst noch lebt. Ein ViewModel kann länger leben als der Screen. Wenn du diese Grenzen ignorierst, hältst du schnell alte Views, Context-Objekte oder große Datenlisten im Speicher.

Speicherverwaltung gehört in der Roadmap zu Performance, Barrierefreiheit, Datenschutz und Sicherheit, weil Speicherprobleme direkt auf Nutzbarkeit wirken. Eine App, die wegen Speicherlast ruckelt, reagiert schlechter auf Eingaben und kann auch assistive Technologien beeinträchtigen. Eine App, die sensible Daten länger als nötig im Speicher hält, ist außerdem aus Datenschutzsicht schwächer. Der Fokus hier bleibt aber klar: Vermeide veraltete Referenzen und lade keine übermäßig großen Datenmengen in den Heap.

Wie funktioniert es?

Baue dir zuerst ein einfaches mentales Modell: Der Heap enthält Objekte. Variablen, Properties, Callbacks, Coroutines und Collections können auf diese Objekte zeigen. Solange es einen erreichbaren Pfad von einer aktiven Wurzel zu einem Objekt gibt, bleibt dieses Objekt am Leben. Wurzeln sind zum Beispiel laufende Threads, statische Objekte, globale Singletons oder aktive Android-Komponenten.

Ein klassisches Problem entsteht, wenn ein langlebiges Objekt ein kurzlebiges Objekt speichert. Ein Repository, Singleton oder Manager lebt oft während der gesamten App-Sitzung. Wenn es eine Activity, ein View oder einen Callback mit Activity-Bezug speichert, lebt diese Activity ebenfalls weiter. Das ist meistens falsch. Die Activity sollte nach onDestroy() verschwinden können.

Allocations sind die zweite Seite des Themas. Eine Allocation ist das Anlegen eines neuen Objekts im Speicher. Einzelne Allocations sind normal. Problematisch wird es, wenn du in sehr häufigen Pfaden ständig neue Objekte erzeugst: während Scrollen, Animationen, Compose-Recompositions oder Bildverarbeitung. Dann muss die Garbage Collection öfter laufen. Das kann kurze Pausen verursachen, die Nutzer als Ruckeln wahrnehmen.

In moderner Android-Entwicklung triffst du Memory Management in mehreren Bereichen. In Jetpack Compose speicherst du UI-Zustand mit remember, rememberSaveable, ViewModel-State oder Flows. In Architecture Components schützt dich ein ViewModel davor, Daten bei Konfigurationsänderungen sofort zu verlieren. Gleichzeitig darf ein ViewModel keine Activity oder View direkt halten. In Coroutines musst du darauf achten, dass Jobs an passende Scopes gebunden sind. Eine Coroutine in viewModelScope darf keine alte View aktualisieren. Ein Flow sollte mit dem Lebenszyklus gesammelt werden, damit keine unnötige Arbeit im Hintergrund läuft.

Auch Bilder sind ein häufiger Speicherfaktor. Ein hochauflösendes Bild kann deutlich mehr Heap belegen als seine Dateigröße vermuten lässt, weil es dekodiert als Pixelmatrix im Speicher liegt. Deshalb nutzt du für Bildlisten etablierte Libraries, passende Größen und Caching-Strategien. Du lädst nicht zehn Megabyte große Originale, wenn die UI nur ein kleines Thumbnail zeigt.

Datenschutz und Berechtigungen berühren das Thema, sobald du mit Kontakten, Standort, Dateien, Kamera oder anderen sensiblen Daten arbeitest. Du solltest Daten nur so lange halten, wie sie für den aktuellen Zweck nötig sind. Speicherverwaltung ist hier keine vollständige Sicherheitsstrategie, aber sie unterstützt das Prinzip der Datenminimierung.

In der Praxis

Ein typischer Fehler ist ein Callback, der eine Activity indirekt festhält. Stell dir vor, du baust einen einfachen Tracker oder Manager als Singleton und registrierst aus einer Activity einen Listener. Wenn du den Listener nie entfernst, bleibt die Activity über den Listener erreichbar.

object UploadStatusBus {
    private val listeners = mutableSetOf<(UploadState) -> Unit>()

    fun addListener(listener: (UploadState) -> Unit) {
        listeners += listener
    }

    fun removeListener(listener: (UploadState) -> Unit) {
        listeners -= listener
    }

    fun publish(state: UploadState) {
        listeners.forEach { it(state) }
    }
}

class UploadActivity : AppCompatActivity() {

    private val listener: (UploadState) -> Unit = { state ->
        findViewById<TextView>(R.id.statusText).text = state.label
    }

    override fun onStart() {
        super.onStart()
        UploadStatusBus.addListener(listener)
    }

    override fun onStop() {
        UploadStatusBus.removeListener(listener)
        super.onStop()
    }
}

Das Beispiel ist bewusst klein. Die Regel dahinter ist wichtiger als die konkrete API: Wer sich bei einem langlebigen Objekt registriert, muss sich passend zum Lebenszyklus wieder abmelden. Noch besser ist oft eine Architektur, bei der die UI State aus einem ViewModel beobachtet und die Sammlung lebenszyklusbewusst passiert.

In Compose sieht ein ähnlicher Gedanke so aus: Sammle Flows nicht dauerhaft ohne Lebenszyklusbezug, sondern nutze passende Lifecycle-APIs aus dem Android-Umfeld, wenn deine App sie verwendet.

@Composable
fun UploadScreen(viewModel: UploadViewModel) {
    val state by viewModel.state.collectAsStateWithLifecycle()

    Text(text = state.label)
}

Hier hält die Composable keine Activity in einem Singleton fest. Sie liest Zustand aus dem ViewModel. Das ViewModel wiederum sollte keine Activity, kein View und keinen kurzlebigen Context speichern. Wenn ein Context nötig ist, prüfe zuerst, ob du ihn wirklich brauchst. Für Ressourcen gibt es oft bessere Wege. Für appweite Dienste kann der Application-Context passend sein, aber auch den solltest du nicht als Ausrede für unklare Abhängigkeiten verwenden.

Eine praktische Entscheidungsregel lautet: Je länger ein Objekt lebt, desto misstrauischer solltest du gegenüber Referenzen auf UI-Objekte sein. Ein Repository darf Datenquellen kennen, aber keine Activity. Ein ViewModel darf UI-State halten, aber keine View. Eine Composable darf temporären UI-Zustand halten, aber keine großen Ergebnislisten erzeugen, wenn dafür Paging oder ein ViewModel-State besser passt.

Achte außerdem auf Allocations in heißen Pfaden. Ein Beispiel ist eine Liste, in der du bei jeder Recomposition teure Mappings ausführst:

@Composable
fun ContactList(contacts: List<Contact>) {
    val sortedContacts = remember(contacts) {
        contacts.sortedBy { it.name.lowercase() }
    }

    LazyColumn {
        items(
            items = sortedContacts,
            key = { it.id }
        ) { contact ->
            Text(text = contact.name)
        }
    }
}

Ohne remember(contacts) würde die Sortierung bei jeder passenden Recomposition erneut laufen und neue Listen erzeugen. Das ist nicht immer ein Problem, aber bei großen Datenmengen oder häufigen UI-Updates wird es teuer. Noch besser kann es sein, die Sortierung außerhalb der UI-Schicht zu erledigen, wenn sie fachlich zum State gehört.

Bei großen Datenmengen solltest du nicht alles in eine List laden, nur weil es bequem ist. Für Datenbanken, Netzwerklisten und Suchergebnisse sind Paging, begrenzte Queries und serverseitige Filter oft die bessere Wahl. Der Heap ist kein Lagerraum für alle Daten, die deine App theoretisch anzeigen könnte. Er ist Arbeitsfläche für das, was gerade gebraucht wird.

Typische Stolperfallen sind statische Caches ohne Limit, globale Listen, nicht entfernte Listener, falsch gebundene Coroutines, große Bitmaps in ViewModels und unnötige Kopien von Collections. Auch Logging kann Speicher kosten, wenn du riesige Strings zusammensetzt oder sensible Daten länger speicherst, als es für Diagnose nötig ist.

Prüfen kannst du dein Verständnis mit dem Android Studio Memory Profiler. Öffne einen Screen, rotiere das Gerät mehrfach, navigiere zurück und erzwinge Garbage Collection. Wenn alte Activity-Instanzen weiterhin auftauchen, hast du wahrscheinlich eine Referenzkette, die du untersuchen musst. Ergänze das durch Code-Reviews: Frage bei jeder langlebigen Klasse, welche Objekte sie hält und ob deren Lebensdauer dazu passt. Tests können zusätzlich prüfen, dass Scopes korrekt beendet und Ressourcen geschlossen werden, auch wenn sie nicht jeden Heap-Fall automatisch finden.

Fazit

Memory Management in Android ist die Fähigkeit, Lebensdauer, Datenmenge und Referenzen bewusst zu gestalten. Du musst nicht gegen die Garbage Collection arbeiten, sondern ihr erlauben, alte Objekte loszuwerden. Halte UI-Objekte nicht in langlebigen Klassen, begrenze große Datenmengen, vermeide unnötige Allocations in häufigen Pfaden und prüfe auffällige Screens mit Profiler, Debugger und Code-Review. Nimm dir als Übung einen Screen mit Liste, Navigation und ViewModel: rotiere das Gerät, verlasse den Screen, beobachte den Heap und erkläre dir anschließend, warum jedes verbleibende größere Objekt noch lebt.

Quellen (5)
Redaktion

Geschrieben von

Redaktion

Das Redaktionsteam recherchiert und schreibt Artikel zu aktuellen Themen rund um Tech, Lifestyle und Ratgeber.