【问题标题】:Closing the database in a ContentProvider在 ContentProvider 中关闭数据库
【发布时间】:2011-05-31 15:40:59
【问题描述】:

本周我一直在学习有关 ContentProvider 的所有知识,并使用 SQLiteOpenHelper 类来管理提供程序内部数据库的创建和升级。具体来说,我一直在阅读 sdk 示例目录中的 NotePad 示例。

现在,我可以看到 SQLiteOpenHelper 有一个 close() 方法。我知道让空闲数据库保持打开状态是不好的做法,并且可能导致内存泄漏等等(除非this 讨论朝着正确的方向发展)。如果我在 Activity 中使用该类,那么我只需在 onDestroy() 方法中调用 close(),但据我所知,ContentProvider 的生命周期与 Activity 不同。 NotePad 的代码似乎从不调用 close(),所以我想假设它是由 SQLiteOpenHelper 或其他一些难题处理的,但我真的很想知道。我也不太信任示例代码...

问题摘要:我们应该什么时候关闭提供程序中的数据库?

【问题讨论】:

标签: android sqlite android-contentprovider


【解决方案1】:

According to Dianne Hackborn(Android 框架工程师)无需在内容提供者中关闭数据库。

内容提供者在其托管进程创建时创建,并且 只要过程持续存在,所以没有必要 关闭数据库——它将作为内核的一部分关闭 在进程被杀死时清理进程的资源。

感谢@bigstones 指出这一点。

【讨论】:

  • 谢谢。顺便说一句,他们(Android 团队)应该在 guideline 或至少在示例代码中评论这个“简单”的东西,而不是让编码人员在网上搜索它。
  • 不要让我开始了解缺少的东西 :)
  • 好的,但是关闭功能是做什么的。代码是: public void shutdown() { Log.w(TAG, "实现 ContentProvider shutdown() 以确保所有数据库" + "连接正常关闭"); }
  • 在进行 Reboelectric ContentProvider 单元测试时,您需要在 shutdown() 关闭 DB。
【解决方案2】:

这个问题有点老了,但仍然很相关。请注意,如果您以“现代”方式做事(例如,使用 LoaderManager 并创建 CursorLoaders 以在后台线程中查询 ContentProvider),请确保不要在 ContentProvider 实现中调用 db.close()。当 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 实现并确保您没有过早关闭数据库。根据this,无论如何,当进程被杀死时,ContentProvider 会自动清理,所以你不需要提前关闭它的数据库。

也就是说,请确保您仍然正确:

  1. 关闭从ContentProvider.query() 返回的游标。 (CursorLoader/LoaderManager 会自动为您执行此操作,但如果您在 LoaderManager 框架之外进行直接查询,或者您已经实现了自定义 CursorLoader/AsyncTaskLoader 子类,则必须确保您正在清理游标正确。)
  2. 以线程安全的方式实现您的 ContentProvider。 (最简单的方法是确保您的数据库访问方法包含在 同步 块中。)

【讨论】:

    【解决方案3】:

    我按照 Mannaz 的 回答并看到 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);
        }
    }
    

    【讨论】:

    • 我喜欢人们花时间回答/更新旧问题。谢谢普莱洛克!看来我需要重新审视这个...
    • YW。顺便说一句,我在我们的 LeaklessCursor 中发现了一些“漏洞”。当您使用内容提供程序时,它无效。例如,当您更新数据库中的数据时,您的光标也会更新。所以这个游标将被关闭,新的游标将被打开。当我们的游标关闭时,我们的数据库也将关闭。它可能会导致错误。例如:ContentProvider(打开数据库)->查询(用于游标,使用数据库)->更新(任何数据,使用数据库)->通知->关闭旧游标(也关闭数据库)->创建新游标(查询,使用 db) 和 POOOOW 错误 -> db 已关闭,无法打开新游标
    • 如果 getDatabase() 调用可以返回 null,那么 Log.d(...) 可能会崩溃,我进行了编辑,将 Log 移动到 if 语句,并放入 getDatabase()在参考中,因为您使用它 3 次。你可以在 if 语句之外添加一个日志,让你知道你“应该”已经关闭了一个数据库,不管 getDatabase() 是否为 null 是否有用。
    • 注意。在 ContentProvider 中关闭数据库的想法不是很好。如果可以,请避免这种情况。 “关闭泄漏的光标”可能会导致许多其他泄漏。内容提供者是用于数据库查询的,所以它总是使用数据库。即使您已经完成了查询,内容提供者(和数据库实例)也可以依赖于其他类
    • 谢谢你,你拯救了我的一天!
    【解决方案4】:

    如果您希望数据库自动关闭,您可以在打开时提供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();
            }
        }
    }
    

    【讨论】:

    • 其他人注意:请注意 Pleerock 的回答,因为它以一种小而重要的方式更新了这个答案。顺便说一句,这是一个很棒的解决方案——当我看到很好地使用设计模式时,我总是有点头晕目眩。 :P
    【解决方案5】:

    如果您在活动中使用您的内容提供者,那么我认为您不必维护内容提供者的连接。您可以只管理使用 startManagingCursor 返回的游标对象。在activity的onPause方法中,可以释放内容提供者。 (您可以在 onResume 中重新加载它)。假设活动生命周期通常是有限的,这就足够了。 (至少在我看来;))

    【讨论】:

    • 当然如果你使用的是sql lite,那么你可以在得到结果后关闭连接。 (再次确保游标的生命周期由使用 startmanagingcursor 的活动处理。
    【解决方案6】:

    完成后关闭它,最好在 finally 块中,这样你就可以确保它发生。我知道这听起来有点陈词滥调,但它确实是我所知道的唯一答案。如果您打开数据库并执行某个操作,请在完成该操作后将其关闭,除非您知道确实会再次需要它(在这种情况下,请确保在不再需要它时将其关闭)。

    【讨论】:

    • 我们知道是否再次需要它的唯一方法是从提供者外部(在使用它的代码中)。我认为在提供程序内部,每次在 SQLiteOpenHelper 上调用 getWriteableDatabase() 或 getReadableDatabase() 时都会访问数据库。根据您的建议,我应该在调用它们的每个方法中添加一个 close() 吗?似乎如果多个查询一个接一个地运行,那么就会有大量的数据库打开和关闭。我不确定,但我想这会影响性能。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-02-16
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多