SQLite Android Database Cursor Fensterzuordnung von 2048 kb fehlgeschlagen

73

Ich habe eine Routine, die viele Abfragen mehrmals pro Sekunde für eine SQLite-Datenbank ausführt. Nach einer Weile würde ich den Fehler bekommen

"android.database.CursorWindowAllocationException: - Cursor window allocation of 2048 kb failed. # Open Cursors = " erscheinen in LogCat.

Ich hatte die Speicherauslastung des App-Protokolls, und wenn die Nutzung eine bestimmte Grenze erreicht, wird dieser Fehler angezeigt, was bedeutet, dass er nicht mehr verfügbar ist. Meine Intuition sagt mir, dass das Datenbankmodul jedes Mal, wenn ich eine Abfrage ausführe, einen NEUEN Puffer (CursorWindow) erstellt, und obwohl ich die Cursor .close () markiere, können weder der Garbage Collector noch SQLiteDatabase.releaseMemory()schnell genug Speicher freigeben . Ich denke, die Lösung könnte darin bestehen, die Datenbank zu "zwingen", immer in denselben Puffer zu schreiben und keine neuen zu erstellen, aber ich konnte keinen Weg finden, dies zu tun. Ich habe versucht, mein eigenes CursorWindow zu instanziieren, und versucht, es ohne Erfolg auf SQLiteCursor zu setzen.

Irgendwelche Ideen?

BEARBEITEN: Beispielcodeanforderung von @GrahamBorland:

public static CursorWindow cursorWindow = new CursorWindow("cursorWindow"); 
public static SQLiteCursor sqlCursor;
public static void getItemsVisibleArea(GeoPoint mapCenter, int latSpan, int lonSpan) {
query = "SELECT * FROM Items"; //would be more complex in real code
sqlCursor = (SQLiteCursor)db.rawQuery(query, null);
sqlCursor.setWindow(cursorWindow);
}

Idealerweise möchte ich in der Lage sein, .setWindow()bevor ich eine neue Abfrage stelle, und die Daten CursorWindowjedes Mal, wenn ich neue Daten erhalte , in die gleichen Daten einfügen.

Alex
quelle
Ich weiß nicht, was das Problem ist .. :), aber ich verwende, um SQLiteOpenHelper-Klasse Singleton zu machen. Ich habe also nie solche Probleme gefunden.
Mohsin Naeem
Nein, ich verwende SQLiteOpenHelper nicht. Ich erstelle eine statische DataAccess-Klasse, die eine SQLiteDatabase enthält. Das funktioniert gut und ich bezweifle, dass das Problem da ist. Das Problem hat mehr damit zu tun, dass die SQLite-Bibliotheken einen NEUEN Container erstellen, um die Ergebnisse jeder neuen Abfrage zu platzieren, anstatt immer wieder denselben Container zu verwenden. Und obwohl ich den Cursor schließen kann, ist die Geschwindigkeit, mit der der GC bereinigt, langsamer als die Geschwindigkeit, mit der neue Container erstellt werden, wodurch das Speicherproblem entsteht.
Alex
Können Sie einen Teil Ihres Codes anzeigen, insbesondere, was Sie mit dem Festlegen Ihres eigenen Codes versucht haben CursorWindow?
Graham Borland
@GrahamBorland public static CursorWindow cursorWindow = new CursorWindow("cursorWindow"); public static SQLiteCursor sqlCursor; public static void getItemsVisibleArea(GeoPoint mapCenter, int latSpan, int lonSpan) { query = "SELECT * FROM Items"; //would be more complex in real code sqlCursor = (SQLiteCursor)db.rawQuery(query, null); sqlCursor.setWindow(cursorWindow); } Idealerweise möchte ich in der Lage sein, .setWindow zu erstellen, bevor ich eine neue Abfrage gebe, und die Daten jedes Mal, wenn ich neue Daten erhalte, in dasselbe CursorWindow einfügen.
Alex
3
Bearbeiten Sie es in Ihre Frage, fügen Sie keine großen Codefragmente als Kommentare ein!
Graham Borland

Antworten:

107

Die Ursache für diesen Fehler sind meistens nicht geschlossene Cursor. Stellen Sie sicher, dass Sie alle Cursor schließen, nachdem Sie sie verwendet haben (auch im Fehlerfall).

Cursor cursor = null;
try {
    cursor = db.query(...
    // do some work with the cursor here.
} finally {
    // this gets called even if there is an exception somewhere above
    if(cursor != null)
        cursor.close();
}

Damit Ihre App abstürzt, wenn Sie keinen Cursor schließen, können Sie den Strikten ModusdetectLeakedSqlLiteObjects in Ihren Anwendungen aktivieren onCreate:

StrictMode.VmPolicy policy = new StrictMode.VmPolicy.Builder()
   .detectLeakedClosableObjects()
   .detectLeakedSqlLiteObjects()
   .penaltyDeath()
   .penaltyLog()
   .build();
StrictMode.setVmPolicy(policy);

Natürlich würden Sie dies nur für Debug-Builds aktivieren.

whlk
quelle
Sie können dieses Beispiel einfach verwenden, um die Null- und Nullprüfung zu vermeiden.
aij
3
Genau wie beim Öffnen von Dateizeigern - behandeln Sie IMMER das Schließen im Abschnitt finally, um sicherzustellen, dass Ihr Code sauber vorhanden ist.
Slott
81

Wenn Sie eine erhebliche Menge an SQL-Code durchsuchen müssen, können Sie möglicherweise das Debuggen beschleunigen, indem Sie das folgende Codeausschnitt in Ihre MainActivity einfügen, um StrictMode zu aktivieren. Wenn durchgesickerte Datenbankobjekte erkannt werden, stürzt Ihre App jetzt mit Protokollinformationen ab, die genau angeben, wo sich Ihr Leck befindet. Dies half mir, innerhalb weniger Minuten einen Schurkencursor zu finden.

@Override
protected void onCreate(Bundle savedInstanceState) {
   if (BuildConfig.DEBUG) {     
         StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder()
         .detectLeakedSqlLiteObjects()
         .detectLeakedClosableObjects()
         .penaltyLog()
         .penaltyDeath()
         .build());
    }
    super.onCreate(savedInstanceState);
    ...
    ...
Skyjacks
quelle
6
Das ist großartig! Ich habe diesen Standard mit if (BuildConfig.DEBUG) {...}einem 'statischen {...}' Block meiner Hauptklasse erstellt.
Brian White
1
Der beste und effizienteste Weg, um diese Art von Problem zu debuggen, die ich bisher gefunden habe, sollte die akzeptierte Antwort sein!
AndroidSeb
Toll! Aber in der Veröffentlichung wird ohne den StrictMode funktionieren?
Alfdev
Laut StrictMode javadoc: "Zukünftige Versionen von Android können mehr (oder weniger) Vorgänge abfangen. Sie sollten StrictMode daher niemals in Anwendungen aktivieren, die auf Google Play verteilt werden."
Demaksee
Dies ist der beste Weg, um diese Art von Problem zu debuggen. Vielen Dank! :)
Shivam Pokhriyal
11

Ich habe dieses Problem gerade erlebt - und die vorgeschlagene Antwort, den Cursor nicht zu schließen, während es gültig ist, war nicht, wie ich es behoben habe. Mein Problem war das Schließen der Datenbank, als SQLite versuchte, den Cursor neu zu füllen. Ich würde die Datenbank öffnen, die Datenbank abfragen, um einen Cursor auf einen Datensatz zu erhalten, die Datenbank schließen und über den Cursor iterieren. Ich bemerkte, dass meine App immer dann mit demselben Fehler im OP abstürzte, wenn ich einen bestimmten Datensatz in diesem Cursor traf.

Ich gehe davon aus, dass der Cursor für den Zugriff auf bestimmte Datensätze die Datenbank erneut abfragen muss. Wenn er geschlossen wird, wird dieser Fehler ausgegeben. Ich habe das Problem behoben, indem ich die Datenbank erst geschlossen habe, nachdem ich alle erforderlichen Arbeiten abgeschlossen hatte.

micnguyen
quelle
5

Es gibt tatsächlich eine maximale Größe, die Android SQLite-Cursorfenster annehmen können, und das sind 2 MB. Alles, was größer als diese Größe ist, würde zu dem obigen Fehler führen. Meistens wird dieser Fehler entweder durch ein großes Bildbyte-Array verursacht, das als Blob in der SQL-Datenbank gespeichert ist, oder durch zu lange Zeichenfolgen. Hier ist, wie ich es behoben habe.

Erstellen Sie eine Java-Klasse, z. FixCursorWindow und geben Sie den folgenden Code ein.

    public static void fix() {
        try {
            Field field = CursorWindow.class.getDeclaredField("sCursorWindowSize");
            field.setAccessible(true);
            field.set(null, 102400 * 1024); //the 102400 is the new size added
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

Gehen Sie nun zu Ihrer Anwendungsklasse (erstellen Sie eine, falls Sie dies noch nicht getan haben) und rufen Sie das FixCursorWindow wie folgt auf

öffentliche Klasse App erweitert Anwendung {

public void onCreate()
{
    super.onCreate();
    CursorWindowFixer.fix();

}

}}

Stellen Sie schließlich sicher, dass Sie Ihre Anwendungsklasse wie folgt in Ihr Manifest auf dem Anwendungs-Tag aufnehmen

    android:name=".App">

Das ist alles, es sollte jetzt perfekt funktionieren.

Anuoluwapo Wahab
quelle
Sie werden wahrscheinlich Probleme auf Android Pie bekommen, weil Sie Reflection API verwenden
Alex Kucherenko
1

Wenn Sie Android P ausführen, können Sie Ihr eigenes Cursorfenster wie folgt erstellen:

if(cursor instanceof SQLiteCursor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
    ((SQLiteCursor) cursor).setWindow(new CursorWindow(null, 1024*1024*10));
}

Auf diese Weise können Sie die Größe des Cursorfensters für einen bestimmten Cursor ändern, ohne auf Reflexionen zurückgreifen zu müssen.

Linus Mårtensson
quelle
-1

Dies ist eine normale Ausnahme, wenn wir insbesondere externes SQLite verwenden. Sie können das Problem beheben, indem Sie das Cursorobjekt wie folgt schließen:

if(myCursor != null)
        myCursor.close();

Dies bedeutet, dass der Cursor über Speicher verfügt und geöffnet und dann geschlossen wird, damit die Anwendung schneller wird, alle Methoden weniger Platz beanspruchen und die mit der Datenbank verbundenen Funktionen ebenfalls verbessert werden.

Numan Gillani
quelle
-3
public class CursorWindowFixer {

  public static void fix() {
    try {
      Field field = CursorWindow.class.getDeclaredField("sCursorWindowSize");
      field.setAccessible(true);
      field.set(null, 102400 * 1024);
    } catch (Exception e) {
      e.printStackTrace();
    }
  }
}
jingyuan iu
quelle
Durchbruch CursorWindow Limit von nur 2 Megabyte
Jingyuan iu