Schließen der Datenbank in einem ContentProvider

73

Diese Woche habe ich alles über ContentProvider gelernt und die SQLiteOpenHelper-Klasse verwendet, um die Erstellung und Aktualisierung der Datenbank innerhalb eines Anbieters zu verwalten. Insbesondere habe ich das NotePad-Beispiel aus dem Beispielverzeichnis des SDK gelesen.

Jetzt kann ich sehen, dass SQLiteOpenHelper eine close () -Methode hat. Ich bin mir bewusst, dass das Öffnen offener Datenbanken eine schlechte Praxis ist und Speicherlecks und so weiter verursachen kann (es sei denn, diese Diskussion geht in die richtige Richtung). Wenn ich die Klasse in einer Aktivität verwenden würde, würde ich einfach close () in der onDestroy () -Methode aufrufen, aber meines Wissens hat ContentProvider nicht den gleichen Lebenszyklus wie Aktivitäten. Der Code für NotePad scheint nie close () aufzurufen, daher würde ich gerne annehmen, dass er von SQLiteOpenHelper oder einem anderen Teil des Puzzles verarbeitet wird, aber ich würde es wirklich gerne sicher wissen. Ich vertraue dem Beispielcode auch nicht so sehr ...

Zusammenfassung der Fragen: Wann sollten wir die Datenbank bei einem Anbieter schließen, wenn überhaupt?

SilithCrowe
quelle
11
Dianne Hackborn sagte, es sei nicht nötig, die Datenbank zu schließen .
Bigstones
1
Dies sind die wichtigsten Informationen zu diesem Thread. Ich habe es zu einer Antwort gemacht.
Philipp

Antworten:

94

Laut Dianne Hackborn (Android Framework Engineer) muss die Datenbank bei einem Inhaltsanbieter nicht geschlossen werden.

Ein Inhaltsanbieter wird erstellt, wenn sein Hosting-Prozess erstellt wird, und bleibt so lange bestehen, wie der Prozess ausgeführt wird. Daher muss die Datenbank nicht geschlossen werden. Er wird geschlossen, wenn der Kernel die Ressourcen des Prozesses bereinigt, wenn der Prozess wird getötet.

Vielen Dank an @bigstones für diesen Hinweis.

philipp
quelle
5
Vielen Dank. Übrigens sollten sie (Android-Team) diese "einfache" Sache in der Richtlinie oder zumindest im Beispielcode kommentieren , anstatt die Codierer im Internet danach suchen zu lassen.
16
Lass
3
Ok, aber was macht die Abschaltfunktion? Der Code lautet: public void shutdown () {Log.w (TAG, "Implementiere ContentProvider shutdown (), um sicherzustellen, dass alle Datenbankverbindungen" + "ordnungsgemäß heruntergefahren werden"); }
ata
Sie müssen DB schließen, shutdown()wenn Sie Reboelectric ContentProvider-Unit-Tests durchführen möchten.
Jongz Puangput
21

Diese Frage ist etwas alt, aber immer noch sehr relevant. Beachten Sie, dass Sie db.close () in Ihrer ContentProvider-Implementierung NICHT aufrufen müssen , wenn Sie die Dinge auf "moderne" Weise ausführen (z. B. LoaderManager verwenden und CursorLoaders erstellen, um einen ContentProvider in einem Hintergrundthread abzufragen) . Beim Versuch, auf den ContentProvider in einem Hintergrundthread zuzugreifen, kam es zu allen möglichen Abstürzen im Zusammenhang mit CursorLoader / AsyncTaskLoader, die durch Entfernen der Aufrufe von db.close () behoben wurden.

Wenn Sie also auf Abstürze stoßen, die so aussehen (Jelly Bean 4.1.1):

Caused by: java.lang.IllegalStateException: Cannot perform this operation because the connection pool has been closed.
    at android.database.sqlite.SQLiteConnectionPool.throwIfClosedLocked(SQLiteConnectionPool.java:962)
    at android.database.sqlite.SQLiteConnectionPool.waitForConnection(SQLiteConnectionPool.java:677)
    at android.database.sqlite.SQLiteConnectionPool.acquireConnection(SQLiteConnectionPool.java:348)
    at android.database.sqlite.SQLiteSession.acquireConnection(SQLiteSession.java:894)
    at android.database.sqlite.SQLiteSession.executeForCursorWindow(SQLiteSession.java:834)
    at android.database.sqlite.SQLiteQuery.fillWindow(SQLiteQuery.java:62)
    at android.database.sqlite.SQLiteCursor.fillWindow(SQLiteCursor.java:143)
    at android.database.sqlite.SQLiteCursor.getCount(SQLiteCursor.java:133)
    at android.content.ContentResolver.query(ContentResolver.java:388)
    at android.content.ContentResolver.query(ContentResolver.java:313)
    at com.hindsightlabs.paprika.loaders.GroceryListLoader.loadInBackground(GroceryListLoader.java:147)
    at com.hindsightlabs.paprika.loaders.GroceryListLoader.loadInBackground(GroceryListLoader.java:1)
    at android.support.v4.content.AsyncTaskLoader.onLoadInBackground(AsyncTaskLoader.java:240)
    at android.support.v4.content.AsyncTaskLoader$LoadTask.doInBackground(AsyncTaskLoader.java:51)
    at android.support.v4.content.AsyncTaskLoader$LoadTask.doInBackground(AsyncTaskLoader.java:40)
    at android.support.v4.content.ModernAsyncTask$2.call(ModernAsyncTask.java:123)
    at java.util.concurrent.FutureTask$Sync.innerRun(FutureTask.java:305)
    ... 4 more

Oder dies (ICS 4.0.4):

Caused by: java.lang.IllegalStateException: database /data/data/com.hindsightlabs.paprika/databases/Paprika.db (conn# 0) already closed
    at android.database.sqlite.SQLiteDatabase.verifyDbIsOpen(SQLiteDatabase.java:2215)
    at android.database.sqlite.SQLiteDatabase.lock(SQLiteDatabase.java:436)
    at android.database.sqlite.SQLiteDatabase.lock(SQLiteDatabase.java:422)
    at android.database.sqlite.SQLiteQuery.fillWindow(SQLiteQuery.java:79)
    at android.database.sqlite.SQLiteCursor.fillWindow(SQLiteCursor.java:164)
    at android.database.sqlite.SQLiteCursor.getCount(SQLiteCursor.java:156)
    at android.content.ContentResolver.query(ContentResolver.java:318)
    at android.support.v4.content.CursorLoader.loadInBackground(CursorLoader.java:49)
    at android.support.v4.content.CursorLoader.loadInBackground(CursorLoader.java:35)
    at android.support.v4.content.AsyncTaskLoader.onLoadInBackground(AsyncTaskLoader.java:240)
    at android.support.v4.content.AsyncTaskLoader$LoadTask.doInBackground(AsyncTaskLoader.java:51)
    at android.support.v4.content.AsyncTaskLoader$LoadTask.doInBackground(AsyncTaskLoader.java:40)
    at android.support.v4.content.ModernAsyncTask$2.call(ModernAsyncTask.java:123)
    at java.util.concurrent.FutureTask$Sync.innerRun(FutureTask.java:305)
    ... 4 more

Oder wenn in LogCat Fehlermeldungen angezeigt werden, die folgendermaßen aussehen:

Cursor: invalid statement in fillWindow()

Überprüfen Sie anschließend Ihre ContentProvider-Implementierung und stellen Sie sicher, dass Sie die Datenbank nicht vorzeitig schließen. Nach dieser wird erhalten die Contentprovider automatisch gereinigt , wenn der Prozess ohnehin getötet wird, so dass Sie die Datenbank vor der Zeit nicht brauchen , zu schließen.

Stellen Sie jedoch sicher, dass Sie immer noch richtig sind:

  1. Schließen Sie Ihre Cursor, die von ContentProvider.query () zurückgegeben werden . (CursorLoader / LoaderManager erledigt dies automatisch für Sie. Wenn Sie jedoch direkte Abfragen außerhalb des LoaderManager-Frameworks ausführen oder eine benutzerdefinierte CursorLoader / AsyncTaskLoader-Unterklasse implementiert haben, müssen Sie sicherstellen, dass Sie Ihre Cursor bereinigen richtig.)
  2. Implementieren Sie Ihren ContentProvider threadsicher. (Der einfachste Weg, dies zu tun, besteht darin, sicherzustellen, dass Ihre Datenbankzugriffsmethoden in einen synchronisierten Block eingeschlossen sind.)
stevesw
quelle
13

Ich folge Mannaz ' Antwort und habe gesehen, dass der SQLiteCursor(database, driver, table, query);Konstruktor veraltet ist. Dann habe ich die getDatabase()Methode gefunden und anstelle des mDatabaseZeigers verwendet. und Konstruktor für Rückwärtsfähigkeit gehalten

public class MyOpenHelper extends SQLiteOpenHelper {
    public static final String TAG = "MyOpenHelper";

    public static final String DB_NAME = "myopenhelper.db";
    public static final int DB_VESRION = 1;

    public MyOpenHelper(Context context) {
        super(context, DB_NAME, new LeaklessCursorFactory(), DB_VESRION);
    }

    //...
}

public class LeaklessCursor extends SQLiteCursor {
    static final String TAG = "LeaklessCursor";

    public LeaklessCursor(SQLiteDatabase db, SQLiteCursorDriver driver,
            String editTable, SQLiteQuery query) {
        super(db, driver, editTable, query);
    }

    @Override
    public void close() {
        final SQLiteDatabase db = getDatabase();
        super.close();
        if (db != null) {
            Log.d(TAG, "Closing LeaklessCursor: " + db.getPath());
            db.close();
        }
    }
}


public class LeaklessCursorFactory implements CursorFactory {
    @Override
    public Cursor newCursor(SQLiteDatabase db, SQLiteCursorDriver masterQuery,
        String editTable, SQLiteQuery query) {
        return new LeaklessCursor(db,masterQuery,editTable,query);
    }
}
Pleerock
quelle
1
Ich liebe es, wenn sich Leute die Zeit nehmen, ältere Fragen zu beantworten / zu aktualisieren. Danke Pleerock! Sieht so aus, als müsste ich das noch einmal
überdenken
YW. Übrigens habe ich in unserem LeaklessCursor ein "Leck" gefunden. Es ist nicht effektiv, wenn Sie Inhaltsanbieter verwenden. Wenn Sie beispielsweise Daten in der Datenbank aktualisieren, wird auch Ihr Cursor aktualisiert. Dieser Cursor wird also geschlossen und ein neuer Cursor geöffnet. Wenn unser Cursor geschlossen wird, wird auch unsere Datenbank geschlossen. Dies kann einen Fehler verursachen. Zum Beispiel: ContentProvider (öffnet die Datenbank) -> Abfrage (für Cursor, verwendet Datenbank) -> Aktualisierung (alle Daten, verwendet Datenbank) -> Benachrichtigungen -> schließt einen alten Cursor (schließt auch Datenbank) -> erstellt neuen Cursor (Abfrage, verwendet db) und POOOOW Fehler -> db wurde geschlossen, kann neuen Cursor nicht öffnen
pleerock
Wenn der Aufruf von getDatabase () null zurückgeben kann, kann die Datei Log.d (...) abstürzen. Ich habe eine Bearbeitung vorgenommen, die das Protokoll in die if-Anweisung verschiebt, und getDatabase () in eine Referenz eingefügt, da Sie sie verwenden dreimal. Sie können ein Protokoll außerhalb der if-Anweisung hinzufügen, das Sie darüber informiert, dass Sie eine Datenbank "hätten schließen sollen", unabhängig davon, ob getDatabase () null war, wenn dies nützlich ist.
Dandre Allison
Hinweis. Die Idee, die Datenbank in einem ContentProvider zu schließen, ist nicht so gut. Vermeiden Sie dies, wenn Sie können. "Schließen des undichten Cursors" kann zu vielen anderen Lecks führen. Der Inhaltsanbieter ist für Datenbankabfragen vorgesehen und verwendet daher immer die Datenbank. Selbst wenn Sie Ihre Abfragen bereits abgeschlossen haben, kann der Inhaltsanbieter (und die Datenbankinstanz) von anderen Klassen abhängen
Pleerock
Danke, du hast meinen Tag gerettet!
Gordon Freeman
7

Wenn Sie möchten, dass Ihre Datenbank automatisch geschlossen wird, können Sie CursorFactorybeim Öffnen Folgendes angeben:

mContext.openOrCreateDatabase(DB_NAME, SQLiteDatabase.OPEN_READWRITE, new LeaklessCursorFactory());

Hier sind die Klassen:

public class LeaklessCursorFactory implements CursorFactory {
    @Override
    public Cursor newCursor(SQLiteDatabase db, SQLiteCursorDriver masterQuery,
        String editTable, SQLiteQuery query) {
        return new LeaklessCursor(db,masterQuery,editTable,query);
    }
}


public class LeaklessCursor extends SQLiteCursor {
    static final String TAG = "LeaklessCursor";
    final SQLiteDatabase mDatabase;

    public LeaklessCursor(SQLiteDatabase database, SQLiteCursorDriver driver, String table, SQLiteQuery query) {
        super(database, driver, table, query);
        mDatabase = database;
    }

    @Override
    public void close() {
        Log.d(TAG, "Closing LeaklessCursor: " + mDatabase.getPath());
        super.close();
        if (mDatabase != null) {
            mDatabase.close();
        }
    }
}
whlk
quelle
1
Hinweis für andere: Bitte beachten Sie auch die Antwort von Pleerock, da diese Antwort auf eine kleine, aber wichtige Weise aktualisiert wird. Dies ist übrigens eine großartige Lösung - ich werde immer ein bisschen schwindlig, wenn ich sehe, dass Designmuster gut verwendet werden. : P
SilithCrowe
1

Schließen Sie es, wenn Sie damit fertig sind, vorzugsweise in einem Endblock, damit Sie sicherstellen können, dass es passiert. Ich weiß, das klingt ein wenig banal und unkompliziert, aber es ist wirklich die einzige Antwort, die ich kenne. Wenn Sie die Datenbank öffnen und eine Aktion ausführen, schließen Sie sie, wenn Sie mit dieser Aktion fertig sind, es sei denn, Sie wissen, dass sie erneut benötigt wird (in diesem Fall müssen Sie sie unbedingt schließen, sobald sie nicht mehr benötigt wird).

Ingwer McMurray
quelle
2
Der einzige Weg, wie wir wissen können, ob wir es wieder brauchen, ist von außerhalb des Anbieters (in dem Code, der es verwendet). Ich denke, dass innerhalb des Anbieters jedes Mal auf die Datenbank zugegriffen wird, wenn getWriteableDatabase () oder getReadableDatabase () auf dem SQLiteOpenHelper aufgerufen wird. Sollte ich auf der Grundlage Ihres Vorschlags anschließend in jeder Methode, in der diese aufgerufen werden, ein close () hinzufügen? Es scheint, als ob, wenn mehrere Abfragen nacheinander ausgeführt würden, eine ganze Menge Datenbank geöffnet und geschlossen würde. Ich bin mir nicht sicher, aber ich würde mir vorstellen, dass dies die Leistung beeinträchtigen würde.
SilithCrowe
0

Wenn Sie Ihren Inhaltsanbieter innerhalb einer Aktivität verwenden, glaube ich nicht, dass Sie die Verbindung des Inhaltsanbieters aufrechterhalten müssen. Sie können das mit startManagingCursor zurückgegebene Cursorobjekt einfach verwalten. In der Aktivitätsmethode onPause können Sie den Inhaltsanbieter freigeben. (Sie können es in onResume neu laden). Unter der Annahme, dass der Aktivitätslebenszyklus normalerweise begrenzt ist, würde dies ausreichen. (Zumindest nach mir;))

uncaught_exceptions
quelle
Wenn Sie SQL Lite verwenden, können Sie die Verbindung natürlich schließen, nachdem Sie die Ergebnisse erhalten haben. (Stellen Sie erneut sicher, dass der Lebenszyklus des Cursors durch Aktivität mit startmanagingcursor behandelt wird.
uncaught_exceptions