Jetzt reparieren
- Datei ablegen
- Reparatur läuft lokal
- Ergebnis herunterladen
Beide Persistenzschichten von Android schreiben gewöhnliches SQLite: Room erzeugt sein Schema auf derselben Engine, die auch das alte SQLiteOpenHelper nutzt, und beide legen eine einzige Datei unter /data/data/<paket>/databases/<name>.db ab. Wird diese Datei beschädigt, wirft Room android.database.sqlite.SQLiteDatabaseCorruptException: database disk image is malformed (code 11 SQLITE_CORRUPT) und die App wird zwangsweise beendet oder zeigt einen leeren Bildschirm. Zieh die vom Gerät geholte .db oben hinein — und füge ihre -wal-Begleitdatei im optionalen Feld hinzu, falls du sie hast — und das Tool scannt die Seiten, baut das Schema neu auf und fügt jede Zeile, die es noch dekodieren kann, in eine frische Datenbank ein, alles innerhalb deines Browser-Tabs. Die Datei wird niemals hochgeladen.
Wo eine Android-App ihre Datenbank ablegt — und was darin steckt
Auf jedem Android-Gerät liegt die Datenbank in der privaten Sandbox der App, unter /data/data/<paket>/databases/, normalerweise als drei Dateien: die Hauptdatei <name>.db, ein Write-Ahead-Log <name>.db-wal und ein Shared-Memory-Index <name>.db-shm. Die Hauptdatei beginnt mit der 16 Byte langen Signatur "SQLite format 3\000"; die Seitengröße als Zwei-Byte-Wert steht an Byte-Offset 16, und die Format-Lese-/Schreibversionsbytes an den Offsets 18 und 19 stehen auf 2, wenn die Datenbank im WAL-Modus läuft — den Room standardmäßig aktiviert.
Room fügt Kennzeichen hinzu, mit denen du bestätigen kannst, dass du die richtige Datei hast. Es legt eine Tabelle android_metadata mit einer einzigen Zeile für die Ländereinstellung an (zum Beispiel en_US) — diese Tabelle schreibt Androids Klasse SQLiteDatabase für jede App-Datenbank, ob mit Room oder ohne — sowie eine room_master_table mit den Spalten id und identity_hash, in der Room den Schema-Hash in der festen Zeile id = 42 speichert. Deine Entitätstabellen (mit @Entity annotiert) und ihre Indizes stehen daneben, alle aufgeführt in der Schematabelle sqlite_master (in neueren SQLite-Versionen unter dem Alias sqlite_schema). Eine App mit reinem SQLiteOpenHelper lässt die room_master_table weg, ist ansonsten aber dieselbe standardmäßige SQLite-Datei.
Wie die Datei 'malformed' wird — und die genauen Fehler
Eine SQLite-Datenbank ist ein flaches Feld aus Seiten fester Größe (standardmäßig 4096 Byte). Seite 1 trägt den Header und das Schema; jede Tabelle und jeder Index ist ein B-Baum, dessen innere Seiten Zellzeiger enthalten, die hinunter zu den Blattseiten führen, und die Blätter enthalten deine eigentlichen Datensätze. Die Engine liest den Header, findet das Schema in sqlite_master und folgt dann diesen Zeigern bis zu den Zeilen.
Die Zeiger sind die Schwachstelle. Wenn der App-Prozess mitten in einem Schreibvorgang vom Low-Memory-Killer von Android beendet wird, dem Telefon der Strom ausgeht, der Speicher volläuft oder zwei Prozesse dieselbe Datei ohne korrekte Sperrung öffnen, kann eine einzelne innere Seite oder ein Header-Feld inkonsistent zurückbleiben. SQLite hält dann beim ersten Problem an und weist die gesamte Datei mit database disk image is malformed zurück — Ergebniscode 11 (SQLITE_CORRUPT) — obwohl die Blattseiten hinter der Bruchstelle weiterhin intakte, selbstbeschreibende Datensätze enthalten. Ein verwandter Fehler, file is not a database — Ergebniscode 26 (SQLITE_NOTADB) — bedeutet, dass die 16 Byte lange Signatur des Headers falsch ist: Entweder wurden diese Header-Bytes beschädigt (die Seiten dahinter sind oft in Ordnung und wiederherstellbar) oder die Datei ist tatsächlich etwas anderes, etwa eine mit SQLCipher verschlüsselte Datenbank, die sich als hochentropisches Rauschen liest.
Eine Android-spezifische Falle macht das dringlich: Der DefaultDatabaseErrorHandler.onCorruption() des Frameworks löscht die Datenbankdatei, wenn er beim Öffnen eine Beschädigung erkennt. Eine App, die auf den Fehler "malformed" gestoßen ist, kann die Datei also bei ihrem nächsten Start bereits gelöscht haben. Kopiere die Datenbank vom Gerät herunter — und arbeite mit dieser Kopie — bevor du die App erneut öffnest.
Die Begleitdateien -wal und -shm: zieh alle drei
Room aktiviert das Write-Ahead-Logging standardmäßig (enableWriteAheadLogging()), was bedeutet, dass zuletzt bestätigte Änderungen vorläufig in der Geschwisterdatei <name>.db-wal abgelegt werden, bis ein Checkpoint sie zurück in die Haupt-.db überträgt. Nach einem Absturz können deine neuesten Zeilen nur in diesem WAL liegen. Ziehst du die Hauptdatei ohne es, fehlt der wiederhergestellten Datenbank alles, was seit dem letzten Checkpoint geschrieben wurde — ein häufiger Grund, warum Leute glauben, "die Hälfte meiner Daten ist weg".
Das WAL ist nicht die Hauptdatenbank: Es beginnt mit einem 32 Byte langen Header, dessen magische Zahl 0x377f0682 oder 0x377f0683 ist (das niederwertige Bit wählt Frame-Prüfsummen im Big-Endian- oder Little-Endian-Format), gefolgt von Frames, die jeweils aus einem 24 Byte langen Frame-Header plus einem Seitenabbild bestehen. Die -shm-Datei ist nur ein Shared-Memory-Index in das WAL; sie wird automatisch neu erzeugt und kann bedenkenlos zurückgelassen werden. Zieh also die Haupt-.db und die -wal zusammen: Leg die Datenbank oben ab und füge dann die -wal im optionalen Begleitdatei-Feld hinzu; ihre bestätigten Frames werden vor dem Scan überlagert, sodass die Wiederherstellung die letzte bestätigte Transaktion statt des letzten Checkpoints widerspiegelt.
Die Datenbank sicher vom Gerät holen
Bei einem debugfähigen Build erreichst du den privaten Ordner mit run-as: zum Beispiel adb exec-out run-as <paket> tar c ./databases | tar xv, oder kopiere die Dateien mit run-as <paket> cp databases/<name>.db /sdcard/ in den gemeinsamen Speicher und dann adb pull. Release-Apps (nicht debugfähig) verweigern run-as, du brauchst also ein gerootetes Gerät oder die app-eigene Exportfunktion. Der alte Weg adb backup -f backup.ab <paket> wurde in Android 12 abgekündigt und respektiert android:allowBackup, weshalb er dafür unzuverlässig ist. Welchen Weg du auch wählst, hol dir <name>.db, <name>.db-wal und <name>.db-shm in einem Zug, damit das WAL zur Hauptdatei passt.
Mit der Kopie auf deinem Rechner ist die klassische Prüfung sqlite3 name.db "PRAGMA integrity_check;" (oder das schnellere quick_check), und die klassische Reparatur ist SQLites eigener Befehl .recover, der jede dekodierbare Zeile ausliest und in eine brandneue Datei schreibt. Genau das macht diese Seite im Browser: Sie geht die Seiten durch, baut die B-Bäume neu auf, greift für Zeilen, die die Zeigerkette nicht mehr erreicht, auf einen rohen Seiten-Scan zurück und legt alles, was sie keiner bekannten Tabelle zuordnen kann, in eine Tabelle lost_and_found, statt es zu verwerfen — so wird nichts weggeworfen und nichts hochgeladen.
Was sich reparieren lässt und was nicht
Kann repariert werden
- "database disk image is malformed (code 11 SQLITE_CORRUPT)" durch eine beschädigte Seite oder einen beschädigten B-Baum-Zellzeiger, mit intakten Blattseiten dahinter
- Ein falscher oder beschädigter Header (fehlerhaftes Seitengrößen- oder Seitenzahl-Feld), wenn die nachfolgenden Seiten noch lesbar sind
- Zeilen auf Seiten, die die Zeigerkette nicht mehr erreicht, durch einen rohen Seiten-Scan in eine Tabelle lost_and_found geholt
- Zeilen ohne Checkpoint aus einer bereitgestellten Begleitdatei
.db-wal (die neuesten Daten nach einem Absturz) - Room- und SQLiteOpenHelper-Datenbanken, die vom Gerät kopiert wurden, bevor der Fehlerhandler der App sie löschte
Kann nicht repariert werden
- Zeilen, die physisch überschrieben oder am Dateiende abgeschnitten wurden — diese Bytes existieren nicht mehr
- Mit SQLCipher verschlüsselte Datenbanken (Signal oder Room-Apps mit einer SupportFactory) ohne die Passphrase — die Seiten sind Chiffretext
- Eine Datenbank, die DefaultDatabaseErrorHandler bereits gelöscht hat — das ist zunächst ein Problem der Datenrettung auf dem Gerät
- Die exakten Spaltenaffinitäten: Werte kommen als ihre Speicherklasse auf der Festplatte zurück (NULL/INTEGER/REAL/TEXT/BLOB)
- Eine .db-Datei mit 0 Byte — es gibt nichts auf der Festplatte zu dekodieren
Wenn eine Reparatur fehlschlägt, sagen wir dir warum (fehlende Daten gegenüber beschädigter Struktur), und für eine fehlgeschlagene Reparatur wird dir nie etwas berechnet.