Закрытие базы данных в ContentProvider
На этой неделе я узнал все о ContentProvider и использовал класс SQLiteOpenHelper для управления созданием и обновлением базы данных внутри провайдера. В частности, я читал пример NotePad из каталога примеров sdk.
Теперь я вижу, что SQLiteOpenHelper имеет метод close(). Я знаю, что оставлять открытые базы данных открытыми - это плохая практика, которая может привести к утечкам памяти и так далее (если это обсуждение не идет в правильном направлении). Если бы я использовал класс в Activity, то я бы просто вызвал close() в методе onDestroy (), но, насколько я знаю, ContentProvider не имеет тот же жизненный цикл, что и действия. Кажется, что код для NotePad никогда не вызывает close(), поэтому я хотел бы предположить, что он обрабатывается SQLiteOpenHelper или каким-то другим фрагментом головоломки, но мне бы очень хотелось знать наверняка. Я не очень доверяю образцу кода, либо...
Резюме вопроса: когда мы должны закрыть базу данных у поставщика, если вообще?
6 ответов
По словам Дайанн Хэкборн ( Dianne Hackborn) (инженер по Android Framework), нет необходимости закрывать базу данных у поставщика контента.
Поставщик контента создается, когда создается его хост-процесс, и остается в нем в течение всего процесса, поэтому нет необходимости закрывать базу данных - он будет закрыт как часть ядра, очищающего ресурсы процесса, когда процесс убит.
Спасибо @bigstones за указание на это.
Этот вопрос немного стар, но все еще актуален. Обратите внимание, что если вы делаете вещи "современным" способом (например, используя LoaderManager и создаете CursorLoaders для запроса ContentProvider в фоновом потоке), убедитесь, что вы НЕ вызываете db.close() в вашей реализации ContentProvider. Я получал всевозможные сбои, связанные с CursorLoader/AsyncTaskLoader, когда он пытался получить доступ к ContentProvider в фоновом потоке, которые были устранены путем удаления вызовов db.close().
Поэтому, если вы столкнулись с авариями, которые выглядят так (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
Или это (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
Или, если вы видите сообщения об ошибках в LogCat, которые выглядят так:
Cursor: invalid statement in fillWindow()
Затем проверьте реализацию ContentProvider и убедитесь, что вы не закрываете базу данных преждевременно. В соответствии с этим ContentProvider будет очищаться автоматически, когда процесс все равно будет остановлен, поэтому вам не нужно закрывать его базу данных заранее.
Тем не менее, убедитесь, что вы все еще правильно:
- Закрытие ваших курсоров, которые возвращены из ContentProvider.query (). (CursorLoader / LoaderManager делает это автоматически для вас, но если вы делаете прямые запросы вне рамок LoaderManager, или вы внедрили собственный подкласс CursorLoader/AsyncTaskLoader, вам необходимо убедиться, что вы очищаете свои курсоры должным образом.)
- Реализация вашего ContentProvider потокобезопасным способом. (Самый простой способ сделать это - убедиться, что ваши методы доступа к базе данных обернуты в синхронизированный блок.)
Я последовал ответу Манназа и увидел, что SQLiteCursor(database, driver, table, query);
конструктор устарел. Потом я нашел getDatabase()
метод и использовал его вместо mDatabase
указатель; и сохранил конструктор для обратной возможности
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);
}
}
Если вы хотите, чтобы ваша база данных закрывалась автоматически, вы можете предоставить CursorFactory
при его открытии:
mContext.openOrCreateDatabase(DB_NAME, SQLiteDatabase.OPEN_READWRITE, new LeaklessCursorFactory());
Вот классы:
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();
}
}
}
Закройте его, когда закончите, желательно в блоке finally, чтобы убедиться, что это произойдет. Я знаю, это звучит немного банально и странно, но это действительно единственный ответ, который я знаю. Если вы откроете базу данных и выполните действие, закройте его, когда закончите с этим действием, если вы точно не уверены, что оно понадобится снова (в этом случае обязательно закройте его, когда оно больше не нужно).
Если вы используете своего контент-провайдера в какой-либо деятельности, то я не считаю, что вам нужно поддерживать соединение с контент-провайдером. Вы можете просто управлять объектом курсора, возвращенным с помощью startManagingCursor. В методе действия onPause вы можете освободить контент-провайдера. (вы можете перезагрузить его в onResume). Если предположить, что жизненный цикл деятельности обычно будет ограниченным, этого будет достаточно. (По крайней мере по мне;))