在Android中同时访问SQLite数据库 - 数据库已关闭

3

我看了很多关于这个主题的话题,但没有人能回答我的问题。

我从并发线程访问我的数据库,我的SQLiteOpenHelper实现了单例设计模式,因此我只有一个应用程序实例。

我使用以下代码访问我的数据库:

 SQLiteDatabase db = DatabaseHelper.getInstance().getWritableDatabase();
 ...
 Do some update in the DB
 ...
 db.close();

我不明白为什么我仍然会收到“db already closed”错误,getWritableDatabase()方法不应该锁定数据库直到调用close()吗?其他线程的getWritableDatabase()调用应该等待直到数据库关闭?是这样的吗还是我错过了什么?
2个回答

7

在elhadi的回答基础上,我遇到了类似的问题,即在多个异步任务中打开和关闭数据库连接。经过我的调查发现,不必要一直打开和关闭数据库连接。我最终采用的方法是对Application进行子类化,在onCreate期间执行单个数据库打开操作,onTerminate期间执行单个数据库关闭操作。然后设置一个静态getter来检索已经打开的SQLiteDatabase对象。虽然这种方式不太适合DI(依赖注入),但安卓目前还无法完全实现依赖注入。

实现方法如下:

    public class MainApplication extends Application {
          private static SQLiteDatabase database;

          /**
           * Called when the application is starting, before any other 
           * application objects have been created. Implementations 
           * should be as quick as possible...
           */
          @Override
          public void onCreate() {
          super.onCreate();
          try {
           database = SQLiteDatabase.openDatabase("/data/data/<yourdbpath>", null, SQLiteDatabase.OPEN_READWRITE);
          } catch (SQLiteException e) {
            // Our app fires an event spawning the db creation task...
           }
         }


          /**
           * Called when the application is stopping. There are no more 
           * application objects running and the process will exit.
           * <p>
           * Note: never depend on this method being called; in many 
           * cases an unneeded application process will simply be killed 
           * by the kernel without executing any application code...
           * <p>
           */
          @Override
          public void onTerminate() {
            super.onTerminate();
            if (database != null && database.isOpen()) {
              database.close();
            }
          }


          /**
           * @return an open database.
           */
          public static SQLiteDatabase getOpenDatabase() {
            return database;
          }
    }

阅读JavaDoc后,我肯定是从某个地方抄袭了这个代码,但是这个静态的单一数据库开/关解决了你遇到的问题。在SO上有另一个答案描述了这种解决方案。
更多细节:
针对Fr4nz以下的NPE评论,我提供了我们特定实现的更多细节。
简短版本:
如果您没有很好地理解BroadcastReceivers,则很难掌握下面的“完整图像”。在您的情况下(首次启动),请在创建数据库之后添加您的DB创建代码并初始化和打开数据库。因此,请编写;
      try {
       database = SQLiteDatabase.openDatabase("/data/data/<yourdbpath>", null, SQLiteDatabase.OPEN_READWRITE);
      } catch (SQLiteException e) {
        // Create your database here!
        database = SQLiteDatabase.openDatabase("/data/data/<your db path>", null, SQLiteDatabase.OPEN_READWRITE);
       }
     }

简略版

以上代码仅为一部分,实际上还需要注意第一次运行应用程序时的异常处理。我们的应用程序会触发一个事件来生成数据库。在应用程序中,我们注册了一个监听器(Android 的 BroadcastReceiver 框架),并且主应用程序活动的其中一项任务是检查 MainApplication 中的 database 静态变量是否为空。如果为空,则会产生一个异步任务来创建数据库,当它完成(即运行 onPostExecute() 方法)时,最终会触发事件,我们知道此事件将被我们在 try-catch 中注册的监听器捕获。该接收器作为 MainApplication 类的内部类存在,如下所示:

    /**
    * Listener waiting for the application to finish
    * creating the database.
    * <p>
    * Once this has been completed the database is ready for I/O.
    * </p>
    *
    * @author David C Branton
    */
      public class OpenDatabaseReceiver extends BroadcastReceiver {
        public static final String BROADCAST_DATABASE_READY = "oceanlife.core.MainApplication$OpenDatabaseReceiver.BROADCAST_DATABASE_READY";

        /**
         * @see android.content.BroadcastReceiver#onReceive(android.content.Context, android.content.Intent)
         */
        @Override
        public void onReceive(final Context context, final Intent intent) {
          Log.i(CreatedDatabaseReceiver.class.getSimpleName(), String.format("Received filter event, '%s'", intent.getAction()));
          database = SQLiteDatabase.openDatabase("/data/data/<your db path>", null, SQLiteDatabase.OPEN_READWRITE);
          unregisterReceiver(openDatabaseReceiver);

          // Broadcast event indicating that the creation process has completed.
          final Intent databaseReady = new Intent();
          databaseReady.setAction(BROADCAST_DATABASE_READY);
          context.sendBroadcast(databaseReady);
        }
      }

因此,首次安装的启动过程概述如下:
  1. 类:MainApplication,角色-检查是否存在数据库?
    • 是?database变量被初始化
    • 否?注册接收器(OpenDatabaseReceiver)
  2. 类:MainActivity,角色-应用程序的着陆活动,并最初检查数据库变量不为空。
    • database为null?不添加执行I/O的片段,并添加对话框,显示“正在创建应用程序数据库”或类似消息。
    • database不为null?继续主应用程序执行流程,添加由db支持的列表等
  3. 类:DatabaseCreationDialogFragment,角色-生成异步任务以创建数据库。
    • 注册新的接收器,监听何时创建了数据库。
    • 在收集到“我已经创建了数据库”消息后,从接收器触发另一个事件,告知应用程序打开数据库。
  4. 类:MainApplication,角色2-侦听“数据库已创建”消息。
    • 上述接收器(OpenDatabaseReceiver)打开数据库并广播(通过另一个事件!),表示该数据库已准备好使用。
  5. 类:MainActivity,角色2-获取“数据库已准备就绪”消息,去除“我们正在创建数据库”的对话框,并开始显示应用程序中的数据/功能。
和平恢复了。

不要忘记在尝试这个操作时,更新你的“Manifest.xml”文件中新应用程序“名称”的路径。 - BrantApps
嗨,我现在正在遇到一些数据库路径的问题。显然它找不到数据库并且getOpenDatabase抛出NullPointerException异常。我能否使用类似于DatabaseHelper.getInstance().getWritableDatabase()(仅一次)而不是openDatabase()? - Fr4nz
Fr4nz添加了更多细节。请阅读并勾画一些东西。如果您想了解我们使用“长版本”的原因,最好在“聊天”中讨论。这一切都关乎片段的角色和责任,以及告知用户正在发生什么。 - BrantApps
谢谢您详细的回答,我会尝试这个。 - Fr4nz
好的总结!(即使概述了一个有点我个人认为很愚蠢的设计 - 对话框没有理由生成线程和接收器 - Ярослав Рахматуллин
该片段启动一个托管AsyncTask的服务。当工作完成时,监听器会被调用。此时对话框是阻塞的(初始数据库创建)。松耦合,高内聚。 - BrantApps

1
如果您在线程中调用DatabaseHelper.getInstance().getWritableDatabase(),我建议您在启动线程之前进行管理。您可以在主程序中打开数据库,然后调用线程。在线程终止后,您可以在主程序中关闭数据库。

是的,我在每个线程中都调用DatabaseHelper.getInstance().getWritableDatabase()。谢谢你的回答,我会尝试这个方法。但是我的理论推断是正确的吗? - Fr4nz
在你的解决方案中,我是否应该在启动线程之前调用getWritableDatabase()以初始化数据库?我真的不明白它应该如何帮助,因为我仍然需要在我的线程中调用getWritableDatabase(),对吗? - Fr4nz

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接