适用于在Android中写入SQLite数据库的推荐设计模式

7

大家好,

我正在寻找一种设计模式,使UI线程能够与客户端SQLite数据库进行交互,该数据库可能有大量插入(需要10秒以上),快速插入和读取,并且不会阻塞UI线程。

我想征求意见,是否使用了最佳的设计模式,因为我最近一直在调试死锁和同步问题,对我的最终产品没有100%的信心。

现在,所有DB访问都通过一个单例类进行瓶颈控制。以下是我在单例DataManager中处理写入的伪代码:

public class DataManager {

  private SQLiteDatabase mDb;
  private ArrayList<Message> mCachedMessages;

  public ArrayList<Message> readMessages() { 
    return mCachedMessages; 
  }

  public void writeMessage(Message m) {
    new WriteMessageAsyncTask().execute(m);
  }

  protected synchronized void dbWriteMessage(Message m) {
    this.mDb.replace(MESSAGE_TABLE_NAME, null, m.toContentValues());
  }

  protected ArrayList<Message> dbReadMessages() {
    // SQLite query for messages
  }

  private class WriteMessageAsyncTask extends AsyncTask<Message, Void, ArrayList<Messages>> {
    protected Void doInBackground(Message... args) {
       DataManager.this.mDb.execSQL("BEGIN TRANSACTION;");
       DataManager.this.dbWriteMessage(args[0]);
       // More possibly expensive DB writes
       DataManager.this.mDb.execSQL("COMMIT TRANSACTION;");
       ArrayList<Messages> newMessages = DataManager.this.dbReadMessages();
       return newMessages;
    }
    protected void onPostExecute(ArrayList<Message> newMessages) {
       DataManager.this.mCachedMessages = newMessages;
    }
  }
}

亮点:

  • 首先:所有公共写操作(writeMessage)都是通过AsyncTask进行的,永远不会在主线程上执行
  • 接下来:所有写操作都是同步的,且被包装在BEGIN TRANSACTIONS中
  • 接下来:读操作是非同步的,因为它们在写操作期间无需阻塞
  • 最后:读操作的结果在onPostExecute中缓存在主线程中

这是否代表了在最小化对UI线程的影响时将大量数据写入SQLite数据库的Android最佳实践?你在上面看到的伪代码中是否有任何明显的同步问题?

更新

我的代码中存在一个重大错误,如下所示:

DataManager.this.mDb.execSQL("BEGIN TRANSACTION;");

这行代码获取了数据库的锁。然而,这是一种延迟锁,因此在发生写操作之前,其他客户端可以读取和写入

DataManager.this.dbWriteMessage(args[0]);

那行代码实际上修改了数据库。此时,锁是一个“保留”锁,因此没有其他客户端可以写入。
请注意,在第一个dbWriteMessage调用之后,可能会有更多的昂贵的DB写操作。假设每个写操作都发生在受保护的同步方法中。这意味着在DataManager上获取锁,进行写操作,然后释放锁。如果WriteAsyncMessageTask是唯一的写入者,那么这就没问题了。
现在假设还有另一个任务也进行写操作,但不使用事务(因为它是快速写入)。它可能看起来像这样:
  private class WriteSingleMessageAsyncTask extends AsyncTask<Message, Void, Message> {
    protected Message doInBackground(Message... args) {
       DataManager.this.dbWriteMessage(args[0]);
       return args[0];
    }
    protected void onPostExecute(Message newMessages) {
       if (DataManager.this.mCachedMessages != null)
         DataManager.this.mCachedMessages.add(newMessages);
    }
  }

在这种情况下,如果WriteSingleMessageAsyncTask正在执行,并且WriteMessageAsyncTask已经执行了至少一次写操作,则WriteSingleMessageAsyncTask可能会调用dbWriteMessage,在DataManager上获取锁,但由于保留锁而被阻塞无法完成写操作。WriteMessageAsyncTask重复获取和释放DataManager上的锁,这是一个问题。
要点:将事务和单例对象级别的锁定组合在一起可能会导致死锁。确保在开始事务之前拥有对象级别的锁。
对于我原来的WriteMessageAsyncTask类的修复:
   synchronized(DataManager.this) {
     DataManager.this.mDb.execSQL("BEGIN TRANSACTION;");
     DataManager.this.dbWriteMessage(args[0]);
     // More possibly expensive DB writes
     DataManager.this.mDb.execSQL("COMMIT TRANSACTION;");
   }

更新2

请查看来自Google I/O 2012的视频: http://youtu.be/gbQb1PVjfqM?t=19m13s

该视频提出了一种设计模式,利用内置的独占事务,然后使用yieldIfContendedSafely函数。


如果我可以在性能方面提出建议:我注意到一件事情,那就是一旦您编写新消息,似乎会读取数据库中的整个消息并设置mCachedMessages。相反,您可以将新添加的消息简单地添加到mCachedMessages列表中,这将节省大量的数据库读取。 - Aswin Kumar
@AswinKumar 这是正确的;我想这只是一个简化的例子,代表了同步最佳实践。性能优化确实是将其添加到已经存在的缓存列表中。 - esilver
3个回答

2

关于同步/死锁部分我无法提供太多信息,这将严重依赖于您代码的其余部分。由于 DataManager 类并不真正与 UI 进行交互,您可能需要使用服务 (IntentService) 而不是 AsyncTask。当同步完成时,您可以显示通知。如果您没有调用 UI 代码,则实际上不需要 onPostExecute()


假设UI确实与DataManager进行交互 - 即当Activity启动、恢复或接收到新消息时,UI线程将调用dataManager.readMessages()以检查是否有更新可用。 - esilver
重要的是,DataManager 是否直接涉及 UI 线程,例如更新 TextView 上的文本等。如果某个 Activity 方法通过 DataManager 访问全局缓存数据,则不算与 UI 的交互 :) - Nikolay Elenkov
1
我正在寻找一种设计模式,使UI线程能够与客户端SQLite数据库进行交互,该数据库可能具有批量插入、快速插入和读取,并且不会阻塞UI线程。虽然可以通过IntentService处理所有SQLite数据库访问,但这似乎有些过度。 - esilver
我不确定你的系统设置,但是你似乎正在做这个:在启动时将数据从数据库读入缓存,然后从活动中使用该缓存。我建议你将填充缓存/同步移动到服务中。所有活动中的直接读取(如果有)应该根据需要使用 AsyncTask - Nikolay Elenkov

1

您可能需要考虑来自SDK的这些信息(http://developer.android.com/reference/android/os/AsyncTask.html)

在首次引入时,AsyncTasks在单个后台线程上串行执行。从DONUT开始,这已更改为一组线程,允许多个任务并行操作。从HONEYCOMB开始,任务在单个线程上执行,以避免由并行执行引起的常见应用程序错误。

如果您真正想要并行执行,可以使用THREAD_POOL_EXECUTOR调用executeOnExecutor(java.util.concurrent.Executor,Object[])。


0

提醒一下,即使您没有指定事务,SQLite 上运行的每个 SQL 语句都会在事务下运行。

如果您正在进行SQLite 批量插入操作,请查看以下主题:

  1. Android 数据库事务
  2. SQLite 批量插入

2
谢谢 - 第一个链接是使用事务的好介绍,第二个链接适用于性能。我正在寻找一个严格正确的设计模式来处理批量插入、短插入和读取,并且不会阻塞UI线程。 - esilver

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