Memory leak e soluzioni
Individui e arresti le perdite di memoria
Memory leak e soluzioni è una lezione Android Academy gratuita su CoddyKit. Questa è la lezione 3 di 4. Puoi leggere la lezione completa qui gratuitamente — poi esercitati direttamente nel browser con un editor di codice integrato e un tutor IA disponibile 24/7. Fa parte del percorso di apprendimento Android Academy, e i tuoi progressi si sincronizzano tra il web e l'app CoddyKit. Il corso Android Academy include 4 lezioni in totale.
Che cos'è una perdita di memoria?
Una perdita di memoria si verifica quando gli oggetti non più necessari non possono essere raccolti dal garbage collector perché qualcosa conserva ancora un riferimento a essi. Su Android, il caso classico è un Activity o un Context che rimane in memoria dopo la distruzione della relativa schermata.
Le perdite fanno crescere l'heap nel tempo, provocano garbage collection più frequenti (che sospendono l'interfaccia e causano jank) e alla fine possono arrestare l'app con un OutOfMemoryError. In questa lezione imparerà a individuarle e correggerle.
Come il GC decide cosa conservare
Il garbage collector di Android conserva ogni oggetto raggiungibile da una radice del GC (thread attivi, campi statici e così via). Tutto ciò che non è raggiungibile viene liberato.
Una perdita è semplicemente un percorso di raggiungibilità indesiderato: un oggetto di lunga durata conserva un riferimento a un oggetto di breve durata. La dimostrazione in puro Kotlin qui sotto mostra come la raggiungibilità mantenga in vita un oggetto.
object GlobalCache {
val items = mutableListOf<ByteArray>()
}
fun cacheSomething() {
// This 1MB array stays alive forever because GlobalCache
// (a static singleton, a GC root) keeps referencing it.
GlobalCache.items.add(ByteArray(1_000_000))
}
fun main() {
repeat(3) { cacheSomething() }
println("Held arrays: ${GlobalCache.items.size}") // never freed
}Il classico: perdita di un’Activity
La perdita di memoria più comune su Android è causata da un oggetto statico o di lunga durata che mantiene un Context. Quando l’Activity viene distrutta (rotazione, navigazione), il sistema vuole liberarla, ma il riferimento statico la mantiene in vita per sempre.
Il codice seguente causa una perdita: un singleton memorizza il Context di un’Activity.
// LEAK: static field holds an Activity Context
object Analytics {
var context: Context? = null // <- keeps Activity alive
fun init(ctx: Context) { context = ctx }
}
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Analytics.init(this) // passing the Activity -> leak on rotation
}
}Soluzione: usare il Context dell’applicazione
Quando è necessario memorizzare un Context in qualcosa di lunga durata, memorizzate il Context dell’applicazione, che vive per l’intero processo ed è sicuro da mantenere.
Mantenete un Context di Activity/View solo finché quella schermata esiste. Questa semplice regola previene la maggior parte delle perdite di memoria su Android.
object Analytics {
private var appContext: Context? = null
fun init(ctx: Context) {
// applicationContext is process-scoped and safe to keep
appContext = ctx.applicationContext
}
}
// Call site is now leak-free:
Analytics.init(this) // stores applicationContext, not the ActivityClassi interne e Handler
Una classe interna non statica (inclusa la maggior parte dei listener, dei Runnable e dei callback di Handler) mantiene un riferimento implicito alla classe esterna. Se quell’attività dura più della schermata, la schermata rimane allocata.
Un Handler.postDelayed ritardato è spesso il responsabile: il messaggio in attesa mantiene in vita l’Activity finché non viene eseguito.
// LEAK: the posted Runnable holds the Activity for 60 seconds
handler.postDelayed({ updateUi() }, 60_000)
// FIX: cancel pending work when the screen goes away
override fun onDestroy() {
super.onDestroy()
handler.removeCallbacksAndMessages(null)
}Coroutine: definite l’ambito del lavoro
Una coroutine che sopravvive alla schermata mantiene in memoria tutto ciò che cattura. La soluzione è la concorrenza strutturata: avviate il lavoro in uno scope legato al ciclo di vita, in modo che venga annullato automaticamente.
In un ViewModel usate viewModelScope; in un controller dell’interfaccia utente usate lifecycleScope. Quando il proprietario viene distrutto, lo scope annulla il lavoro e i riferimenti vengono rilasciati.
class FeedViewModel : ViewModel() {
fun load() {
// Cancelled automatically when the ViewModel is cleared
viewModelScope.launch {
val feed = repository.fetchFeed()
_state.value = feed
}
}
}
// In Compose, collect tied to the lifecycle:
val state by viewModel.state.collectAsStateWithLifecycle()DisposableEffect in Compose
In Compose, qualsiasi elemento che registrate deve essere annullato quando il composable viene rimosso. DisposableEffect fornisce un blocco onDispose proprio per questo: listener, observer, sensori e broadcast receiver.
Dimenticare la pulizia in questo punto mantiene in memoria il callback e tutto ciò che cattura.
@Composable
fun LocationDisplay(manager: LocationManager) {
DisposableEffect(manager) {
val listener = LocationListener { /* update */ }
manager.register(listener)
onDispose { manager.unregister(listener) } // prevents the leak
}
}Individuare le perdite con LeakCanary
LeakCanary è lo strumento standard per individuare automaticamente le perdite nelle build di debug. Aggiungete la dipendenza: lo strumento monitora gli oggetti distrutti e, se uno di essi non viene raccolto dal garbage collector, acquisisce un heap dump e mostra l’esatta catena di riferimenti che lo mantiene in vita.
Quella catena indica la soluzione: conduce direttamente al campo o al listener responsabile.
// build.gradle.kts (app module)
dependencies {
debugImplementation("com.squareup.leakcanary:leakcanary-android:2.14")
}
// No code needed: on a debug build, navigate away from a screen and
// LeakCanary posts a notification with the leak trace, e.g.:
// Analytics.context -> MainActivity (leaked)Heap dump nel Profiler
Per le perdite che LeakCanary non segnala (crescita lenta, memoria nativa, cache di grandi dimensioni), usate il Memory Profiler. Acquisite un heap dump, forzate il GC e controllate quali classi hanno un numero di istanze sorprendentemente elevato.
Un numero crescente di istanze della vostra Activity, del Fragment o del ViewModel dopo aver lasciato la schermata è una prova evidente. Il profiler mostra anche il percorso fino alle radici del GC, così potete seguire il riferimento.
Bitmap e allocazioni di grandi dimensioni
Non tutti i problemi di memoria sono perdite di riferimenti. Le bitmap di grandi dimensioni e le cache senza limiti possono esaurire l’heap autonomamente. Una foto a risoluzione completa può occupare decine di megabyte in memoria.
Lasciate che una libreria per immagini (Coil/Glide) riduca la risoluzione alle dimensioni visualizzate e imponete un limite esplicito a qualsiasi cache creiate. Lo snippet mostra una cache LRU con dimensioni limitate.
// Bound memory: keep at most ~1/8 of available app memory
val maxKb = (Runtime.getRuntime().maxMemory() / 1024 / 8).toInt()
val bitmapCache = object : LruCache<String, Bitmap>(maxKb) {
override fun sizeOf(key: String, value: Bitmap) = value.byteCount / 1024
}
// An unbounded HashMap<String, Bitmap> would grow until OutOfMemoryError.Checklist per prevenire le perdite
Adottate queste abitudini e la maggior parte delle perdite non si verificherà:
- Memorizzate applicationContext negli oggetti di lunga durata, mai un’Activity/View.
- Eseguite il lavoro asincrono in una coroutine con scope legato al ciclo di vita (
viewModelScope,lifecycleScope). - Effettuate sempre l’unregister di listener e receiver (
onDispose,onDestroy). - Annullate gli Handler ritardati e i timer.
- Imponete un limite alle cache e riducete la risoluzione delle bitmap.
- Mantenete LeakCanary nelle build di debug e intervenite seguendo le sue tracce.
Verifica rapida
Dovete usare un oggetto singleton per mantenere un Context per tutta la sua durata. Quale Context dovrebbe mantenere per evitare di trattenere una schermata?
Riepilogo: mantenere pulita la memoria
Avete imparato che cosa sono le perdite di memoria e come prevenirle:
- Le perdite sono percorsi di raggiungibilità indesiderati che mantengono in vita oggetti non più utilizzati.
- La perdita classica è un riferimento statico o di lunga durata a un’Activity/Context: usate invece applicationContext.
- I listener delle classi interne, gli Handler ritardati e le coroutine senza scope trattengono il proprietario: definite il loro scope e annullateli.
- In Compose usate
DisposableEffectper annullare la registrazione dei callback. - LeakCanary e il Memory Profiler mostrano la catena di riferimenti.
- Imponete un limite alle cache e riducete la risoluzione delle bitmap.
Prossimo argomento: avviare rapidamente l’app con l’ottimizzazione dell’avvio e i baseline profile.
Domande Frequenti
La lezione «Memory leak e soluzioni» è gratuita?
Sì — il testo completo di «Memory leak e soluzioni» è gratuito qui sul web. Per esercitarvi in modo interattivo (un editor di codice integrato e un tutor IA 24/7) e sbloccare il resto del corso Android Academy, passa a CoddyKit PRO. Il corso Android Academy include 4 lezioni in totale.
Cosa imparerò in «Memory leak e soluzioni»?
Individui e arresti le perdite di memoria Eserciti Android Academy con codice pratico che esegui direttamente nel browser, e un tutor IA 24/7 risponde alle tue domande mentre lavori sulla lezione.
Ho bisogno di esperienza per iniziare Android Academy?
Non è richiesta alcuna esperienza precedente. Android Academy su CoddyKit è strutturato per principianti e studenti avanzati, quindi puoi iniziare da qui o dall'inizio e procedere al tuo ritmo. Questa è la lezione 3 di 4.
Quanto tempo richiede la lezione «Memory leak e soluzioni»?
La maggior parte delle lezioni CoddyKit richiede circa 5–10 minuti. Ogni lezione è breve e interattiva, quindi fai progressi costanti e riprendi esattamente da dove hai lasciato su web e app.
Posso scrivere ed eseguire codice in questa lezione Android Academy?
Sì. Ogni lezione Android Academy include un editor di codice integrato, quindi scrivi ed esegui codice reale direttamente nel tuo browser e ricevi feedback istantaneo dall'IA — nessuna configurazione locale necessaria.
Tutte le lezioni di questo corso
- Misurare le prestazioni
- Domare la ricomposizione
- Memory leak e soluzioni
- Avvio e profili baseline