Mantieni il footprint della memoria della tua app snello, efficiente e privo di errori di segmentazione con Firebase Crashlytics. Questa sessione esplora la nostra suite completa di strumenti per la risoluzione dei problemi di memoria disponibili nella console Firebase e in Android Studio. Inizieremo spiegando come sfruttare gli approfondimenti sugli ANR e i report GWP-ASan per diagnosticare bug di memoria difficili da individuare, per poi passare ai nostri strumenti più recenti per gli eventi di memoria esaurita (OOM) e Memory Limiter Kill (MLK), progettati specificamente per aiutarti a navigare e adattarti ai nuovi limiti di memoria introdotti in Android 17.
ApplicationExitInfo per mostrare le tracce multithread e identificare i thread di blocco principali.ProfilingManager di Android 17 per raccogliere istantanee heap deoffuscate che possono essere ispezionate in Perfetto o tramite la CLI Android.Trascrizione
Polina Govorkova
Ciao,
Ti diamo il benvenuto nella risoluzione dei problemi relativi alla memoria con Firebase Crashlytics.
Aspetta, che titolo aziendale.
Cambiamo un po' le cose.
Keeping apps lean and mean with Firebase Crashlytics.
Oh, sembra meglio.
Mi chiamo Polina, sono una sviluppatrice software senior del team di Firebase Crashlytics e oggi sarò la vostra relatrice.
A un certo punto del ciclo di sviluppo, avrai l'impressione di aver risolto tutti i bug.
Gli errori di battitura nell'analisi JSON, i controlli dei puntatori null dimenticati, tutto sparito.
E potresti chiederti: "Ho finito?"
Beh, ho una brutta notizia per te, perché non è così.
Perché ciò che vi rimarrà sono, in realtà, i problemi difficili.
E oggi sostengo che le due categorie di problemi più difficili che dovrete affrontare saranno la reattività e la memoria.
La buona notizia è che puoi affidarti a noi.
Oggi esamineremo gli strumenti esistenti e quelli nuovi di Android e Crashlytics per aiutarti a individuare la causa principale dei problemi di reattività che si manifestano tramite errori di tipo "L'applicazione non risponde", nonché eccezioni di out of memory, perdite di memoria e danneggiamento della memoria nativa.
Partiamo.
Perché i problemi di reattività e memoria sono così noti?
Prendiamo un bug classico: dichiari una stringa, la imposti su null e, a un certo punto, provi a leggerne la lunghezza.
Che cosa ricevi?
Una classica NullPointerException.
Ma in realtà qui la vita è facile, perché l'analisi dello stack delle eccezioni indica la riga esatta di codice che ha causato l'interruzione dell'app.
Guarda l'analisi dello stack a sinistra.
java.lang.NullPointerException
at MainActivity.onCreate(MainActivity.kt:42)
at android.app.Activity.performCreate
at android.os.Handler.dispatchMessage
at android.os.Looper.loopOnce
at android.app.ActivityThread.mainPossiamo capire subito che il problema si trova proprio lì, alla riga 42 del file MainActivity.kt.
Questo approccio è valido per la maggior parte dei bug banali.
Ora, quando si verifica un problema di reattività o di memoria, come questo errore "L'applicazione non risponde" sulla destra, l'analisi dello stack non indica quasi mai il vero colpevole.
"main" prio=5 tid=1 Blocked
| group="main" sCount=1 dsCount=0 flags=1
| state=S schedstat=( 1450239 48192 145 )
at DataManager.getData(DataManager.kt:25)
- waiting to lock <0x0a1b> held by thread 12
at MainActivity.onResume(MainActivity.kt:50)
at android.app.Activity.performResume
at android.os.Handler.dispatchMessage
at android.os.Looper.loopOnce
at android.app.ActivityThread.mainNel migliore dei casi, mostra solo l'effetto, ovvero un thread o un'allocazione innocente che era in esecuzione nel momento in cui l'app si è bloccata o ha esaurito la memoria.
Tutto questo mentre la vera causa principale si è verificata molto prima o, forse, in un altro luogo.
Per fortuna, quando si tratta di questi problemi, non devi tirare a indovinare, perché Firebase Crashlytics ha la risposta per noi.
Ma prima di rimboccarci le maniche con la risoluzione dei problemi, alzate la mano se conoscete Crashlytics.
Oh, fantastico.
Mi fa piacere vedere tutte queste mani alzate.
Per chi non lo sapesse, Firebase Crashlytics è una soluzione di Error Reporting in tempo reale che fornisce dati sul campo, oltre a insight contestuali e basati sull'AI, suggerimenti per la risoluzione dei problemi e un segnale chiaro sulle nuove release, il tutto per aiutarti a risolvere i problemi più velocemente.
Crashlytics è molto apprezzato dai nostri sviluppatori, in quanto è la soluzione di reporting degli arresti anomali più adottata su dispositivi mobili, che supporta oltre 370.000 app native, tra cui il 57% delle principali app per Android.
Per vedere come Crashlytics ci aiuta a individuare i bug in azione, diamo un'occhiata a un esempio concreto.
Oggi voglio raccontarvi la storia di ChromaClip.
Si tratta di un estrattore di tavolozze di colori per video che ho creato e voglio raccontarvi come ho affrontato i bug di reattività e memoria durante lo sviluppo.
L'idea alla base di ChromaClip è semplice: gli utenti scelgono un video, ad esempio un bellissimo tramonto o una scena con un'ottima gradazione dei colori, e l'app analizza i frame per generare una tavolozza di colori personalizzata, che gli utenti possono poi utilizzare per i loro progetti, l'arte digitale, le moodboard e così via.
La nostra storia inizia poco prima della mia release 1.0.
Volevo assicurarmi che fosse solida come una roccia prima del lancio, quindi ho eseguito i test finali in locale su un paio di dispositivi, scegliendo il video, toccando il pulsante per estrarre la tavolozza dei colori e, per mia sfortuna, la finestra di dialogo preferita di ogni sviluppatore Android, non è vero?
ChromaClip non risponde.
Si tratta di un errore di tipo L'applicazione non risponde, altrimenti noto come ANR.
Polina Govorkova
Quindi, cosa facciamo in questa situazione, oltre a farci prendere dal panico?
Seguendo il nostro primo istinto, dato che lo smartphone di test è collegato direttamente al laptop, apriamo Android Studio e controlliamo Logcat.
Vediamo l'analisi dello stack per il thread principale.
14:32:15.105 1205-1250/system_process E/ActivityManager: ANR in com.chromaclip.app (com.chromaclip.app/.MainActivity)
PID: 14205
Reason: Input dispatching timed out
Dumped ANR trace to /data/anr/traces.txt
"main" prio=5 tid=1 Native
| group="main" sCount=1 dsCount=0 flags=1 obj=0x72a8c3d0 self=0x759fb92000
| sysTid=14205 nice=-10 cgrp=default sched=0/0 handle=0x7623910548
| state=S schedstat=( 1450239120 4819203 1453 ) utm=132 stm=13 core=4 HZ=100
| stack=0x7fed2a0000-0x7fed2a2000 stackSize=8MB
at sun.misc.Unsafe.park(Native method)
- waiting on an unknown object
at java.util.concurrent.locks.LockSupport.park(LockSupport.java:190)
at kotlinx.coroutines.EventLoopImplBase.processNextEvent(EventLoop.common.kt:279)
at kotlinx.coroutines.BlockingCoroutine.joinBlocking(Builders.kt:85)
at kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking(Builders.kt:59)
at kotlinx.coroutines.BuildersKt.runBlocking(Unknown Source:1)
at com.chromaclip.app.extractor.PaletteExtractor.extractColors(PaletteExtractor.kt:42)Cosa ci dice, in realtà?
Innanzitutto, in alto, abbiamo la conferma che si tratta di un ANR, in esecuzione in MainActivity.
Che altro?
Vediamo il motivo, ovvero il timeout dell'invio dell'input.
Ciò significa che l'utente ha toccato lo schermo.
Sì, l'abbiamo fatto.
Tuttavia, il thread principale era così impegnato che non è riuscito a elaborare l'evento entro il timeout di cinque secondi del watchdog di Android.
Inoltre, vediamo che il sistema ha scaricato il file in traces.txt e, subito sotto, logcat stampa l'analisi dello stack per il nostro thread principale.
Nota che lo stato è uguale a S.
Il nostro thread principale non sta nemmeno consumando attivamente la CPU.
S sta per "sleeping" (dormire).
Perché il nostro thread principale è in stato di sospensione mentre aspettiamo?
Diamo un'occhiata più da vicino agli stack frame.
Proprio qui, al centro della traccia, vediamo che il nostro thread principale è parcheggiato all'interno di Unsafe.park in attesa di un oggetto sconosciuto.
Perché non sappiamo cosa sta aspettando?
In realtà è vero che per alcuni ANR, ad esempio quando un thread è bloccato da un classico blocco sincronizzato Java, l'analisi dello stack del thread principale è abbastanza chiara da dirci cosa sta effettivamente aspettando, cosa sta trattenendo il blocco.
Ma quando utilizzi primitive di concorrenza di livello superiore, tutto ciò che logcat ci dice è che siamo in attesa di un oggetto sconosciuto.
Quindi, non abbiamo idea di quale oggetto stiamo aspettando, di quale thread di lavoro in background stiamo aspettando nello specifico o del motivo per cui ci vogliono più di 5 secondi.
E poiché stiamo eseguendo i test localmente, potremmo estrarre il traces.txt del dispositivo e scorrere manualmente decine di thread inattivi, per poi magari dare la caccia al colpevole o forse consumare i token chiedendo all'AI.
In alcuni casi, funzionerebbe davvero e, in effetti, funzionerebbe per questo caso.
Ma non solo non voglio davvero leggere i dump dei thread non elaborati a mano (onestamente, è una cosa così 2019), so che questo approccio non è nemmeno fattibile una volta che siamo in produzione.
Non abbiamo accesso diretto al file traces.txt negli smartphone degli utenti, giusto?
Per fortuna, abbiamo eseguito l'integrazione con Firebase Crashlytics.
Perché elimina la necessità di eseguire ricerche e va alla radice dell'ANR anche in produzione.
Come?
È qui che entra in gioco l'API ApplicationExitInfo di Android.
A partire da Android 11, ogni volta che il sistema segnala un errore ANR o termina il processo dell'app, il sistema operativo registra metadati di uscita completi, tra cui il motivo esatto dell'arresto anomalo, lo stato della memoria e un tracciamento del sistema completo per ogni thread in esecuzione nel processo in quel momento.
E la parte migliore è che non devi installare plug-in aggiuntivi o scrivere una sola riga di codice.
Se utilizzi l'SDK Crashlytics 18.3.5 o versioni successive, funziona immediatamente.
Quindi, non appena l'app si riavvia, l'SDK Crashlytics esegue automaticamente una query su ApplicationExitInfo, acquisisce il dump multithread completo insieme alla telemetria del dispositivo e lo invia al backend di Crashlytics.
Apriamo la console di Crashlytics e vediamo cosa ci dice.
Questa è la visualizzazione dei dettagli del problema nella console Firebase Crashlytics per l'ANR e vediamo subito nella scheda dell'analisi dello stack il worker DefaultDispatcher.
DefaultDispatcher-worker-1 (Root blocking)
dalvik.system.VMRuntime.allocateNode (Native method)
android.graphics.Bitmap.createBitmap (Bitmap.java:1124)
android.graphics.Bitmap.createBitmap (Bitmap.java:1091)
com.chromaclip.app.extractor.FrameDecoder.getUncompressedFrame (FrameDecoder.kt:112)
com.chromaclip.app.extractor.PaletteExtractor$processVideo$2.invokeSuspend (PaletteExtractor.kt:65)
kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith (ContinuationImpl.kt:33)
kotlinx.coroutines.DispatchedTask.run (DispatchedTask.kt:106)
java.util.concurrent.ThreadPoolExecutor.runWorker (ThreadPoolExecutor.java:1167)
java.util.concurrent.ThreadPoolExecutor$Worker.run (ThreadPoolExecutor.java:641)
java.lang.Thread.run (Thread.java:923)Questo è infatti il thread che è stato visualizzato in cima da Crashlytics, che è il collo di bottiglia per il nostro thread principale, e Crashlytics lo contrassegna con un badge chiamato Blocco principale.
Come spiega la descrizione comando nel badge, questo thread di lavoro è effettivamente quello che blocca il thread che ha attivato l'ANR.
A proposito, Crashlytics ha badge intelligenti anche per altri colpevoli.
Ad esempio, se il thread era bloccato durante la lettura di un file di grandi dimensioni o l'esecuzione di una query del database molto lenta, se ha attivato un ANR, il thread verrà taggato come blocco della radice I/O.
Se due thread erano bloccati in attesa l'uno dell'altro, Crashlytics li contrassegnava come in deadlock.
Ora esaminiamo gli stack frame all'interno del thread di blocco principale.
Quindi, abbiamo PaletteExtractor processVideo che chiama FrameDecoder getUncompressedFrame evidenziato in blu, che è completamente bloccato all'interno di createBitmap solo per cercare di allocare memoria per un singolo frame.
Poiché sappiamo che non è in attesa di un altro blocco e che non è in una chiamata di rete, il nostro principale sospettato deve essere la memoria e possiamo confermarlo subito controllando la memoria libera del dispositivo anche qui nella console Crashlytics.
Osserva i due punti in cui Crashlytics mostra queste informazioni.
Nella scheda dei dati per l'evento specifico, proprio accanto alla scheda dell'analisi dello stack che stavamo esplorando, e anche in alto a destra, dove viene mostrata la media di tutti gli eventi.
In entrambi i casi, sembra che non ci sia quasi nulla, nemmeno 100 kB.
Quindi, riflettiamo.
In questo caso, poiché il processo non disponeva di memoria sufficiente, Android Runtime ha impiegato tutto il tempo a eseguire freneticamente la garbage collection, cercando di trovare spazio sufficiente per la bitmap successiva, senza mai riuscirci.
Bloccando il thread di lavoro e privando la UI di risorse per oltre cinque secondi.
Ecco un suggerimento rapido.
Ogni volta che ricevi un ANR associato a questo tipo di RAM libera ridotta, è la prova schiacciante che l'ANR è in realtà una perdita di memoria o un problema di bloat.
Ora la domanda è: la nostra app è la vittima o è anche quella che sta monopolizzando la memoria?
Torniamo quindi all'elenco dei problemi del nostro progetto e vediamo se c'è qualcos'altro che sembra pertinente.
Polina Govorkova
E guarda proprio lì.
In basso, vediamo l'elenco dei problemi e, proprio sopra l'ANR con gli otto eventi, abbiamo l'eccezione OutOfMemory.
Quindi, dopotutto, potremmo essere noi i colpevoli.
Questo è un indizio molto importante.
Perché su Android, un errore Java OutOfMemoryError, o OOM in breve, non significa solo che lo smartphone aveva poca RAM.
Ciò significa che il processo della nostra app ha superato il limite dell'heap Java di Android Runtime.
Quindi, a questo punto, la mia teoria è che ANR e OOM siano in realtà le due facce della stessa medaglia.
A seconda della tempistica durante un'esecuzione di test, ChromaClip si bloccava nel thrashing all'interno della garbage collection per un tempo sufficiente ad attivare un ANR o esauriva completamente l'heap, causando un OOM.
Esaminiamo l'eccezione OutOfMemory per saperlo con certezza.
Iniziamo con l'analisi dello stack.
java.lang.OutOfMemoryError: Failed to allocate a 256 byte allocation
at android.graphics.Bitmap.createBitmap
at android.view.LayoutInflater.inflate(LayoutInflater.java:532)
at com.chromaclip.app.SettingsActivity.onCreate(SettingsActivity.kt:21)Aspetta un attimo.
Bitmap.createBitmap, mi sembra familiare.
Non l'abbiamo già visto?
Beh, tranne per il fatto che questa volta viene chiamata da qui.
Non è stato possibile allocare 256 byte durante l'apertura dell'attività delle impostazioni.
Stiamo cadendo di nuovo nella stessa trappola.
Se l'app si arresta in modo anomalo perché non riesce a espandere una piccola icona delle impostazioni, l'attività delle impostazioni non sarà il problema.
È solo il chiamante sfortunato che ha chiesto un po' di memoria quando non ce n'era.
Quindi, per trovare il vero colpevole, dobbiamo rimpicciolire e vedere tutto ciò che era presente nella memoria quando si è verificato l'arresto anomalo.
In altre parole, abbiamo bisogno di un dump dell'heap Java.
Se sai come attivare un OOM, puoi acquisire manualmente un dump dell'heap Java con Memory Profiler di Android Studio.
Tuttavia, quando un OOM si verifica in modo imprevedibile, durante le esecuzioni dei test o in produzione, questo metodo non funziona.
Polina Govorkova
Questo è un altro momento di gloria per Firebase Crashlytics.
Oltre alle informazioni sull'uscita dall'applicazione, Crashlytics ora si integra con l'API ProfilingManager di Android.
L'API ProfilingManager di Android 17 è stata potenziata con la profilazione della memoria attivata dal sistema, ed è esattamente ciò di cui abbiamo bisogno qui.
Ciò significa che, non appena l'app genera un errore OutOfMemory o supera i limiti di memoria di sistema, il sistema operativo acquisisce un'istantanea dell'heap in background e, in Crashlytics, la raccolta attivata è disponibile senza costi aggiuntivi, a condizione che tu utilizzi l'SDK Crashlytics 20.1.0 o versioni successive.
In seguito, Crashlytics raggrupperà automaticamente le interruzioni per OOM e limitatore di memoria, di cui parleremo più avanti, nella dashboard.
Quando attivi la raccolta dei dump dell'heap tramite la console Firebase o l'interfaccia a riga di comando, Crashlytics inizia a raccogliere automaticamente le istantanee heap acquisite, le deoffusca e, in pratica, ti fornisce una radiografia completa di tutto ciò che si trova nell'heap Java della tua app al momento dell'arresto anomalo.
Ecco come appare l'OOM di ChromaClip nella console Crashlytics una volta acquisito l'heap.
In alto, viene visualizzata una spiegazione del motivo per cui Java ha esaurito l'heap e perché è successo.
Inoltre, vedrai suggerimenti utili per la risoluzione dei problemi e indicazioni per le risorse esterne.
Nella scheda dell'analisi dello stack, proprio sopra l'attività delle impostazioni fuorviante, Crashlytics segnala che le analisi dello stack OOM non sono deterministiche, in modo che le persone non debbano perdere tempo a esaminare l'analisi dello stack come abbiamo appena fatto.
Poi, Crashlytics allega il file Perfetto deoffuscato, ovvero il dump dell'heap, per la sessione.
Da qui possiamo scaricare il file.
Possiamo sfogliare tutti i dump in Google Cloud Storage o aprirli nell'interfaccia utente di Perfetto.
Se fai clic su Analizza nell'interfaccia utente di Perfetto, si apre l'esploratore di dump dell'heap di Perfetto.
Ora, alzate la mano se avete già visto questo.
Ok, vedo un paio di mani alzate.
Per me, questa visualizzazione è molto intimidatoria, ma, in questo caso, il problema è in realtà piuttosto semplice.
Osserva l'insieme di barre enormi che si estendono quasi per l'intero heap Java.
Quindi, per la maggior parte, si tratta di un array di oggetti coppia, ognuno dei quali contiene un colore e un'istanza di stringa.
Oh!
In realtà so cosa sta succedendo, credo.
Il nostro codice sta allocando un colore più alcuni dati di supporto per ogni singolo pixel in ogni frame e ne sta conservando milioni in un elenco.
Quindi, non c'è da stupirsi che l'attività Impostazioni non sia riuscita ad allocare quei 256 byte.
Beh, almeno un mistero è stato risolto.
Risolviamo il problema.
A questo punto, abbiamo un paio di opzioni.
Quindi, dato che Perfetto ci ha già indirizzato a PaletteExtractor, potremmo aprire il nostro IDE e riscrivere il loop manualmente o utilizzare l'agente di programmazione AI.
In alternativa, possiamo bypassare completamente l'ispezione manuale dell'heap e sfruttare la CLI Android.
# 1. Install Android CLI & Perfetto skills
$ android skills add android-cli perfetto
# 2. List recent heap dumps from Crashlytics
$ android signals heaps list --kind oom --timeframe=90d
# 3. Download a heap dump as a Perfetto trace
$ android signals heaps download <heap-dump-id> \
--output ./heaps
# 4. Query top retained objects in the heap dump
$ trace_processor query ./heaps/oom.perfetto-trace ...
# 5. Fetch matching Crashlytics events
$ android signals heaps describe <heap-dump-id>Quindi, l'installazione delle competenze CLI Android e Perfetto ti consentirà di elencare e scaricare i dump dell'heap di Crashlytics deoffuscati, di eseguire query sugli oggetti principali conservati da questi dump dell'heap tramite trace_processor e di recuperare i dati dei problemi di Crashlytics corrispondenti, come chiavi personalizzate, log, breadcrumb, implementazioni associate e altro ancora, tutto dal tuo terminale.
Ancora meglio, guarda la parte inferiore delle slide.
Non eseguire questi comandi autonomamente.
Ancora una volta, chiedi al tuo agente di programmazione AI preferito in linguaggio naturale di ispezionare l'ultimo dump dell'heap OOM utilizzando il comando Android e di inviare la correzione.
# Or ask your favorite LLM in natural language
> Using `android` command, inspect the latest heap dump
for my OOM issue and propose a fix
● Ran android signals heaps list · download · describe
● Analyzed ./heaps/oom.perfetto-trace (perfetto)
● Updated PaletteExtractor.kt (+6 -4 lines)Inoltre, avrà un contesto sufficiente per eseguire l'intero flusso per te.
Quindi, elenca gli OOM, scarica la traccia, esegui l'analisi e genera la patch di codice direttamente nel tuo progetto.
E guarda questa patch nel file PaletteExtractor.
// PaletteExtractor.kt
fun extractPalette(framePixels: IntArray): SparseIntArray {
- val pixelColors = mutableListOf<Pair<Color, String>>() // 8.3M objects per 4K frame!
- for (px in framePixels) {
- pixelColors.add(Color.valueOf(px) to "#%06X".format(px))
- }
+ val colorHistogram = SparseIntArray() // Downsampled & packed ARGB ints!
+ for (i in framePixels.indices step 16) {
+ val argb = framePixels[i]
+ colorHistogram.put(argb, colorHistogram.get(argb) + 1)
+ }
+ return colorHistogram
}Invece di milioni di allocazioni di oggetti, ora eseguiamo semplicemente il downsampling del frame e contiamo direttamente i numeri interi non elaborati.
Quindi, applichiamo la correzione, chiudiamo i problemi, aggiorniamo la versione e continuiamo i test.
Incrociamo le dita: non solo l'errore di memoria esaurita dovrebbe scomparire, ma poiché non avremo più problemi con la garbage collection, speriamo che anche quel fastidioso ANR scompaia.
Quindi, abbiamo eseguito il deployment della build della patch sui nostri dispositivi di test, scelto un nuovo clip e iniziato a estrarre le tavolozze.
E la UI si blocca di nuovo.
Un altro errore ANR.
È interessante notare che, eseguendo altri test, abbiamo scoperto che in alcuni dispositivi si verifica ancora l'errore ANR, ma in altri l'app si arresta in modo anomalo in modo casuale e anche abbastanza spesso, più spesso rispetto agli errori OOM.
Ma abbiamo corretto l'OOM, quindi cosa sta succedendo?
Polina Govorkova
Torniamo a Crashlytics.
Iniziamo con le buone notizie.
Consulta il nostro elenco dei problemi.
L'eccezione di memoria esaurita è completamente scomparsa, come speravamo.
La cattiva notizia?
Ora che ChromaClip sopravvive un po' più a lungo, ha il tempo di scoprire cos'altro si nascondeva sotto.
Quindi, l'ANR non è scomparso.
Ora abbiamo due arresti anomali nativi in basso, fortunatamente con solo un paio di eventi.
E proprio in cima, con 11 eventi, abbiamo l'interruzione del limitatore di memoria, per fortuna, già contrassegnato come dotato di dump dell'heap.
Aspetta,
Che cos'è un'interruzione del limitatore di memoria?
Quanti di voi ne hanno sentito parlare? Alzate la mano.
Okay.
Alcune mani rare.
Impariamo insieme.
Se non hai mai visto Memory Limiter kill, è perché è una novità di Android 17.
Inoltre, un errore OOM di Java si attiva solo quando superi il limite dell'heap Java gestito, ma quando il footprint della memoria totale della tua app include anche la RAM nativa, come i buffer grafici e le allocazioni C++, in precedenza non esisteva un limite per app.
Quindi, avremmo il low memory killer, che era globale.
Interveniva quando un'app consumava tutta la RAM del sistema fino a bloccare l'intero smartphone.
Ora, a partire da Android 17, il limitatore di memoria limita il footprint della memoria di ogni app.
Pertanto, quando il processo consuma una quantità sproporzionata di RAM, il sistema operativo termina l'app specifica.
Crashlytics acquisisce tutti questi dettagli e raggruppa le terminazioni nella dashboard.
Quindi, facendo clic su Chiusura per limitatore di memoria in Crashlytics, proprio come con l'OOM, ricevi una spiegazione del motivo per cui il sistema operativo è intervenuto, quindi non devi memorizzare ciò che ho appena detto.
Riceverai anche suggerimenti su come affrontare il problema e link diretti alla documentazione sull'applicazione della memoria di Android.
E proprio come con OOM, l'analisi dello stack incolpa un frame innocente quando il sistema operativo ha staccato la spina e la dashboard di Crashlytics lo evidenzia.
Ora, le interruzioni del limitatore di memoria si verificano principalmente nel codice nativo perché negli ambienti nativi manca quel livello che gestisce la sicurezza e le allocazioni della memoria.
In caso contrario, si sarebbe verificato un errore OOM.
Quindi, la nostra prima domanda è: i nostri oggetti Kotlin conservano enormi allocazioni supportate da codice nativo?
O si tratta di un codice C++ non elaborato che presenta un leak?
Facciamo clic su Analizza nell'interfaccia utente di Perfetto per scoprirlo.
Guarda la scheda Panoramica in Perfetto Heap Dump Explorer.
Nella sezione Informazioni generali, l'RSS anonimo più lo swap mostra che la memoria totale del processo ha raggiunto i 4 GB.
Ok.
Tuttavia, sotto vediamo che in Bytes Retained by Heap, l'intero heap gestito, inclusa la memoria nativa legata agli oggetti Kotlin come le bitmap, è di soli 42 MB.
Ok.
Quindi, in 10 secondi, abbiamo escluso tutto il codice Kotlin e Java.
Quindi, il problema deve essere una perdita da un livello C++ nativo.
E poiché ChromaClip ha un sottile livello C++ per l'elaborazione dei frame, non dobbiamo cercare lontano.
Se indirizziamo il nostro agente di programmazione o il profiler nativo di Android Studio a NativePaletteEngine.cpp processFrame, il bug è proprio lì.
// NativePaletteEngine.cpp
void NativePaletteEngine::processFrame(const uint8_t* rgba, size_t size) {
uint8_t* scratch = new uint8_t[size]; // Temporary frame buffer
transformColorSpace(rgba, scratch, size);
// BUG: Allocated every frame, but never freed!
+ delete[] scratch; // Free native buffer when done
}Quindi, su ogni singolo frame video, il nostro codice C++ alloca un buffer temporaneo, non così temporaneo se non lo rilasci, che errore da principiante.
Quindi, è il momento di collegare i puntini.
Che cosa è successo?
Una volta corretto il leak di Java, quello di C++ avrà il tempo di accumularsi e scatenarsi.
Su Android 17, il limitatore di memoria ha rilevato il problema e ha chiuso l'app.
Tuttavia, sui dispositivi meno recenti in cui il limitatore di memoria non era ancora presente, si è manifestato come un errore ANR.
Quindi, finalmente, tutto ha senso.
Aggiungiamo delete[] scratch qui, aggiorniamo la versione e inviamo l'aggiornamento.
Con l'eliminazione definitiva delle interruzioni del limitatore di memoria e degli ANR, dovremmo essere pronti per il lancio della versione 1.0.
Emozionante.
E grazie alla velocità e alla stabilità di ChromaClip, la nostra app decolla.
Migliaia di persone caricano e remixano i video ogni giorno.
Ma con tutto il traffico, ricordi quegli arresti anomali nativi misteriosi che abbiamo visto prima nella parte inferiore della dashboard?
Beh, anche loro sono impazziti.
All'improvviso, ce ne sono molti di più.
E il nostro lavoro non è ancora finito.
Polina Govorkova
Quindi, aprendo l'arresto anomalo nativo in Crashlytics, esamina l'analisi dello stack.
Non c'è una sola riga di codice della nostra app.
RenderThread si arresta in modo anomalo all'interno di libc.so durante la liberazione della memoria.
Un altro suggerimento: quando un thread di sistema si arresta in modo anomalo all'interno dell'allocatore della memoria senza che il codice della tua app sia presente, questo è un segno evidente di danneggiamento della memoria nativa.
Quindi, il nostro codice C++ ha danneggiato o liberato il buffer in precedenza e il thread di rendering lo ha semplicemente rilevato in un secondo momento.
Quindi, come facciamo a individuare il vero colpevole nel momento esatto in cui si è verificato il danneggiamento?
Android ci offre due strumenti potenti e Crashlytics li supporta entrambi in modo nativo.
Innanzitutto, GWP-ASan, una rete di sicurezza leggera creata appositamente per la produzione.
Campiona una piccola frazione delle allocazioni heap native e le circonda con pagine di protezione in modo che l'overhead di CPU e memoria sia quasi pari a zero.
In migliaia di sessioni utente in condizioni reali, quando un bug tocca la memoria liberata o fuori dai limiti in un'allocazione campionata, GWP-ASan lo rileva immediatamente.
Tuttavia, poiché GWP-ASan è, per sua natura, probabilistico, quando esegui test in locale, non vuoi eseguire il test sul telefono per 16 o più ore per raggiungere il campione casuale.
Quindi, cosa ottieni?
Per il debug locale deterministico, puoi utilizzare ARM MTE o Memory Tagging Extension.
MTE utilizza un puntatore hardware e tag di memoria per controllare ogni allocazione ed è disponibile sui dispositivi ARM v9 come Pixel 8 e modelli successivi.
Quindi, abbinalo a GWP-ASan in produzione e otterrai il meglio di entrambi i mondi.
Come probabilmente ti aspetti, quando uno dei due strumenti rileva il bug, Crashlytics assegna automaticamente un badge al problema e ti consente di filtrare per GWP-ASan o MTE e di bloccare la scheda informativa con la causa esatta del danneggiamento nella dashboard.
Vediamo cosa succede nella console Crashlytics quando GWP-ASan rileva l'arresto anomalo.
Accanto alla scheda di analisi dello stack normale, abbiamo l'analisi dello stack della memoria.
In alto, vediamo la causa esatta del danneggiamento, Use After Free.
Invece di mostrare l'arresto anomalo del thread di rendering in libc.so, GWP-ASan ora ci fornisce la biografia completa di quel buffer all'interno del nostro effect_pipeline.cc.
Quindi, in Allocated By (Allocato da), vediamo dove è stato creato il buffer.
E proprio sotto Deallocated By, vediamo il colpevole esatto che l'ha liberata dal thread di rendering.
che si trova alla riga 208 della nostra pipeline di effetti.
Quindi, apriamo il nostro codice nativo in EffectPipeline e il bug ci sorride.
// effect_pipeline.cc
void EffectPipeline::onFilterSwitched(FilterPresetId preset) {
// BUG: Freed on worker thread while RenderThread is still reading it!
+ std::lock_guard<std::mutex> lock(lut_mutex_);
releaseLutBuffer(); // Frees old color-grading LUT
loadFilterPreset(preset); // Allocates new color-grading LUT
}Ogni volta che un utente cambia il filtro colore nel thread in background, liberiamo il buffer precedente mentre RenderThread lo sta ancora leggendo.
Ok, quindi aggiungiamo il blocco mutex intorno allo scambio del buffer, inviamo l'aggiornamento e, infine, osserviamo i report GWP-ASan e tutti quegli arresti anomali misteriosi svanire quasi da un giorno all'altro.
Inoltre, la nostra metrica degli utenti senza arresti anomali è salita al 100%.
Evviva!
Ricordate che all'inizio del discorso ci siamo chiesti: "Ho finito?"
Subito prima di essere colpito da praticamente ogni singolo boss della reattività e della memoria del libro.
Ora gli ANR, gli errori out of memory, le interruzioni del limitatore di memoria e gli arresti anomali per danneggiamento della memoria nativa sono tutti risolti.
E anche se l'analisi dello stack mente e il colpevole si nasconde da qualche parte tra i thread in C++, Android e Firebase Crashlytics ti offrono lo strumento per rintracciarlo e pubblicare con sicurezza.
Polina Govorkova
Quindi, ricapitoliamo cosa puoi usare non appena torni al tuo laptop: insight sui thread ANR e altri metadati pertinenti tramite ApplicationExitInfo; raccolta automatica di trigger di interruzione OOM e limitatore di memoria tramite ProfilingManager; e supporto completo di GWP-ASan e Arm MTE per il danneggiamento della memoria nativa.
Inoltre, stiamo lanciando in anteprima privata i dump dell'heap sul campo su Android 17 o versioni successive.
Se vuoi essere tra i primi a provare la raccolta automatica dei dump dell'heap su Firebase Crashlytics e analizzarli con Perfetto e la CLI Android, scatta una foto della slide e registrati al link.
Ora che abbiamo affrontato la maggior parte degli argomenti, voglio ringraziarti per aver partecipato oggi e per il tempo che mi hai dedicato.
Che il tuo thread principale rimanga sbloccato, il tuo heap rimanga pulito e la tua prossima release sia la più stabile di sempre.
Buon debug!
Grazie ancora.
java.lang.NullPointerException
at MainActivity.onCreate(MainActivity.kt:42)
at android.app.Activity.performCreate
at android.os.Handler.dispatchMessage
at android.os.Looper.loopOnce
at android.app.ActivityThread.main"main" prio=5 tid=1 Blocked
| group="main" sCount=1 dsCount=0 flags=1
| state=S schedstat=( 1450239 48192 145 )
at DataManager.getData(DataManager.kt:25)
- waiting to lock <0x0a1b> held by thread 12
at MainActivity.onResume(MainActivity.kt:50)
at android.app.Activity.performResume
at android.os.Handler.dispatchMessage
at android.os.Looper.loopOnce
at android.app.ActivityThread.main14:32:15.105 1205-1250/system_process E/ActivityManager: ANR in com.chromaclip.app (com.chromaclip.app/.MainActivity)
PID: 14205
Reason: Input dispatching timed out
Dumped ANR trace to /data/anr/traces.txt
"main" prio=5 tid=1 Native
| group="main" sCount=1 dsCount=0 flags=1 obj=0x72a8c3d0 self=0x759fb92000
| sysTid=14205 nice=-10 cgrp=default sched=0/0 handle=0x7623910548
| state=S schedstat=( 1450239120 4819203 1453 ) utm=132 stm=13 core=4 HZ=100
| stack=0x7fed2a0000-0x7fed2a2000 stackSize=8MB
at sun.misc.Unsafe.park(Native method)
- waiting on an unknown object
at java.util.concurrent.locks.LockSupport.park(LockSupport.java:190)
at kotlinx.coroutines.EventLoopImplBase.processNextEvent(EventLoop.common.kt:279)
at kotlinx.coroutines.BlockingCoroutine.joinBlocking(Builders.kt:85)
at kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking(Builders.kt:59)
at kotlinx.coroutines.BuildersKt.runBlocking(Unknown Source:1)
at com.chromaclip.app.extractor.PaletteExtractor.extractColors(PaletteExtractor.kt:42)DefaultDispatcher-worker-1 (Root blocking)
dalvik.system.VMRuntime.allocateNode (Native method)
android.graphics.Bitmap.createBitmap (Bitmap.java:1124)
android.graphics.Bitmap.createBitmap (Bitmap.java:1091)
com.chromaclip.app.extractor.FrameDecoder.getUncompressedFrame (FrameDecoder.kt:112)
com.chromaclip.app.extractor.PaletteExtractor$processVideo$2.invokeSuspend (PaletteExtractor.kt:65)
kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith (ContinuationImpl.kt:33)
kotlinx.coroutines.DispatchedTask.run (DispatchedTask.kt:106)
java.util.concurrent.ThreadPoolExecutor.runWorker (ThreadPoolExecutor.java:1167)
java.util.concurrent.ThreadPoolExecutor$Worker.run (ThreadPoolExecutor.java:641)
java.lang.Thread.run (Thread.java:923)java.lang.OutOfMemoryError: Failed to allocate a 256 byte allocation
at android.graphics.Bitmap.createBitmap
at android.view.LayoutInflater.inflate(LayoutInflater.java:532)
at com.chromaclip.app.SettingsActivity.onCreate(SettingsActivity.kt:21)# 1. Install Android CLI & Perfetto skills
$ android skills add android-cli perfetto
# 2. List recent heap dumps from Crashlytics
$ android signals heaps list --kind oom --timeframe=90d
# 3. Download a heap dump as a Perfetto trace
$ android signals heaps download <heap-dump-id> \
--output ./heaps
# 4. Query top retained objects in the heap dump
$ trace_processor query ./heaps/oom.perfetto-trace ...
# 5. Fetch matching Crashlytics events
$ android signals heaps describe <heap-dump-id># Or ask your favorite LLM in natural language
> Using `android` command, inspect the latest heap dump
for my OOM issue and propose a fix
● Ran android signals heaps list · download · describe
● Analyzed ./heaps/oom.perfetto-trace (perfetto)
● Updated PaletteExtractor.kt (+6 -4 lines)// PaletteExtractor.kt
fun extractPalette(framePixels: IntArray): SparseIntArray {
- val pixelColors = mutableListOf<Pair<Color, String>>() // 8.3M objects per 4K frame!
- for (px in framePixels) {
- pixelColors.add(Color.valueOf(px) to "#%06X".format(px))
- }
+ val colorHistogram = SparseIntArray() // Downsampled & packed ARGB ints!
+ for (i in framePixels.indices step 16) {
+ val argb = framePixels[i]
+ colorHistogram.put(argb, colorHistogram.get(argb) + 1)
+ }
+ return colorHistogram
}// NativePaletteEngine.cpp
void NativePaletteEngine::processFrame(const uint8_t* rgba, size_t size) {
uint8_t* scratch = new uint8_t[size]; // Temporary frame buffer
transformColorSpace(rgba, scratch, size);
// BUG: Allocated every frame, but never freed!
+ delete[] scratch; // Free native buffer when done
}// effect_pipeline.cc
void EffectPipeline::onFilterSwitched(FilterPresetId preset) {
// BUG: Freed on worker thread while RenderThread is still reading it!
+ std::lock_guard<std::mutex> lock(lut_mutex_);
releaseLutBuffer(); // Frees old color-grading LUT
loadFilterPreset(preset); // Allocates new color-grading LUT
}