Recupera il database SQLite corrotto di un'app Android

Le app Android conservano il proprio stato in SQLite, quindi un crash all'avvio è di solito una singola pagina o un puntatore rotti in un file .db — non dati persi. Room e SQLiteOpenHelper scrivono un database SQLite 3 standard, e le righe quasi sempre sono ancora al loro posto nelle pagine. Ricostruirle a partire da una copia che hai estratto dal dispositivo non implica mai consegnare quel database a un server.

Il tuo file non lascia mai il tuo dispositivo — la riparazione avviene nel browser. 0 byte caricati.

Your data is the big block — usually intact. What breaks is the small index. Repair rebuilds it.

Drop a file here or browseTap to choose a file

ZIP · Office · PDF · video · JPG · PNG · RAR/7z · SQLite — repaired right here in your browser. Nothing is uploaded

0 bytes uploadedFree: 3 repairs/day — up to 500 MB video, 100 MB docs & archives, 50 MB photos. Free download. Single repair €5.90. No account required.

Ripara ora

  1. Trascina il file
  2. La riparazione avviene in locale
  3. Scarica il risultato

Entrambi i livelli di persistenza di Android scrivono SQLite comune: Room genera il proprio schema sopra lo stesso motore usato dal vecchio SQLiteOpenHelper, ed entrambi depositano un unico file in /data/data/<pacchetto>/databases/<nome>.db. Quando quel file si danneggia, Room solleva android.database.sqlite.SQLiteDatabaseCorruptException: database disk image is malformed (code 11 SQLITE_CORRUPT) e l'app si chiude forzatamente o mostra una schermata vuota. Trascina qui sopra il .db estratto — e aggiungi il file laterale -wal nella casella opzionale, se ce l'hai — e lo strumento analizza le pagine, ricostruisce lo schema e reinserisce ogni riga che riesce ancora a decodificare in un database nuovo, tutto all'interno della scheda del browser. Il file non viene mai caricato.

Dove un'app Android conserva il suo database — e cosa contiene

Su qualsiasi dispositivo Android il database risiede nella sandbox privata dell'app, in /data/data/<pacchetto>/databases/, di solito come tre file: il principale <nome>.db, un registro di scrittura anticipata (write-ahead log) <nome>.db-wal e un indice in memoria condivisa <nome>.db-shm. Il file principale si apre con la stringa magica di 16 byte "SQLite format 3\000"; la dimensione di pagina di due byte si trova all'offset 16 in byte, e i byte di versione di lettura/scrittura del formato, agli offset 18 e 19, valgono 2 quando il database è in modalità WAL — che Room attiva per impostazione predefinita.

Room aggiunge delle impronte che puoi usare per confermare di avere il file giusto. Crea una tabella android_metadata che contiene un'unica riga con le impostazioni internazionali (per esempio en_US) — quella tabella viene scritta dalla classe SQLiteDatabase di Android per ogni database di app, che si usi Room o meno — e una room_master_table con le colonne id e identity_hash, dove Room memorizza l'hash dello schema nella riga fissa id = 42. Le tue tabelle di entità (annotate con @Entity) e i relativi indici si trovano accanto a esse, tutte elencate nella tabella di schema sqlite_master (con alias sqlite_schema nelle versioni più recenti di SQLite). Un'app con SQLiteOpenHelper puro salta la room_master_table, ma per il resto è lo stesso file SQLite standard.

Come il file diventa 'malformed' — e gli errori esatti

Un database SQLite è un array piatto di pagine di dimensione fissa (4096 byte per impostazione predefinita). La pagina 1 contiene l'intestazione e lo schema; ogni tabella e indice è un albero-B le cui pagine interne contengono puntatori di cella che scendono fino alle pagine foglia, e le foglie custodiscono i tuoi record veri e propri. Il motore legge l'intestazione, trova lo schema in sqlite_master e poi segue quei puntatori fino alle righe.

I puntatori sono il punto debole. Quando il processo dell'app viene ucciso dal low-memory killer di Android a metà di una scrittura, il telefono resta senza batteria, lo spazio di archiviazione si riempie, oppure due processi aprono lo stesso file senza un blocco corretto, una singola pagina interna o un campo dell'intestazione può rimanere incoerente. SQLite si ferma allora al primo problema e rifiuta l'intero file con database disk image is malformed — codice di risultato 11 (SQLITE_CORRUPT) — anche se le pagine foglia oltre la rottura contengono ancora record intatti e autodescrittivi. Un guasto affine, file is not a database — codice di risultato 26 (SQLITE_NOTADB) — significa che la stringa magica di 16 byte dell'intestazione è errata: o quei byte si sono danneggiati (le pagine che stanno dietro sono spesso integre e recuperabili) oppure il file è davvero qualcos'altro, come un database cifrato con SQLCipher che si legge come rumore ad alta entropia.

Una trappola specifica di Android rende tutto ciò urgente: il DefaultDatabaseErrorHandler.onCorruption() del framework elimina il file del database quando rileva una corruzione all'apertura. Perciò un'app che ha incontrato l'errore "malformed" potrebbe aver già cancellato il file al suo avvio successivo. Copia il database fuori dal dispositivo — e lavora su quella copia — prima di riaprire l'app.

I file laterali -wal e -shm: estraili tutti e tre

Room attiva per impostazione predefinita il registro di scrittura anticipata (enableWriteAheadLogging()), il che significa che le modifiche confermate di recente vengono conservate provvisoriamente nel file gemello <nome>.db-wal finché un checkpoint non le riversa di nuovo nel .db principale. Dopo un crash, le tue righe più recenti possono vivere solo in quel WAL. Estrai il file principale senza di esso e al database recuperato mancherà tutto ciò che è stato scritto dall'ultimo checkpoint — un motivo frequente per cui la gente pensa "metà dei miei dati sono spariti".

Il WAL non è il database principale: inizia con un'intestazione di 32 byte il cui numero magico è 0x377f0682 o 0x377f0683 (il bit meno significativo sceglie somme di controllo dei frame big-endian o little-endian), seguita da frame composti ciascuno da un'intestazione di frame di 24 byte più un'immagine di pagina. Il file -shm è soltanto un indice in memoria condivisa verso il WAL; viene rigenerato automaticamente ed è sicuro lasciarlo indietro. Quindi estrai insieme il .db principale e il -wal: trascina qui sopra il database, poi aggiungi il -wal nella casella laterale opzionale e i suoi frame confermati vengono sovrapposti prima della scansione, in modo che il recupero rifletta l'ultima transazione confermata anziché l'ultimo checkpoint.

Estrarre il database dal dispositivo in sicurezza

In una build debuggabile puoi raggiungere la cartella privata con run-as: per esempio adb exec-out run-as <pacchetto> tar c ./databases | tar xv, oppure copiare i file nell'archiviazione condivisa con run-as <pacchetto> cp databases/<nome>.db /sdcard/ e poi adb pull. Le app di release (non debuggabili) rifiutano run-as, quindi ti serve un dispositivo con root oppure la funzione di esportazione dell'app stessa. La vecchia via adb backup -f backup.ab <pacchetto> è stata deprecata in Android 12 e rispetta android:allowBackup, perciò non è affidabile per questo scopo. Qualunque strada tu scelga, prendi <nome>.db, <nome>.db-wal e <nome>.db-shm in un colpo solo, così il WAL corrisponde al file principale.

Con la copia sul tuo computer, il controllo classico è sqlite3 name.db "PRAGMA integrity_check;" (o il più rapido quick_check), e la riparazione classica è il comando .recover di SQLite stesso, che legge ogni riga decodificabile e la scrive in un file nuovo di zecca. Questa pagina fa esattamente questo nel browser: percorre le pagine, ricostruisce gli alberi-B, ricorre a una scansione grezza delle pagine per le righe che la catena di puntatori non raggiunge più, e mette tutto ciò che non riesce ad attribuire a una tabella nota in una tabella lost_and_found invece di scartarlo — così non si butta via niente e non si carica niente.

Cosa può e cosa non può riparare

Può riparare

  • "database disk image is malformed (code 11 SQLITE_CORRUPT)" causato da una pagina o da un puntatore di cella di albero-B danneggiati, con pagine foglia intatte dietro
  • Un'intestazione errata o danneggiata (campo di dimensione o di conteggio delle pagine sbagliato) quando le pagine successive sono ancora leggibili
  • Righe su pagine che la catena di puntatori non raggiunge più, estratte da una scansione grezza delle pagine in una tabella lost_and_found
  • Righe senza checkpoint da un file laterale .db-wal fornito (i dati più recenti dopo un crash)
  • Database di Room e SQLiteOpenHelper copiati dal dispositivo prima che il gestore di errori dell'app li eliminasse

Non può riparare

  • Righe sovrascritte fisicamente o troncate dalla fine del file — quei byte non esistono più
  • Database cifrati con SQLCipher (Signal, o app con Room che usano un SupportFactory) senza la passphrase — le pagine sono testo cifrato
  • Un database che DefaultDatabaseErrorHandler ha già eliminato — quello è prima di tutto un problema di recupero dati dal dispositivo
  • Le affinità esatte delle colonne: i valori tornano come la loro classe di archiviazione su disco (NULL/INTEGER/REAL/TEXT/BLOB)
  • Un file .db di 0 byte — non c'è nulla su disco da decodificare

Se una riparazione fallisce, ti diciamo perché (dati mancanti rispetto a struttura danneggiata), e non ti viene mai addebitato nulla per una riparazione fallita.

Domande frequenti

La mia app ha eliminato il proprio database dopo il crash — perché?

Il DefaultDatabaseErrorHandler.onCorruption() di Android elimina il file quando rileva SQLITE_CORRUPT all'apertura, quindi un secondo avvio può cancellarlo. Ecco perché la soluzione è copiare prima il .db (e il suo -wal) fuori dal dispositivo con adb run-as e recuperare dalla copia — mai dal file attivo che l'app sta ancora aprendo.

Ho estratto il .db ma mancano i dati recenti. Cos'è successo?

Room funziona in modalità WAL per impostazione predefinita, quindi le righe confermate più recenti si trovano in <nome>.db-wal, non ancora riversate nel file principale. Estrai tutti e tre i file — .db, -wal e -shm — e aggiungi il -wal nella casella laterale opzionale; i suoi frame confermati vengono sovrapposti prima della scansione, così viene inclusa l'ultima transazione.

Funziona con un database Room o solo con SQLite grezzo?

Con entrambi — sotto il cofano Room è SQLite normale. Il recupero ricostruisce android_metadata, room_master_table (compreso l'identity_hash memorizzato in id = 42) e le tue tabelle @Entity. Se l'hash dello schema arriva intatto, Room riapre il file ricostruito senza lamentarsi di non poter verificare l'integrità dei dati.

Nel mio caso l'errore dice 'file is not a database'. È recuperabile?

A volte. Si tratta di SQLITE_NOTADB (codice 26): la stringa magica di 16 byte dell'intestazione è errata. Se si sono danneggiati solo i byte dell'intestazione, le pagine dietro di essi sono di solito integre e recuperabili. Se l'intero file è rumore ad alta entropia, è un database cifrato (SQLCipher) o non è affatto un database — quello non può essere recuperato senza la chiave. La diagnosi distingue i due casi.

Il mio database viene caricato per essere riparato?

No. Il database di un'app è il file che meno vorresti sul server di qualcun altro — può contenere ogni record utente, messaggio e token che l'app ha memorizzato. Qui il .db viene letto dal tuo disco e ricostruito nel browser; puoi osservare la scheda Rete e verificare che escano 0 byte.

Correlati: Ripara un database SQLite (qualsiasi origine) · Ripara un video Android · Ripara una cartella di lavoro Excel · Verifica la promessa di zero caricamenti